Files
idsync/internal/config/config_test.go
T
caowangrenbo 947b1a1475 chore: 初始化 idsync 项目
飞书通讯录 ↔ LDAP 同步工具的首个提交。

- internal/feishu: 飞书通讯录客户端、快照与重试
- internal/ldaplib: LDAP 读写与批量操作
- internal/mapping: 飞书部门/用户到 LDAP 条目的映射
- internal/syncer: 同步计划生成、执行与调度
- internal/store: SQLite 台账
- internal/server + internal/webui: 管理后台与内嵌前端产物
- internal/password: SSHA 口令处理
- config.example.yml / Dockerfile / Makefile / web 前端源码

真实凭据(config.yml)、运行时数据(data/、backup/)与构建产物(bin/)不入库。
2026-09-29 00:10:59 +08:00

211 lines
5.6 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.
package config
import (
"os"
"path/filepath"
"testing"
)
// writeConfig 写一份最小可用配置并加载它。
func writeConfig(t *testing.T, extra string) *Config {
t.Helper()
body := `
system:
data-dir: /tmp/idsync-test
server:
admin-user: admin
admin-password: pw
feishu:
app-id: a
app-secret: b
ldap:
url: ldap://localhost:389
base-dn: "dc=example,dc=org"
admin-dn: "cn=admin,dc=example,dc=org"
admin-pass: pw
` + extra
dir := t.TempDir()
path := filepath.Join(dir, "config.yml")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("写配置失败: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("加载配置失败: %v", err)
}
return cfg
}
// TestGroupSchemaDefaultsToUniqueMember 验证默认形态保持兼容。
func TestGroupSchemaDefaultsToUniqueMember(t *testing.T) {
cfg := writeConfig(t, "")
if cfg.Ldap.GroupObjectClass != "groupOfUniqueNames" {
t.Errorf("默认 objectClass = %q", cfg.Ldap.GroupObjectClass)
}
if cfg.Ldap.GroupMemberAttr != "uniqueMember" {
t.Errorf("默认成员属性 = %q", cfg.Ldap.GroupMemberAttr)
}
}
// TestGroupSchemaDerivesMemberAttr 验证只写 objectClass 时能自动推导出成员属性。
//
// 这是实际使用中最容易出错的地方:groupOfNames 必须配 member、
// groupOfUniqueNames 必须配 uniqueMember,写错会被 LDAP 以
// objectClassViolation 拒绝,而错误信息很不直观。
func TestGroupSchemaDerivesMemberAttr(t *testing.T) {
cases := []struct {
objectClass string
wantAttr string
}{
{"groupOfNames", "member"},
{"groupOfUniqueNames", "uniqueMember"},
// 大小写不敏感
{"GroupOfNames", "member"},
{"GROUPOFUNIQUENAMES", "uniqueMember"},
}
for _, c := range cases {
t.Run(c.objectClass, func(t *testing.T) {
cfg := writeConfig(t, " group-object-class: \""+c.objectClass+"\"\n")
if cfg.Ldap.GroupMemberAttr != c.wantAttr {
t.Errorf("objectClass=%s 推导出的成员属性 = %q, 期望 %q",
c.objectClass, cfg.Ldap.GroupMemberAttr, c.wantAttr)
}
})
}
}
// TestGroupSchemaRejectsMismatch 验证 objectClass 与成员属性不匹配时直接报错。
//
// 静默容忍会让问题推迟到运行时,表现为分组建好了但成员查不到,
// 排查成本远高于启动即失败。
func TestGroupSchemaRejectsMismatch(t *testing.T) {
cases := []struct {
name string
body string
}{
{
name: "groupOfNames 配 uniqueMember",
body: " group-object-class: \"groupOfNames\"\n group-member-attr: \"uniqueMember\"\n",
},
{
name: "groupOfUniqueNames 配 member",
body: " group-object-class: \"groupOfUniqueNames\"\n group-member-attr: \"member\"\n",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yml")
body := `
system:
data-dir: /tmp/idsync-test
server:
admin-user: admin
admin-password: pw
feishu:
app-id: a
app-secret: b
ldap:
url: ldap://localhost:389
base-dn: "dc=example,dc=org"
admin-dn: "cn=admin,dc=example,dc=org"
admin-pass: pw
` + c.body
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("写配置失败: %v", err)
}
if _, err := Load(path); err == nil {
t.Errorf("不匹配的组合应被拒绝,却加载成功")
}
})
}
}
// TestDerivedDNs 验证各 OU 拼接结果正确。
func TestDerivedDNs(t *testing.T) {
cfg := writeConfig(t, " user-ou: \"ou=users\"\n inactive-ou: \"ou=Inactive\"\n group-root-name: \"feishuroot\"\n")
if got := cfg.UserOUDN(); got != "ou=users,dc=example,dc=org" {
t.Errorf("UserOUDN = %q", got)
}
if got := cfg.InactiveOUDN(); got != "ou=Inactive,dc=example,dc=org" {
t.Errorf("InactiveOUDN = %q", got)
}
if got := cfg.GroupRootDN(); got != "ou=feishuroot,dc=example,dc=org" {
t.Errorf("GroupRootDN = %q", got)
}
}
// TestInactiveOUOutsideUserOU 是一个架构约束测试。
//
// ou=Inactive 必须位于用户 OU 之外,否则 Authelia(其 base 为 additional_users_dn)
// 依然能搜到离职用户,停用机制就失效了。
func TestInactiveOUOutsideUserOU(t *testing.T) {
cfg := writeConfig(t, " user-ou: \"ou=users\"\n inactive-ou: \"ou=Inactive\"\n")
userDN := cfg.UserOUDN()
inactiveDN := cfg.InactiveOUDN()
if inactiveDN == userDN {
t.Fatalf("离职 OU 与用户 OU 相同: %s", inactiveDN)
}
// 离职 OU 不应位于用户 OU 之下
if len(inactiveDN) > len(userDN) && inactiveDN[len(inactiveDN)-len(userDN):] == userDN {
t.Errorf("离职 OU (%s) 位于用户 OU (%s) 之下,Authelia 仍能搜到离职用户", inactiveDN, userDN)
}
}
// TestNormalizeRejectsMissingCredentials 验证关键字段缺失时启动即失败。
func TestNormalizeRejectsMissingCredentials(t *testing.T) {
cases := []struct {
name string
over string
}{
{"缺少 app-id", " app-id: \"\"\n"},
{"缺少 base-dn", "BASE_DN_MARKER"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yml")
feishuID, baseDN := "app", `"dc=example,dc=org"`
if c.name == "缺少 app-id" {
feishuID = ""
}
if c.name == "缺少 base-dn" {
baseDN = `""`
}
body := `
system:
data-dir: /tmp/idsync-test
server:
admin-user: admin
admin-password: pw
feishu:
app-id: "` + feishuID + `"
app-secret: b
ldap:
url: ldap://localhost:389
base-dn: ` + baseDN + `
admin-dn: "cn=admin,dc=example,dc=org"
admin-pass: pw
`
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("写配置失败: %v", err)
}
if _, err := Load(path); err == nil {
t.Errorf("缺少必要配置项时应加载失败")
}
})
}
}