Scert/backend/internal/adminconsole/flow_test.go

149 lines
4.1 KiB
Go

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("错误消息泄露了参数中的密码")
}
}
}