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/)不入库。
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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("缺少必要配置项时应加载失败")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user