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 端点
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//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 }
|
||||
Reference in New Issue
Block a user