refactor: config + lldaplib 收敛到 LLDAP 形态(读只读、删 OpenLDAP 写路径与密码包)

- config 重写:删 user-ou/inactive-ou/group-root-* / group-object-class /
  group-member-attr / initial-password;新增 lldap 写通道 + webhook + confirm-base-url
  + 两套 schedule + guard
- ldaplib 收拢为只读:属性常量/DN 归入 attrs.go;删 ops.go 写路径与 internal/password
- config.example.yml 重写成 LLDAP 形态
- 组命名修订(呼应 AGENTS.md 约束12):用部门稳定索引 cn=<dept_id> 做组身份,
  mirror_departments 增 group_id 列并跨快照保留,可读中文名只走 DB 展示
This commit is contained in:
2026-09-29 23:48:49 +08:00
parent 10ed3984dc
commit 979a98d502
16 changed files with 387 additions and 1031 deletions
+26 -63
View File
@@ -3,80 +3,43 @@ package mapping
import (
"strings"
"testing"
"idsync/internal/feishu"
)
// TestFlatDeptInfoParentChild 验证拍平拼音链:子部门链 = 父链.子拼音。
func TestFlatDeptInfoParentChild(t *testing.T) {
depts := []*feishu.Department{
dept("od-root", "0", "Acme"),
dept("od-rd", "od-root", "研发部"),
dept("od-backend", "od-rd", "后端组"),
}
out := FlatDeptInfo(depts, "dc=example,dc=com")
if out["od-root"].Chain != "Acme" || out["od-root"].Level != 0 {
t.Errorf("根部门 = %+v", out["od-root"])
}
if out["od-rd"].Chain != "Acme.yanfabu" || out["od-rd"].Level != 1 {
t.Errorf("研发部 = %+v", out["od-rd"])
}
if out["od-backend"].Chain != "Acme.yanfabu.houduanzu" || out["od-backend"].Level != 2 {
t.Errorf("后端组 = %+v", out["od-backend"])
}
wantDN := "cn=Acme.yanfabu.houduanzu,ou=groups,dc=example,dc=com"
if out["od-backend"].DN != wantDN {
t.Errorf("后端组 DN = %q, 期望 %q", out["od-backend"].DN, wantDN)
// TestDeptGroupDN 验证部门组 DN 直接用部门稳定索引(dept_id),不编码祖先路径。
func TestDeptGroupDN(t *testing.T) {
got := DeptGroupDN("od-yanfa-1", "dc=example,dc=com")
want := "cn=od-yanfa-1,ou=groups,dc=example,dc=com"
if got != want {
t.Errorf("DeptGroupDN = %q, 期望 %q", got, want)
}
}
// TestFlatDeptInfoFallsBackOnEmptyPinyin 验证纯符号部门名回落部门 ID,链非空且稳定。
func TestFlatDeptInfoFallsBackOnEmptyPinyin(t *testing.T) {
depts := []*feishu.Department{
dept("od-root", "0", "Acme"),
dept("od-sym", "od-root", "★☆"),
}
out := FlatDeptInfo(depts, "dc=example,dc=com")
if out["od-sym"].Chain != "Acme.dept-od-sym" {
t.Errorf("纯符号部门链 = %q, 期望回落部门 ID", out["od-sym"].Chain)
}
if !strings.Contains(out["od-sym"].DN, out["od-sym"].Chain) {
t.Errorf("DN 未包含 Chain: %q", out["od-sym"].DN)
// TestDeptGroupDNStableAcrossRename 验证部门改名(中文名变化)不影响组 DN——
// 组身份绑在索引上,改名不引起身份漂移。
func TestDeptGroupDNStableAcrossRename(t *testing.T) {
a := DeptGroupDN("od-123", "dc=example,dc=com")
// 无论部门叫「研发部」还是「产品部」,索引不变,DN 不变。
b := DeptGroupDN("od-123", "dc=example,dc=com")
if a != b {
t.Errorf("同索引的组 DN 应稳定不变: %q vs %q", a, b)
}
}
// TestFlatDeptInfoDisambiguatesCollision 验证同名部门消歧:链撞车时追加部门 ID,组名保持唯一。
func TestFlatDeptInfoDisambiguatesCollision(t *testing.T) {
depts := []*feishu.Department{
dept("od-root", "0", "Acme"),
dept("od-a", "od-root", "开发组"),
dept("od-b", "od-root", "开发组"),
}
out := FlatDeptInfo(depts, "dc=example,dc=com")
if out["od-a"].Chain != "Acme.kaifazu" {
t.Errorf("od-a 链 = %q", out["od-a"].Chain)
}
if out["od-b"].Chain != "Acme.kaifazu.od-b" {
t.Errorf("od-b 链 = %q, 期望追加部门 ID 消歧", out["od-b"].Chain)
}
if out["od-a"].DN == out["od-b"].DN {
t.Errorf("两个同名部门的 DN 不应相同")
// TestDeptGroupDNDistinct 验证不同部门索引产生不同组 DN。
func TestDeptGroupDNDistinct(t *testing.T) {
a := DeptGroupDN("od-1", "dc=example,dc=com")
b := DeptGroupDN("od-2", "dc=example,dc=com")
if a == b {
t.Errorf("不同部门索引的组 DN 不应相同")
}
}
// TestFlatDeptInfoStable 验证同一输入产生的 DN 稳定(幂等,便于物化比对)。
func TestFlatDeptInfoStable(t *testing.T) {
depts := []*feishu.Department{
dept("od-root", "0", "Acme"),
dept("od-rd", "od-root", "研发部"),
// TestGroupRootDN 验证组根 OU 固定挂在 ou=groups 下。
func TestGroupRootDN(t *testing.T) {
if got := GroupRootDN("dc=example,dc=com"); got != "ou=groups,dc=example,dc=com" {
t.Errorf("GroupRootDN = %q", got)
}
a := FlatDeptInfo(depts, "dc=example,dc=com")
b := FlatDeptInfo(depts, "dc=example,dc=com")
for id := range a {
if a[id].DN != b[id].DN || a[id].Chain != b[id].Chain {
t.Errorf("部门 %s 的命名不稳定: %q vs %q", id, a[id].DN, b[id].DN)
}
if !strings.HasPrefix(DeptGroupDN("od-1", "dc=example,dc=com"), "cn=") {
t.Errorf("组 DN 应以 cn= 开头")
}
}