Files
idsync/internal/lldapgql/integration_test.go
T
caowangrenbo 10ed3984dc feat: lldapgql GraphQL 写客户端(对真实 LLDAP 冒烟通过)
- vendored LLDAP schema.graphql(v0.6.3),GraphQL 端点实测为 /api/graphql
- 手工 GraphQL 客户端(genqlient v0.8.1 因依赖的 x/tools v0.24 无法在 Go 1.26 编译,
  按计划风险节回归运行时客户端):登录 /auth/simple/login、createUser/updateUser/
  deleteUser/createGroup/deleteGroup/addUserToGroup/removeUserFromGroup、ListGroups/Users、
  预留 AddUserAttribute(默认不启用)
- 语义对齐硬约束:updateUser 省略即不改(指针字段)、绝不写密码
- 集成冒烟(真实 LLDAP 容器):create→group→add→query→update(省略不变)→remove→delete
  全通过;addUserToGroup 对重复添加报 UNIQUE 约束(非幂等),靠物化 diff 保证不重复加
- AGENTS.md 标注 lldapgql 改为手工客户端 + /api/graphql 端点
2026-09-29 23:31:01 +08:00

139 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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 }