From d5c0d81e22d3f16a51ac4112b5861fbefee87285 Mon Sep 17 00:00:00 2001 From: zhr Date: Thu, 24 Sep 2026 15:25:23 +0800 Subject: [PATCH] feat: update skill fix friend request reject --- .cursor/rules/git-commit-gate.mdc | 15 ++ AGENTS.md | 1 + Makefile | 10 +- docs/daily/2026-09-24-change-list.md | 44 ++++ internal/bootstrap/app.go | 4 +- internal/module/auth/logic/refresh_test.go | 44 ++++ internal/module/auth/logic/static_test.go | 55 +++++ internal/module/client/logic/service.go | 37 ++-- internal/module/client/logic/service_test.go | 209 +++++++++++++++++- internal/module/client/model/repo.go | 17 +- internal/module/friend/logic/friend_test.go | 6 +- .../transport/http/handler/admin/client.go | 4 +- .../transport/http/handler/friend/handler.go | 1 - scripts/git-hooks/pre-commit | 9 + 14 files changed, 428 insertions(+), 28 deletions(-) create mode 100644 .cursor/rules/git-commit-gate.mdc create mode 100644 internal/module/auth/logic/refresh_test.go create mode 100644 internal/module/auth/logic/static_test.go create mode 100755 scripts/git-hooks/pre-commit diff --git a/.cursor/rules/git-commit-gate.mdc b/.cursor/rules/git-commit-gate.mdc new file mode 100644 index 0000000..b56161c --- /dev/null +++ b/.cursor/rules/git-commit-gate.mdc @@ -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`). diff --git a/AGENTS.md b/AGENTS.md index 54971b0..4b50397 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/Makefile b/Makefile index 4f8ef5d..8159f9d 100644 --- a/Makefile +++ b/Makefile @@ -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" diff --git a/docs/daily/2026-09-24-change-list.md b/docs/daily/2026-09-24-change-list.md index 847434f..6e8e070 100644 --- a/docs/daily/2026-09-24-change-list.md +++ b/docs/daily/2026-09-24-change-list.md @@ -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 # 本机只需一次 +``` diff --git a/internal/bootstrap/app.go b/internal/bootstrap/app.go index 612e115..cd36106 100644 --- a/internal/bootstrap/app.go +++ b/internal/bootstrap/app.go @@ -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" diff --git a/internal/module/auth/logic/refresh_test.go b/internal/module/auth/logic/refresh_test.go new file mode 100644 index 0000000..443a431 --- /dev/null +++ b/internal/module/auth/logic/refresh_test.go @@ -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) + } +} diff --git a/internal/module/auth/logic/static_test.go b/internal/module/auth/logic/static_test.go new file mode 100644 index 0000000..4654aa1 --- /dev/null +++ b/internal/module/auth/logic/static_test.go @@ -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) + } +} diff --git a/internal/module/client/logic/service.go b/internal/module/client/logic/service.go index 48abe78..7929f5a 100644 --- a/internal/module/client/logic/service.go +++ b/internal/module/client/logic/service.go @@ -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"}, } } diff --git a/internal/module/client/logic/service_test.go b/internal/module/client/logic/service_test.go index 60d8506..92d8df4 100644 --- a/internal/module/client/logic/service_test.go +++ b/internal/module/client/logic/service_test.go @@ -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) + } +} diff --git a/internal/module/client/model/repo.go b/internal/module/client/model/repo.go index e31c4d6..25db4ff 100644 --- a/internal/module/client/model/repo.go +++ b/internal/module/client/model/repo.go @@ -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, diff --git a/internal/module/friend/logic/friend_test.go b/internal/module/friend/logic/friend_test.go index 8015b6a..5795550 100644 --- a/internal/module/friend/logic/friend_test.go +++ b/internal/module/friend/logic/friend_test.go @@ -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") } } diff --git a/internal/transport/http/handler/admin/client.go b/internal/transport/http/handler/admin/client.go index 46fd79c..32b4093 100644 --- a/internal/transport/http/handler/admin/client.go +++ b/internal/transport/http/handler/admin/client.go @@ -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 } diff --git a/internal/transport/http/handler/friend/handler.go b/internal/transport/http/handler/friend/handler.go index 9b8b82d..d39ed29 100644 --- a/internal/transport/http/handler/friend/handler.go +++ b/internal/transport/http/handler/friend/handler.go @@ -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 { diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit new file mode 100755 index 0000000..9f15fef --- /dev/null +++ b/scripts/git-hooks/pre-commit @@ -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