feat(M3): 申请审批 + 邮件队列 — 公开申请、管理员审批自动建号、mail_logs 落库重试
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user