package service import ( "context" "errors" "io" "log/slog" "strings" "testing" "time" "ws_usernode/internal/model" ) // recordingMail 捕获邮件发送,用于断言审批通知。 type recordingMail struct { lastTo string lastSubject string lastBody string } func (m *recordingMail) Send(_ context.Context, to, subject, body string) error { m.lastTo = to m.lastSubject = subject m.lastBody = body return nil } func testApprovalSvc(t *testing.T) (*ApprovalService, *UserService, *recordingMail) { t.Helper() db := testDB(t) sys := newFakeSys() users := NewUserService(db, sys, testConfig()) mailer := &recordingMail{} log := slog.New(slog.NewTextHandler(io.Discard, nil)) return NewApprovalService(db, testConfig(), users, mailer, log), users, mailer } func TestApprovalServiceSubmitValidate(t *testing.T) { svc, _, _ := testApprovalSvc(t) ctx := context.Background() // 非法用户名 / 邮箱 if _, err := svc.Submit(ctx, "Bad Name", "x@example.com", "", ""); err == nil { t.Fatal("invalid username should fail") } if _, err := svc.Submit(ctx, "okname", "not-an-email", "", ""); err == nil { t.Fatal("invalid email should fail") } // 合法提交 a, err := svc.Submit(ctx, "zhangsan", "zs@example.com", "prof.li", "科研") if err != nil { t.Fatalf("submit: %v", err) } if a.Status != model.StatusPending || a.UsernameRequested != "zhangsan" { t.Fatalf("approval = %+v", a) } // 同名待审批 → 冲突 if _, err := svc.Submit(ctx, "zhangsan", "z2@example.com", "", ""); !errors.Is(err, ErrUsernameTaken) { t.Fatalf("duplicate pending err = %v, want ErrUsernameTaken", err) } } func TestApprovalServiceReviewApproveCreatesUser(t *testing.T) { svc, users, mailer := testApprovalSvc(t) ctx := context.Background() a, err := svc.Submit(ctx, "lisi", "ls@example.com", "prof.wang", "毕设") if err != nil { t.Fatalf("submit: %v", err) } // 拒绝必须填理由 if _, err := svc.Review(ctx, a.ID, false, 1, " "); !errors.Is(err, ErrReasonRequired) { t.Fatalf("reject without reason err = %v, want ErrReasonRequired", err) } // 通过 → 自动建号(系统账号 + DB 用户)+ 通知邮件 got, err := svc.Review(ctx, a.ID, true, 1, "") if err != nil { t.Fatalf("approve: %v", err) } if got.Status != model.StatusApproved || got.ReviewerID == nil || *got.ReviewerID != 1 { t.Fatalf("approved = %+v", got) } u, err := users.GetByUsername(ctx, "ext_lisi") if err != nil { t.Fatalf("user not created: %v", err) } if u.Status != model.UserStatusActive { t.Fatalf("user status = %s", u.Status) } if mailer.lastTo != "ls@example.com" || !strings.Contains(mailer.lastBody, "ext_lisi") { t.Fatalf("approval mail = %s / %s", mailer.lastTo, mailer.lastBody) } // 默认 TTL(90 天)生效 if u.ExpireAt == nil || time.Until(*u.ExpireAt) < 80*24*time.Hour { t.Fatalf("expire_at = %v", u.ExpireAt) } // 重复审批 → 冲突 if _, err := svc.Review(ctx, a.ID, false, 1, "x"); !errors.Is(err, ErrApprovalReviewed) { t.Fatalf("re-review err = %v, want ErrApprovalReviewed", err) } // 用户名已被占用(已建号)→ 新申请冲突 if _, err := svc.Submit(ctx, "lisi", "x@example.com", "", ""); !errors.Is(err, ErrUsernameTaken) { t.Fatalf("resubmit taken err = %v, want ErrUsernameTaken", err) } } func TestApprovalServiceRejectAndResubmit(t *testing.T) { svc, _, mailer := testApprovalSvc(t) ctx := context.Background() a, err := svc.Submit(ctx, "wangwu", "ww@example.com", "", "") if err != nil { t.Fatalf("submit: %v", err) } got, err := svc.Review(ctx, a.ID, false, 1, "用途不明确") if err != nil { t.Fatalf("reject: %v", err) } if got.Status != model.StatusRejected || got.Reason != "用途不明确" { t.Fatalf("rejected = %+v", got) } if mailer.lastTo != "ww@example.com" || !strings.Contains(mailer.lastBody, "用途不明确") { t.Fatalf("reject mail = %s / %s", mailer.lastTo, mailer.lastBody) } // 被拒后可重新提交同名申请 if _, err := svc.Submit(ctx, "wangwu", "ww2@example.com", "", ""); err != nil { t.Fatalf("resubmit after reject: %v", err) } // 不存在 → ErrApprovalNotFound if _, err := svc.Review(ctx, 99999, true, 1, ""); !errors.Is(err, ErrApprovalNotFound) { t.Fatalf("missing review err = %v, want ErrApprovalNotFound", err) } } func TestApprovalServiceList(t *testing.T) { svc, _, _ := testApprovalSvc(t) ctx := context.Background() for _, n := range []string{"a1", "a2", "a3"} { if _, err := svc.Submit(ctx, n, n+"@example.com", "", ""); err != nil { t.Fatalf("submit %s: %v", n, err) } } rows, err := svc.List(ctx, model.StatusPending) if err != nil { t.Fatalf("list: %v", err) } if len(rows) != 3 { t.Fatalf("pending count = %d, want 3", len(rows)) } // 最新在前 if rows[0].UsernameRequested != "a3" { t.Fatalf("first = %s, want a3", rows[0].UsernameRequested) } if all, err := svc.List(ctx, ""); err != nil || len(all) != 3 { t.Fatalf("list all = %d, %v", len(all), err) } }