package server // 一次性离职确认端点的单元测试:有效/过期/复用/串用 token 各测一遍。 // 该端点不经 Auth 中间件,token 即凭证,因此测试直接打到 Server 的 mux。 import ( "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "idsync/internal/config" "idsync/internal/store" ) func newConfirmServer(t *testing.T, st *store.Store) *Server { t.Helper() auth, err := NewAuth("admin", nil, "pw", make([]byte, 32), false) if err != nil { t.Fatalf("构造认证失败: %v", err) } return New(&Deps{Store: st, Auth: auth, Cfg: &config.Config{}}) } func confirmGET(s *Server, uid, token string) *httptest.ResponseRecorder { req := httptest.NewRequest("GET", "/api/resign/confirm?uid="+uid+"&token="+token, nil) rec := httptest.NewRecorder() s.mux.ServeHTTP(rec, req) return rec } func seedPendingUser(t *testing.T) *store.Store { t.Helper() st, err := store.Open(filepath.Join(t.TempDir(), "idsync.db")) if err != nil { t.Fatalf("打开 store 失败: %v", err) } if err := st.ReplaceMirror(&store.MirrorSnapshot{ Users: []*store.MirrorUser{{UID: "alice", UnionID: "u-alice", Email: "a@x.com"}}, }); err != nil { t.Fatalf("灌镜像失败: %v", err) } for _, fn := range []func() error{ func() error { return st.ToLeaveCandidate("alice") }, func() error { return st.ToPendingConfirm("alice") }, } { if err := fn(); err != nil { t.Fatalf("状态转移失败: %v", err) } } return st } func assertState(t *testing.T, st *store.Store, want string) { t.Helper() m, _ := st.MapUsersByUID() if u := m["alice"]; u == nil || u.State != want { t.Fatalf("alice state = %+v, 期望 %s", m["alice"], want) } } func TestResignConfirmEndpoint(t *testing.T) { st := seedPendingUser(t) defer st.Close() s := newConfirmServer(t, st) // 有效 token:确认成功,状态置 confirmed_leave。 valid, err := st.CreateToken("alice", time.Hour) if err != nil { t.Fatalf("创建 token 失败: %v", err) } rec := confirmGET(s, "alice", valid.Token) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "已确认离职") { t.Errorf("有效 token 应成功,code=%d body=%q", rec.Code, rec.Body.String()) } assertState(t, st, store.StateConfirmedLeave) // 复用同一 token:一次使用,必须被拒。 rec = confirmGET(s, "alice", valid.Token) if !strings.Contains(rec.Body.String(), "确认链接无效") { t.Errorf("复用已用 token 应被拒,body=%q", rec.Body.String()) } // 过期 token:拒绝。 expired, _ := st.CreateToken("alice", -time.Minute) rec = confirmGET(s, "alice", expired.Token) if !strings.Contains(rec.Body.String(), "确认链接无效") { t.Errorf("过期 token 应被拒,body=%q", rec.Body.String()) } // uid 与 token 不一致(串用):拒绝。 other, _ := st.CreateToken("alice", time.Hour) rec = confirmGET(s, "bob", other.Token) if !strings.Contains(rec.Body.String(), "确认链接无效") { t.Errorf("uid 与 token 不一致应被拒,body=%q", rec.Body.String()) } // 乱写的 token:拒绝。 rec = confirmGET(s, "alice", "garbage-token") if !strings.Contains(rec.Body.String(), "确认链接无效") { t.Errorf("无效 token 应被拒,body=%q", rec.Body.String()) } }