//go:build integration // lldapgql 的集成冒烟测试:对真实 LLDAP(本地容器)跑通 // 登录 → 建用户 → 建组 → 加组成员 → 更新(省略即不改)→ 查询 → 移除成员 → 删除。 // // 依赖环境: // LLDAP_GQL_URL 默认 http://127.0.0.1:17170 // LLDAP_ADMIN_USER 默认 admin // LLDAP_ADMIN_PASS 默认 adminpass package lldapgql_test import ( "context" "fmt" "io" "log/slog" "os" "testing" "time" "idsync/internal/lldapgql" ) func env(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func newClient(t *testing.T) *lldapgql.Client { t.Helper() log := slog.New(slog.NewTextHandler(io.Discard, nil)) c := lldapgql.New(env("LLDAP_GQL_URL", "http://127.0.0.1:17170"), log) if err := c.Login(context.Background(), env("LLDAP_ADMIN_USER", "admin"), env("LLDAP_ADMIN_PASS", "adminpass")); err != nil { t.Skipf("无法登录 LLDAP,跳过集成测试: %v", err) } return c } func suffix() string { return fmt.Sprintf("%d", time.Now().UnixNano()%100000000) } // TestLLDAPCRUD 覆盖用户/分组/成员关系的完整生命周期。 func TestLLDAPCRUD(t *testing.T) { c := newClient(t) ctx := context.Background() uid := "idsync-smoke-" + suffix() grp := "idsync-grp-" + suffix() // 1. 建用户(不设密码)。 email := uid + "@example.com" err := c.CreateUser(ctx, &lldapgql.CreateUserInput{ ID: uid, Email: sp(email), DisplayName: sp("冒烟用户"), FirstName: sp("冒烟"), LastName: sp("用户"), }) if err != nil { t.Fatalf("createUser 失败: %v", err) } t.Cleanup(func() { _ = c.DeleteUser(context.Background(), uid) }) // 2. 建组。 gid, err := c.CreateGroup(ctx, grp) if err != nil { t.Fatalf("createGroup 失败: %v", err) } t.Cleanup(func() { _ = c.DeleteGroup(context.Background(), gid) }) // 3. 加组成员。注意 addUserToGroup 对重复添加会以 UNIQUE 约束报错(非幂等), // 因此依赖材料化阶段的 diff 保证只加不存在的成员,不在此层做幂等掩盖。 if err := c.AddUserToGroup(ctx, uid, gid); err != nil { t.Fatalf("addUserToGroup 失败: %v", err) } // 4. 查询侧验证:按 uid 找用户、按 displayName 找组。 users, err := c.Users(ctx) if err != nil { t.Fatalf("List users 失败: %v", err) } found := false for _, u := range users { if u.ID == uid { found = true if u.Email != email { t.Errorf("用户 email = %q, 期望 %q", u.Email, email) } } } if !found { t.Fatalf("未在 users 查询中找到 uid=%s", uid) } groups, err := c.Groups(ctx) if err != nil { t.Fatalf("List groups 失败: %v", err) } gFound := false for _, g := range groups { if g.DisplayName == grp { gFound = true if g.ID != gid { t.Errorf("组 id 不匹配") } } } if !gFound { t.Fatalf("未在 groups 查询中找到 %s", grp) } // 5. 更新:只改 email,displayName 应保持不变(省略即不改)。 if err := c.UpdateUser(ctx, &lldapgql.UpdateUserInput{ ID: uid, Email: sp("new-" + email), }); err != nil { t.Fatalf("updateUser 失败: %v", err) } users, _ = c.Users(ctx) for _, u := range users { if u.ID == uid { if u.Email != "new-"+email { t.Errorf("email 未更新: %q", u.Email) } if u.DisplayName != "冒烟用户" { t.Errorf("省略的 displayName 被改了: %q", u.DisplayName) } } } // 6. 移除成员 + 删除用户(验证 deleteUser 成功清掉成员关系)。 if err := c.RemoveUserFromGroup(ctx, uid, gid); err != nil { t.Fatalf("removeUserFromGroup 失败: %v", err) } if err := c.DeleteUser(ctx, uid); err != nil { t.Fatalf("deleteUser 失败: %v", err) } if err := c.DeleteGroup(ctx, gid); err != nil { t.Fatalf("deleteGroup 失败: %v", err) } } func sp(s string) *string { return &s }