feat: update skill fix friend request reject
This commit is contained in:
parent
1edcb959b7
commit
d5c0d81e22
15
.cursor/rules/git-commit-gate.mdc
Normal file
15
.cursor/rules/git-commit-gate.mdc
Normal 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`).
|
||||
@ -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.
|
||||
|
||||
10
Makefile
10
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"
|
||||
|
||||
@ -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 # 本机只需一次
|
||||
```
|
||||
|
||||
@ -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"
|
||||
|
||||
44
internal/module/auth/logic/refresh_test.go
Normal file
44
internal/module/auth/logic/refresh_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
55
internal/module/auth/logic/static_test.go
Normal file
55
internal/module/auth/logic/static_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
9
scripts/git-hooks/pre-commit
Executable 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
|
||||
Loading…
Reference in New Issue
Block a user