package adminconsole import ( "context" "errors" "reflect" "strings" "testing" "git.pcux.cn/xuans/Scert/backend/internal/localreset" ) type testTerminalInput struct { values []string hidden []bool prompts []string failAt int readHook func(int) } func (s *testTerminalInput) Read(ctx context.Context, prompt string, hidden bool) (string, error) { if err := ctx.Err(); err != nil { return "", err } s.hidden = append(s.hidden, hidden) s.prompts = append(s.prompts, prompt) index := len(s.hidden) if s.readHook != nil { s.readHook(index) } if s.failAt == index { return "", errors.New("test-input-failure") } if index > len(s.values) { return "", errors.New("test-input-exhausted") } return s.values[index-1], nil } func (*testTerminalInput) Show(string) error { return nil } type testResetClient struct { methods []localreset.Method calls []string fail string password string } func (c *testResetClient) Methods() []localreset.Method { return c.methods } func (c *testResetClient) invoke(action string) error { c.calls = append(c.calls, action) if c.fail == action { return localreset.ErrProtocol } return nil } func (c *testResetClient) Select(value string) error { if value != "email" { return localreset.ErrProtocol } return c.invoke("select") } func (c *testResetClient) Verify(value string) error { if value != "01234567" { return localreset.ErrProtocol } return c.invoke("verify") } func (c *testResetClient) Reset(value string) error { c.password = value return c.invoke("reset") } func (*testResetClient) Close() error { return nil } func TestAdminConsoleFlowRequiresVerificationAndHiddenInput(t *testing.T) { client := &testResetClient{methods: []localreset.Method{{ID: "email"}}} screen := &testTerminalInput{values: []string{"1", "01234567", "NewPass123!", "NewPass123!"}} if err := resetFlow(t.Context(), client, screen); err != nil { t.Fatal(err) } if !reflect.DeepEqual(client.calls, []string{"select", "verify", "reset"}) || client.password != "NewPass123!" { t.Fatal("重置未按验证后提交的顺序执行") } if !reflect.DeepEqual(screen.hidden, []bool{false, true, true, true}) { t.Fatal("验证码或密码出现回显") } for _, prompt := range screen.prompts { if strings.Contains(prompt, "01234567") || strings.Contains(prompt, "NewPass123!") { t.Fatal("提示文字泄露秘密") } } } func TestAdminConsoleFailureNeverSubmitsPassword(t *testing.T) { for _, scenario := range []string{"unknown-method", "cancel-choice", "select", "verify", "input-code", "input-password", "input-confirm", "weak-password", "mismatch", "cancel-after-verify"} { t.Run(scenario, func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() client := &testResetClient{methods: []localreset.Method{{ID: "email"}}} screen := &testTerminalInput{values: []string{"1", "01234567", "NewPass123!", "NewPass123!"}} switch scenario { case "unknown-method": client.methods[0].ID = "unknown" case "cancel-choice": screen.values[0] = "0" case "select", "verify": client.fail = scenario case "input-code": screen.failAt = 2 case "input-password": screen.failAt = 3 case "input-confirm": screen.failAt = 4 case "weak-password": screen.values[2] = "short" case "mismatch": screen.values[3] = "WrongPass12!" case "cancel-after-verify": screen.readHook = func(index int) { if index == 4 { cancel() } } } if err := resetFlow(ctx, client, screen); err == nil { t.Fatal("失败条件未被拒绝") } for _, call := range client.calls { if call == "reset" { t.Fatal("验证或交互失败仍然提交了密码") } } }) } } func TestAdminConsoleRejectsAllArgumentBasedInput(t *testing.T) { for _, args := range [][]string{ {"--password", "TestPass123!"}, {"--config", "scert.json"}, {"--code-output", "reset-code"}, {"TestPass123!"}, {"--help"}, } { err := Run(t.Context(), args) if err == nil { t.Fatal("接受了命令行参数") } if strings.Contains(err.Error(), "TestPass123!") { t.Fatal("错误消息泄露了参数中的密码") } } }