feat: update skill fix friend request reject

This commit is contained in:
zhr 2026-09-24 15:25:23 +08:00
parent 1edcb959b7
commit d5c0d81e22
14 changed files with 428 additions and 28 deletions

View File

@ -0,0 +1,15 @@
---
description: Require make test before git commit; never bypass pre-commit without explicit user request
alwaysApply: true
---
# Git commit gate
Before **`git commit`** (including when the user asks the agent to commit):
1. From the repo root, run **`make test`** and confirm exit code **0** (`make test` already runs `make lint` first).
2. Only then stage and commit. Do **not** commit while tests are failing or were not run for this change set.
3. Do **not** use `git commit --no-verify` / `-n` unless the user **explicitly** requests skipping hooks in that message.
4. If the local repo has no pre-commit hook yet, run **`make install-git-hooks`** once (installs `scripts/git-hooks/pre-commit`).
Docs-only commits with zero compile/test surface may skip `make test` only when the change cannot affect build or CI (same bar as `verify-before-done.mdc`).

View File

@ -11,5 +11,6 @@ Before writing code:
5. MySQL: all tables `t_*`; migrations replay-all — details in `.cursor/skills/spark-im-v3/mysql.md`. After changing `migrations/*.sql`, run `go run ./cmd/migrate -config configs/config.yaml` yourself.
6. Before finishing any code change, run `make test` at the repo root (runs `lint` first, logic coverage ≥65%). Do not report the task as OK until it passes. `make deploy` chains through `test`.
7. After edits that touch the repo, load `.cursor/skills/daily-change-list/SKILL.md` and append `docs/daily/{yyyy}-{mm}-{dd}-change-list.md` before handoff.
8. Before **`git commit`**, `make test` must pass; install enforcement with **`make install-git-hooks`** (see `.cursor/rules/git-commit-gate.mdc`). Do not use `--no-verify` unless the user explicitly asks.
Do not introduce Kafka, K8s, microservices, ConversationSeq, application-layer handshake crypto, or public gnet ports.

View File

@ -9,7 +9,7 @@ GOLANGCI_LINT := golangci-lint
CONFIG ?= configs/config.yaml
LOGIC_COVER_MIN ?= 65
.PHONY: all build image test lint fmt vet tidy migrate seed start run deploy deploy-rollback
.PHONY: all build image test lint fmt vet tidy migrate seed start run deploy deploy-rollback install-git-hooks
all: test build
@ -77,3 +77,11 @@ deploy: test
deploy-rollback:
bash deploy/docker/rollback.sh
# Copy pre-commit hook: runs `make test` before every commit (see .cursor/rules/git-commit-gate.mdc).
install-git-hooks:
@test -d .git || { echo "not a git repository" >&2; exit 1; }
@mkdir -p .git/hooks
@cp scripts/git-hooks/pre-commit .git/hooks/pre-commit
@chmod +x scripts/git-hooks/pre-commit .git/hooks/pre-commit
@echo "Installed .git/hooks/pre-commit → make test"

View File

@ -76,3 +76,47 @@
### 关联
- Spark Messenger 全页面接入检查
## 14:48 — 修复 make lint / make test
### 概要
- 修复 `friend_test` 中 `Accept` 双返回值与测试邮箱 OTP
- 修复 `client` 模块 goconst/gocritic/goimports;`UpsertLegal`/`UpsertPlan` 改为指针入参
- 补充 `client`/`auth` logic 单测以满足 65% 覆盖率门禁
### 主要文件
- `internal/module/friend/logic/friend_test.go`
- `internal/module/client/logic/service.go`、`model/repo.go`
- `internal/module/client/logic/service_test.go`
- `internal/module/auth/logic/refresh_test.go`、`static_test.go`
- `internal/bootstrap/app.go`、`handler/friend/handler.go`、`handler/admin/client.go`
### 验证
- `make lint`: pass
- `make test`: pass
## 15:22 — Git commit 前必须 make test
### 概要
- 新增 always-on 规则 `git-commit-gate.mdc`:Agent 与用户 commit 前须 `make test` 通过,禁止擅自 `--no-verify`
- 新增 `scripts/git-hooks/pre-commit` 与 `make install-git-hooks` 安装到 `.git/hooks`
### 主要文件
- `.cursor/rules/git-commit-gate.mdc`
- `scripts/git-hooks/pre-commit`
- `Makefile`、`AGENTS.md`
### 验证
- `make test`: pass
### 使用
```bash
make install-git-hooks # 本机只需一次
```

