package system import ( "context" "io" "log/slog" "os" "path/filepath" "testing" "ws_usernode/internal/config" ) // discardLog 供测试注入的静默日志器。 func discardLog() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func testCfg() config.SystemConfig { cfg := config.Default() return cfg.System } func TestDryRunDoesNotExecute(t *testing.T) { cfg := testCfg() cfg.DryRun = true m := New(cfg, discardLog()) ctx := context.Background() // dry-run 下创建/删除不应报错(只打印计划) if err := m.CreateUser(ctx, Account{Username: "ext_zhangsan"}); err != nil { t.Fatalf("dry-run create: %v", err) } if err := m.RemoveUser(ctx, "ext_zhangsan"); err != nil { t.Fatalf("dry-run remove: %v", err) } if err := m.SetLock(ctx, "ext_zhangsan", true); err != nil { t.Fatalf("dry-run lock: %v", err) } if err := m.SyncAuthorizedKeys(ctx, "ext_zhangsan", nil); err != nil { t.Fatalf("dry-run keys: %v", err) } // 参数校验在 dry-run 下仍然生效 if err := m.CreateUser(ctx, Account{Username: "ext_..bad"}); err == nil { t.Fatal("expected validation error for illegal account name") } } func TestExistsParsesPasswd(t *testing.T) { // 用临时 passwd 文件验证解析逻辑 dir := t.TempDir() p := filepath.Join(dir, "passwd") content := "root:x:0:0:root:/root:/bin/sh\n" + "ext_zhangsan:x:1001:1001::/home/ext_zhangsan:/bin/sh\n" if err := os.WriteFile(p, []byte(content), 0o600); err != nil { t.Fatal(err) } old := passwdPath passwdPath = p t.Cleanup(func() { passwdPath = old }) m := New(testCfg(), discardLog()) ctx := context.Background() // 已带前缀与未带前缀都会命中同一账号 for _, name := range []string{"ext_zhangsan", "zhangsan"} { ok, err := m.Exists(ctx, name) if err != nil { t.Fatalf("exists(%q): %v", name, err) } if !ok { t.Fatalf("user %q should exist", name) } } // 不存在的账号返回 false if ok, err := m.Exists(ctx, "ghost_xyz"); err != nil || ok { t.Fatalf("exists(ghost) = %v/%v, want false/nil", ok, err) } // 非法账号名直接报错 if _, err := m.Exists(ctx, "bad..name"); err == nil { t.Fatal("expected validation error") } } func TestPrefixNormalization(t *testing.T) { cfg := testCfg() lm := &localManager{cfg: cfg} // sysName 逻辑:已带前缀不重复加 if got := lm.sysName("ext_x"); got != "ext_x" { t.Fatalf("sysName(ext_x) = %q", got) } if got := lm.sysName("x"); got != "ext_x" { t.Fatalf("sysName(x) = %q", got) } } func TestSyncAuthorizedKeysDirect(t *testing.T) { cfg := testCfg() cfg.DryRun = false cfg.Sudo = false cfg.HomeBase = t.TempDir() // 临时家目录基路径,进程用户直写 m := New(cfg, discardLog()) ctx := context.Background() keys := []Key{ {Type: "ssh-ed25519", PublicKey: "AAAAC3NzaC1lZDI1NTE5AAAAIB-test"}, {Type: "ssh-rsa", PublicKey: "AAAAB3NzaC1yc2EAAAADAQAB-test"}, } if err := m.SyncAuthorizedKeys(ctx, "ext_zhangsan", keys); err != nil { t.Fatalf("sync keys: %v", err) } sshDir := filepath.Join(cfg.HomeBase, "ext_zhangsan", ".ssh") dest := filepath.Join(sshDir, "authorized_keys") content, err := os.ReadFile(dest) if err != nil { t.Fatalf("read authorized_keys: %v", err) } want := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB-test\nssh-rsa AAAAB3NzaC1yc2EAAAADAQAB-test\n" if string(content) != want { t.Fatalf("authorized_keys = %q, want %q", content, want) } // .ssh 目录 0700、文件 0600 di, err := os.Stat(sshDir) if err != nil { t.Fatalf("stat .ssh: %v", err) } if di.Mode().Perm() != 0o700 { t.Fatalf(".ssh mode = %v, want 0700", di.Mode().Perm()) } fi, err := os.Stat(dest) if err != nil { t.Fatalf("stat authorized_keys: %v", err) } if fi.Mode().Perm() != 0o600 { t.Fatalf("authorized_keys mode = %v, want 0600", fi.Mode().Perm()) } // 无有效密钥 → 写空文件(SSH 行为一致) if err := m.SyncAuthorizedKeys(ctx, "ext_zhangsan", nil); err != nil { t.Fatalf("sync empty: %v", err) } content, err = os.ReadFile(dest) if err != nil { t.Fatalf("re-read authorized_keys: %v", err) } if len(content) != 0 { t.Fatalf("empty sync left content: %q", content) } } func TestSyncAuthorizedKeysDryRunSudoPlan(t *testing.T) { cfg := testCfg() cfg.DryRun = true cfg.Sudo = true m := New(cfg, discardLog()) // sudo 模式的 dry-run 只打印计划(install 落位路径),不执行任何命令 if err := m.SyncAuthorizedKeys(context.Background(), "ext_zhangsan", []Key{{Type: "ssh-ed25519", PublicKey: "AAA"}}); err != nil { t.Fatalf("dry-run sudo plan: %v", err) } }