package httpapi_test import ( "fmt" "net/http" "strings" "testing" "unicode/utf8" "git.yeij.top/AskaEth/Scoreboard/server/internal/auth" ) // 建号与重置密码都必须拒绝超过 bcrypt 上限的密码,避免静默截断(P2-6)。 func TestUserPasswordLengthLimits(t *testing.T) { env := newTestEnv(t) env.createUser("admin", "admin-pass", "admin") client, _ := env.client() env.login(client, "admin", "admin-pass") tooLong := strings.Repeat("a", auth.MaxPasswordBytes+1) status, env2 := env.do(client, http.MethodPost, "/api/users", map[string]any{ "username": "u1", "password": tooLong, "role": "recorder", }, true) if status != http.StatusBadRequest { t.Fatalf("超长密码建号应 400,得到 %d %+v", status, env2.Error) } status, env2 = env.do(client, http.MethodPost, "/api/users", map[string]any{ "username": "u1", "password": "u1-pass-123", "role": "recorder", }, true) if status != http.StatusOK { t.Fatalf("建号应 200,得到 %d %+v", status, env2.Error) } created := decodeData[map[string]any](t, env2) id, _ := created["id"].(float64) if id == 0 { t.Fatalf("建号响应缺少 id: %+v", created) } status, env2 = env.do(client, http.MethodPost, fmt.Sprintf("/api/users/%d/password", int64(id)), map[string]any{"password": tooLong}, true) if status != http.StatusBadRequest { t.Fatalf("超长密码重置应 400,得到 %d %+v", status, env2.Error) } } // 「至少 8 位」按字符(rune)算,不是按字节:3 个汉字是 9 字节, // 按字节判会把前端(按字符数)拒绝的口令放进来,同一口令界面拒、接口收。 // 上限仍按字节——bcrypt 只吃前 72 字节,这是算法边界,不是长度口径。 // 口径必须与改密接口(handlers_config.go)完全一致(P2 A37 同源缺口)。 func TestUserPasswordLengthCountsRunes(t *testing.T) { env := newTestEnv(t) env.createUser("admin", "admin-pass", "admin") client, _ := env.client() env.login(client, "admin", "admin-pass") // 3 个汉字 + 5 个字母 = 8 字符 / 14 字节:字数够,必须放行。 eightRunes := "密码呀abcde" if utf8.RuneCountInString(eightRunes) != 8 || len(eightRunes) != 14 { t.Fatalf("用例构造错误:应 8 字符 / 14 字节,得到 %d 字符 / %d 字节", utf8.RuneCountInString(eightRunes), len(eightRunes)) } // 7 个汉字 = 7 字符 / 21 字节:字数不够,必须拒(按字节判的老口径会放行)。 sevenRunes := strings.Repeat("汉", 7) // 25 个汉字 = 25 字符 / 75 字节:字数够但超 bcrypt 上限,必须拒。 tooManyBytes := strings.Repeat("汉", 25) status, env2 := env.do(client, http.MethodPost, "/api/users", map[string]any{ "username": "u-runes", "password": eightRunes, "role": "recorder", }, true) if status != http.StatusOK { t.Fatalf("8 字符(14 字节)的密码建号应 200,得到 %d %+v", status, env2.Error) } created := decodeData[map[string]any](t, env2) id := int64(created["id"].(float64)) if id == 0 { t.Fatalf("建号响应缺少 id: %+v", created) } // 建号的密码能直接登录,说明服务端真的按这一串收下了(没有被截断成别的口令)。 // 用新 client:带会话再登录要过 CSRF,而这里要的正是「换个人重新登」。 fresh, _ := env.client() env.login(fresh, "u-runes", eightRunes) status, env2 = env.do(client, http.MethodPost, "/api/users", map[string]any{ "username": "u-short", "password": sevenRunes, "role": "recorder", }, true) if status != http.StatusBadRequest || env2.Error == nil || env2.Error.Message != "password 至少 8 位" { t.Fatalf("7 个汉字是 7 字符,建号应 400「password 至少 8 位」,得到 %d %+v", status, env2.Error) } status, env2 = env.do(client, http.MethodPost, "/api/users", map[string]any{ "username": "u-long", "password": tooManyBytes, "role": "recorder", }, true) if status != http.StatusBadRequest || env2.Error == nil || env2.Error.Message != fmt.Sprintf("password 不能超过 %d 字节", auth.MaxPasswordBytes) { t.Fatalf("25 个汉字是 75 字节,建号应 400 字节上限,得到 %d %+v", status, env2.Error) } // 重置密码与建号同口径:8 字符放行、7 字符拒绝。 status, env2 = env.do(client, http.MethodPost, fmt.Sprintf("/api/users/%d/password", id), map[string]any{"password": eightRunes}, true) if status != http.StatusOK { t.Fatalf("8 字符(14 字节)的密码重置应 200,得到 %d %+v", status, env2.Error) } status, env2 = env.do(client, http.MethodPost, fmt.Sprintf("/api/users/%d/password", id), map[string]any{"password": sevenRunes}, true) if status != http.StatusBadRequest || env2.Error == nil || env2.Error.Message != "password 至少 8 位" { t.Fatalf("7 个汉字是 7 字符,重置应 400「password 至少 8 位」,得到 %d %+v", status, env2.Error) } status, env2 = env.do(client, http.MethodPost, fmt.Sprintf("/api/users/%d/password", id), map[string]any{"password": tooManyBytes}, true) if status != http.StatusBadRequest || env2.Error == nil || env2.Error.Message != fmt.Sprintf("password 不能超过 %d 字节", auth.MaxPasswordBytes) { t.Fatalf("25 个汉字是 75 字节,重置应 400 字节上限,得到 %d %+v", status, env2.Error) } }