View File

@ -23,6 +23,8 @@ import (
adminlogic "spark-server/internal/module/admin/logic"
adminmodel "spark-server/internal/module/admin/model"
authlogic "spark-server/internal/module/auth/logic"
clientlogic "spark-server/internal/module/client/logic"
clientmodel "spark-server/internal/module/client/model"
convlogic "spark-server/internal/module/conversation/logic"
convmodel "spark-server/internal/module/conversation/model"
deliverlogic "spark-server/internal/module/deliver/logic"
@ -43,8 +45,6 @@ import (
momentmodel "spark-server/internal/module/moment/model"
musiclogic "spark-server/internal/module/music/logic"
musicmodel "spark-server/internal/module/music/model"
clientlogic "spark-server/internal/module/client/logic"
clientmodel "spark-server/internal/module/client/model"
smslogic "spark-server/internal/module/sms/logic"
squarelogic "spark-server/internal/module/square/logic"
squaremodel "spark-server/internal/module/square/model"

View File

@ -0,0 +1,44 @@
package logic
import (
"testing"
"time"
)
func TestParsePairKey(t *testing.T) {
did, aid, ok := parsePairKey("1:9:2:3", "1:")
if !ok || did != 9 || aid != 2 {
t.Fatalf("triple %+v %v %v", did, aid, ok)
}
did, aid, ok = parsePairKey("1:9:0", "1:")
if !ok || did != 9 || aid != 0 {
t.Fatalf("pair mode key %+v %v %v", did, aid, ok)
}
if _, _, ok := parsePairKey("x", "p:"); ok {
t.Fatal("bad key")
}
}
func TestRedisPairsDeleteAllOnDeviceNil(t *testing.T) {
p := NewRedisPairs(nil, "")
ctx := t.Context()
if err := p.DeleteAllOnDevice(ctx, 1, 1); err == nil {
t.Fatal("expected unavailable")
}
if _, err := p.ListByUser(ctx, 1); err == nil {
t.Fatal("expected list error")
}
}
func TestMemoryPairsDeleteAllOnDevicePairMode(t *testing.T) {
p := NewMemoryPairsMode(PairKeyPair)
ctx := t.Context()
_ = p.Put(ctx, 1, 9, 0, TokenPair{Access: "a", Refresh: "r"}, time.Minute)
if err := p.DeleteAllOnDevice(ctx, 1, 9); err != nil {
t.Fatal(err)
}
got, err := p.Get(ctx, 1, 9, 0)
if err != nil || got.Access != "" {
t.Fatalf("cleared %+v %v", got, err)
}
}

View File

@ -0,0 +1,55 @@
package logic
import (
"context"
"testing"
"time"
)
func TestStaticUserAuthenticateEmail(t *testing.T) {
s := StaticUser{Email: "ops@spark.com", Code: "42", UserID: 99, SparkID: "sp99"}
ctx := context.Background()
u, err := s.AuthenticateEmail(ctx, "ops@spark.com", "42", nil)
if err != nil || u.ID != 99 || u.SparkID != "sp99" {
t.Fatalf("ok %+v %v", u, err)
}
if _, err := s.AuthenticateEmail(ctx, "other@spark.com", "42", nil); err == nil {
t.Fatal("wrong email")
}
if _, err := s.AuthenticateEmail(ctx, "ops@spark.com", "bad", nil); err == nil {
t.Fatal("wrong code")
}
}
func TestNewRedisPairsDefaultPrefix(t *testing.T) {
p := NewRedisPairsMode(nil, "", PairKeyTriple)
if p.prefix != "refresh:" {
t.Fatalf("prefix %q", p.prefix)
}
}
func TestMemoryPairsPutSkipsInvalid(t *testing.T) {
p := NewMemoryPairs()
ctx := t.Context()
if err := p.Put(ctx, 0, 1, 1, TokenPair{Access: "a"}, time.Minute); err != nil {
t.Fatal(err)
}
if err := p.Put(ctx, 1, 0, 1, TokenPair{Access: "a"}, time.Minute); err != nil {
t.Fatal(err)
}
got, err := p.Get(ctx, 1, 1, 1)
if err != nil || got.Access != "" {
t.Fatalf("empty %+v %v", got, err)
}
}
func TestMemoryPairsListByUserDropsExpired(t *testing.T) {
p := NewMemoryPairs()
ctx := t.Context()
_ = p.Put(ctx, 3, 4, 1, TokenPair{Access: "x"}, time.Millisecond)
time.Sleep(3 * time.Millisecond)
rows, err := p.ListByUser(ctx, 3)
if err != nil || len(rows) != 0 {
t.Fatalf("expired listed %+v %v", rows, err)
}
}

View File

@ -18,13 +18,13 @@ import (
type Repository interface {
GetLegal(ctx context.Context, lang string) (privacy, terms *entity.LegalDocument, err error)
UpsertLegal(ctx context.Context, d entity.LegalDocument) error
UpsertLegal(ctx context.Context, d *entity.LegalDocument) error
InsertFeedback(ctx context.Context, f *entity.Feedback) error
ListFeedback(ctx context.Context, page, size int) ([]entity.Feedback, int64, error)
UpdateFeedbackStatus(ctx context.Context, id int64, status string) error
ListPlans(ctx context.Context, lang string) ([]entity.SubscriptionPlan, error)
ListPlansAdmin(ctx context.Context, lang string) ([]entity.SubscriptionPlan, error)
UpsertPlan(ctx context.Context, p entity.SubscriptionPlan) error
UpsertPlan(ctx context.Context, p *entity.SubscriptionPlan) error
GetPasswordHash(ctx context.Context, userID int64) (string, error)
SetPasswordHash(ctx context.Context, userID int64, hash string) error
}
@ -70,8 +70,8 @@ func (s *Service) LegalAgreements(ctx context.Context) (*entity.LegalBundle, err
if err == nil && privacy != nil && terms != nil {
return &entity.LegalBundle{Privacy: *privacy, Terms: *terms}, nil
}
if lang != "zh-CN" {
privacy, terms, err = s.repo.GetLegal(ctx, "zh-CN")
if lang != string(i18n.ZhCN) {
privacy, terms, err = s.repo.GetLegal(ctx, string(i18n.ZhCN))
if err == nil && privacy != nil && terms != nil {
return &entity.LegalBundle{Privacy: *privacy, Terms: *terms}, nil
}
@ -80,10 +80,13 @@ func (s *Service) LegalAgreements(ctx context.Context) (*entity.LegalBundle, err
return defaultLegal(lang), nil
}
func (s *Service) UpsertLegalAdmin(ctx context.Context, d entity.LegalDocument) error {
func (s *Service) UpsertLegalAdmin(ctx context.Context, d *entity.LegalDocument) error {
if s.repo == nil {
return errs.ErrAvailable
}
if d == nil {
return errs.ErrInvalidArgument
}
d.Key = strings.TrimSpace(d.Key)
d.Lang = normalizeLang(d.Lang)
if d.Key != entity.DocPrivacy && d.Key != entity.DocTerms {
@ -97,9 +100,12 @@ func (s *Service) SubmitFeedback(ctx context.Context, in FeedbackInput) (*entity
return nil, err
}
f := &entity.Feedback{
Email: strings.TrimSpace(strings.ToLower(in.Email)),
Content: strings.TrimSpace(in.Content), AppVersion: strings.TrimSpace(in.AppVersion),
Language: normalizeLang(reqctx.Language(ctx)), Status: "open", CreatedAt: time.Now(),
Email: strings.TrimSpace(strings.ToLower(in.Email)),
Content: strings.TrimSpace(in.Content),
AppVersion: strings.TrimSpace(in.AppVersion),
Language: normalizeLang(reqctx.Language(ctx)),
Status: "open",
CreatedAt: time.Now(),
}
if rc := reqctx.From(ctx); rc != nil && rc.UserID > 0 {
uid := rc.UserID
@ -141,8 +147,8 @@ func (s *Service) SubscriptionPlans(ctx context.Context) (*entity.SubscriptionCo
if err == nil && len(plans) > 0 {
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: plans}, nil
}
if lang != "zh-CN" {
plans, err = s.repo.ListPlans(ctx, "zh-CN")
if lang != string(i18n.ZhCN) {
plans, err = s.repo.ListPlans(ctx, string(i18n.ZhCN))
if err == nil && len(plans) > 0 {
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: plans}, nil
}
@ -158,10 +164,13 @@ func (s *Service) ListPlansAdmin(ctx context.Context, lang string) ([]entity.Sub
return s.repo.ListPlansAdmin(ctx, strings.TrimSpace(lang))
}
func (s *Service) UpsertPlanAdmin(ctx context.Context, p entity.SubscriptionPlan) error {
func (s *Service) UpsertPlanAdmin(ctx context.Context, p *entity.SubscriptionPlan) error {
if s.repo == nil {
return errs.ErrAvailable
}
if p == nil {
return errs.ErrInvalidArgument
}
p.Lang = normalizeLang(p.Lang)
p.PlanID = strings.TrimSpace(p.PlanID)
switch p.PlanID {
@ -249,7 +258,7 @@ func normalizeLang(lang string) string {
case "zh-TW", "zh-HK":
return "zh-TW"
default:
return "zh-CN"
return string(i18n.ZhCN)
}
}
@ -264,9 +273,9 @@ func defaultLegal(lang string) *entity.LegalBundle {
}
default:
return &entity.LegalBundle{
Privacy: entity.LegalDocument{Key: entity.DocPrivacy, Lang: "zh-CN", Title: "隐私政策",
Privacy: entity.LegalDocument{Key: entity.DocPrivacy, Lang: string(i18n.ZhCN), Title: "隐私政策",
Content: "SleepHeart 收集邮箱与设备数据用于登录与推荐,不会出售个人信息。您可随时注销账号。", Version: "1.0"},
Terms: entity.LegalDocument{Key: entity.DocTerms, Lang: "zh-CN", Title: "用户协议",
Terms: entity.LegalDocument{Key: entity.DocTerms, Lang: string(i18n.ZhCN), Title: "用户协议",
Content: "使用 SleepHeart 即表示同意社区规范。会员权益以订阅页展示为准。", Version: "1.0"},
}
}

View File

@ -1,6 +1,137 @@
package logic
import "testing"
import (
"context"
"testing"
"spark-server/internal/module/client/entity"
userentity "spark-server/internal/module/user/entity"
"spark-server/internal/pkg/errs"
"spark-server/internal/pkg/i18n"
"spark-server/internal/pkg/reqctx"
)
type stubClientRepo struct {
legal map[string]map[string]*entity.LegalDocument
feedback []entity.Feedback
plans []entity.SubscriptionPlan
passwords map[int64]string
}
func (s *stubClientRepo) GetLegal(_ context.Context, lang string) (*entity.LegalDocument, *entity.LegalDocument, error) {
if s.legal == nil {
return nil, nil, errs.ErrNotFound
}
byLang := s.legal[lang]
if byLang == nil {
return nil, nil, errs.ErrNotFound
}
return byLang[entity.DocPrivacy], byLang[entity.DocTerms], nil
}
func (s *stubClientRepo) UpsertLegal(_ context.Context, d *entity.LegalDocument) error {
if s.legal == nil {
s.legal = map[string]map[string]*entity.LegalDocument{}
}
if s.legal[d.Lang] == nil {
s.legal[d.Lang] = map[string]*entity.LegalDocument{}
}
cp := *d
s.legal[d.Lang][d.Key] = &cp
return nil
}
func (s *stubClientRepo) InsertFeedback(_ context.Context, f *entity.Feedback) error {
f.ID = int64(len(s.feedback) + 1)
s.feedback = append(s.feedback, *f)
return nil
}
func (s *stubClientRepo) ListFeedback(_ context.Context, _, _ int) ([]entity.Feedback, int64, error) {
return s.feedback, int64(len(s.feedback)), nil
}
func (s *stubClientRepo) UpdateFeedbackStatus(_ context.Context, id int64, status string) error {
for i := range s.feedback {
if s.feedback[i].ID == id {
s.feedback[i].Status = status
return nil
}
}
return errs.ErrNotFound
}
func (s *stubClientRepo) ListPlans(_ context.Context, lang string) ([]entity.SubscriptionPlan, error) {
out := make([]entity.SubscriptionPlan, 0)
for i := range s.plans {
p := &s.plans[i]
if p.Lang == lang && p.Enabled {
out = append(out, *p)
}
}
return out, nil
}
func (s *stubClientRepo) ListPlansAdmin(_ context.Context, lang string) ([]entity.SubscriptionPlan, error) {
if lang == "" {
return s.plans, nil
}
out := make([]entity.SubscriptionPlan, 0)
for i := range s.plans {
p := &s.plans[i]
if p.Lang == lang {
out = append(out, *p)
}
}
return out, nil
}
func (s *stubClientRepo) UpsertPlan(_ context.Context, p *entity.SubscriptionPlan) error {
s.plans = append(s.plans, *p)
return nil
}
func (s *stubClientRepo) GetPasswordHash(_ context.Context, userID int64) (string, error) {
if s.passwords == nil {
return "", nil
}
return s.passwords[userID], nil
}
func (s *stubClientRepo) SetPasswordHash(_ context.Context, userID int64, hash string) error {
if s.passwords == nil {
s.passwords = map[int64]string{}
}
s.passwords[userID] = hash
return nil
}
type stubUserFinder struct {
byEmail map[string]*userentity.User
}
func (s stubUserFinder) GetByEmail(_ context.Context, email string) (*userentity.User, error) {
u := s.byEmail[userentity.NormalizeEmail(email)]
if u == nil {
return nil, errs.ErrNotFound
}
return u, nil
}
type stubOTP struct {
user *userentity.User
}
func (s stubOTP) AuthenticateEmail(context.Context, string, string, *userentity.LoginDevice) (*userentity.User, error) {
if s.user == nil {
return nil, errs.ErrUnauthorized
}
return s.user, nil
}
func ctxLang(lang string) context.Context {
return reqctx.With(context.Background(), &reqctx.RequestContext{Language: lang})
}
func TestLegalPlansFeedbackFallback(t *testing.T) {
svc := NewService(nil, nil, nil)
@ -17,3 +148,79 @@ func TestLegalPlansFeedbackFallback(t *testing.T) {
t.Fatalf("feedback %+v err=%v", fb, err)
}
}
func TestClientServiceAdminAndRepo(t *testing.T) {
repo := &stubClientRepo{
legal: map[string]map[string]*entity.LegalDocument{
string(i18n.ZhCN): {
entity.DocPrivacy: {Key: entity.DocPrivacy, Lang: string(i18n.ZhCN), Title: "P", Content: "c", Version: "1"},
entity.DocTerms: {Key: entity.DocTerms, Lang: string(i18n.ZhCN), Title: "T", Content: "c", Version: "1"},
},
},
plans: []entity.SubscriptionPlan{{PlanID: "month", Lang: string(i18n.ZhCN), Title: "月", Enabled: true}},
}
svc := NewService(repo, nil, nil)
if !svc.Ready() {
t.Fatal("ready")
}
ctx := ctxLang("en")
bundle, err := svc.LegalAgreements(ctx)
if err != nil || bundle.Privacy.Title != "P" {
t.Fatalf("legal %+v %v", bundle, err)
}
if err := svc.UpsertLegalAdmin(ctx, &entity.LegalDocument{Key: "bad", Lang: "en", Title: "x", Content: "y"}); err == nil {
t.Fatal("bad key")
}
if err := svc.UpsertLegalAdmin(ctx, &entity.LegalDocument{Key: entity.DocPrivacy, Lang: "en", Title: "Privacy", Content: "body"}); err != nil {
t.Fatal(err)
}
list, total, err := svc.ListFeedbackAdmin(ctx, 0, 10)
if err != nil || total != 0 || len(list) != 0 {
t.Fatalf("feedback list %+v %d %v", list, total, err)
}
_, err = svc.SubmitFeedback(ctx, FeedbackInput{Content: "ok"})
if err != nil {
t.Fatal(err)
}
if err := svc.UpdateFeedbackStatus(ctx, 1, "closed"); err != nil {
t.Fatal(err)
}
adminPlans, err := svc.ListPlansAdmin(ctx, string(i18n.ZhCN))
if err != nil || len(adminPlans) != 1 {
t.Fatalf("plans %+v %v", adminPlans, err)
}
if err := svc.UpsertPlanAdmin(ctx, &entity.SubscriptionPlan{PlanID: "week", Lang: "en", Title: "W", PriceCents: 100}); err != nil {
t.Fatal(err)
}
if err := svc.UpsertPlanAdmin(ctx, &entity.SubscriptionPlan{PlanID: "bad", Lang: "en"}); err == nil {
t.Fatal("bad plan")
}
}
func TestClientPasswordFlow(t *testing.T) {
repo := &stubClientRepo{}
user := &userentity.User{ID: 42, Status: userentity.StatusActive}
email := "real@example.com"
svc := NewService(repo, stubUserFinder{byEmail: map[string]*userentity.User{
userentity.NormalizeEmail(email): user,
}}, stubOTP{user: user})
ctx := ctxLang(string(i18n.ZhCN))
if err := svc.SetPassword(ctx, SetPasswordInput{Email: email, VerifyCode: "1", Password: "secret1"}); err != nil {
t.Fatal(err)
}
got, err := svc.VerifyPassword(ctx, email, "secret1")
if err != nil || got.ID != user.ID {
t.Fatalf("verify %+v %v", got, err)
}
if _, err := svc.VerifyPassword(ctx, email, "wrong"); err == nil {
t.Fatal("want auth fail")
}
testUser := &userentity.User{ID: 7, Status: userentity.StatusActive}
testEmail := "test01@spark.com"
svc2 := NewService(nil, stubUserFinder{byEmail: map[string]*userentity.User{
userentity.NormalizeEmail(testEmail): testUser,
}}, nil)
if _, err := svc2.VerifyPassword(ctx, testEmail, "any"); err != nil {
t.Fatalf("test email skip password %v", err)
}
}

View File

@ -2,6 +2,7 @@ package model
import (
"context"
"errors"
"time"
"gorm.io/gorm"
@ -90,7 +91,10 @@ func (r *Repo) GetLegal(ctx context.Context, lang string) (privacy, terms *entit
return privacy, terms, nil
}
func (r *Repo) UpsertLegal(ctx context.Context, d entity.LegalDocument) error {
func (r *Repo) UpsertLegal(ctx context.Context, d *entity.LegalDocument) error {
if d == nil {
return errors.New("client: nil legal document")
}
row := legalRow{
DocKey: d.Key, Lang: d.Lang, Title: d.Title, Content: d.Content,
Version: d.Version, UpdatedAt: time.Now(),
@ -135,7 +139,8 @@ func (r *Repo) ListFeedback(ctx context.Context, page, size int) ([]entity.Feedb
return nil, 0, err
}
out := make([]entity.Feedback, 0, len(rows))
for _, row := range rows {
for i := range rows {
row := &rows[i]
out = append(out, entity.Feedback{
ID: row.ID, UserID: row.UserID, Email: row.Email, Content: row.Content,
AppVersion: row.AppVersion, Language: row.Language, Status: row.Status, CreatedAt: row.CreatedAt,
@ -174,7 +179,8 @@ func (r *Repo) ListPlansAdmin(ctx context.Context, lang string) ([]entity.Subscr
func mapPlans(rows []planRow) []entity.SubscriptionPlan {
out := make([]entity.SubscriptionPlan, 0, len(rows))
for _, row := range rows {
for i := range rows {
row := &rows[i]
out = append(out, entity.SubscriptionPlan{
PlanID: row.PlanID, Lang: row.Lang, Title: row.Title, Subtitle: row.Subtitle,
PriceDisplay: row.PriceDisplay, PriceCents: row.PriceCents, Currency: row.Currency,
@ -185,7 +191,10 @@ func mapPlans(rows []planRow) []entity.SubscriptionPlan {
return out
}
func (r *Repo) UpsertPlan(ctx context.Context, p entity.SubscriptionPlan) error {
func (r *Repo) UpsertPlan(ctx context.Context, p *entity.SubscriptionPlan) error {
if p == nil {
return errors.New("client: nil subscription plan")
}
row := planRow{
PlanID: p.PlanID, Lang: p.Lang, Title: p.Title, Subtitle: p.Subtitle,
PriceDisplay: p.PriceDisplay, PriceCents: p.PriceCents, Currency: p.Currency,

View File

@ -288,11 +288,11 @@ func TestSystemGreeterWritesMessage(t *testing.T) {
func TestRejectFriendRequest(t *testing.T) {
svc, users, _ := testFriends(t)
ctx := context.Background()
a, err := users.AuthenticateEmail(ctx, "rej-a@spark.com", "000000", &userentity.LoginDevice{DeviceID: 1, AppID: 1, DeviceType: "ios"})
a, err := users.AuthenticateEmail(ctx, "test03@spark.com", "1", &userentity.LoginDevice{DeviceID: 1, AppID: 1, DeviceType: "ios"})
if err != nil {
t.Fatal(err)
}
b, err := users.AuthenticateEmail(ctx, "rej-b@spark.com", "000000", &userentity.LoginDevice{DeviceID: 2, AppID: 1, DeviceType: "ios"})
b, err := users.AuthenticateEmail(ctx, "test04@spark.com", "1", &userentity.LoginDevice{DeviceID: 2, AppID: 1, DeviceType: "ios"})
if err != nil {
t.Fatal(err)
}
@ -306,7 +306,7 @@ func TestRejectFriendRequest(t *testing.T) {
if err := svc.Reject(ctx, b.ID, RejectInput{RequestID: out.RequestID}); err != nil {
t.Fatal(err)
}
if err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: out.RequestID}); err == nil {
if _, err := svc.Accept(ctx, b.ID, AcceptInput{RequestID: out.RequestID}); err == nil {
t.Fatal("expected accept after reject to fail")
}
}

View File

@ -48,7 +48,7 @@ func (h *ClientHandler) PutLegal(c *gin.Context) {
Fail(c, http.StatusBadRequest, "无效 JSON")
return
}
if err := h.svc.UpsertLegalAdmin(c.Request.Context(), body); err != nil {
if err := h.svc.UpsertLegalAdmin(c.Request.Context(), &body); err != nil {
Fail(c, http.StatusBadRequest, err.Error())
return
}
@ -77,7 +77,7 @@ func (h *ClientHandler) PutPlan(c *gin.Context) {
Fail(c, http.StatusBadRequest, "无效 JSON")
return
}
if err := h.svc.UpsertPlanAdmin(c.Request.Context(), body); err != nil {
if err := h.svc.UpsertPlanAdmin(c.Request.Context(), &body); err != nil {
Fail(c, http.StatusBadRequest, err.Error())
return
}

View File

@ -65,7 +65,6 @@ func (h *Handler) ListRequests(c *gin.Context) {
respond.JSON(c, out)
}
func (h *Handler) Reject(c *gin.Context) {
uid, ok := currentUID(c)
if !ok {

9
scripts/git-hooks/pre-commit Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Runs before every git commit. Install: make install-git-hooks
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
echo "pre-commit: make test (lint + go test + logic coverage)…" >&2
make test