- 确认端点单元测试:有效/复用/过期/串用/乱写 token 各测一遍,token 即凭证不经 Auth - WebUI 重建:对齐新 API 的操作类型(含 delete_user)、用户生命周期状态视图、 概览含离职待确认列表与被跳过告警、「立即快照」与「立即物化」触发按钮、 部门分组展示 groupId;重编译 app.css/app.js 并提交产物 - 界面文案改指 LLDAP(不再提 OpenLDAP),确认弹窗说明离职才物理删除
105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
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())
|
|
}
|
|
} |