package httpapi_test import ( "database/sql" "fmt" "net/http" "strings" "testing" "git.yeij.top/AskaEth/Scoreboard/server/internal/auth" ) // 登录审计必须落在真实用户上:请求 ctx 里仍是 guest,靠 AuditEntry.ActorID 补(P1-13)。 func TestLoginAuditRecordsUserID(t *testing.T) { env := newTestEnv(t) userID := env.createUser("recorder", "recorder-pass", "recorder") client, _ := env.client() env.login(client, "recorder", "recorder-pass") var auditUser sql.NullInt64 if err := env.ev.QueryRow( `SELECT user_id FROM audit_logs WHERE action = 'auth.login' ORDER BY id DESC LIMIT 1`).Scan(&auditUser); err != nil { t.Fatalf("查询登录审计: %v", err) } if !auditUser.Valid || auditUser.Int64 != userID { t.Fatalf("登录审计 user_id 应为 %d,得到 %+v", userID, auditUser) } // 按 userId 过滤必须能查到这条登录记录。 env.createUser("admin", "admin-pass", "admin") adminClient, _ := env.client() env.login(adminClient, "admin", "admin-pass") _, env2 := env.do(adminClient, http.MethodGet, fmt.Sprintf("/api/audit-logs?userId=%d", userID), nil, false) page := decodeData[map[string]any](t, env2) items, _ := page["items"].([]any) found := false for _, raw := range items { item, ok := raw.(map[string]any) if !ok { continue } if item["action"] == "auth.login" && item["userId"] == float64(userID) { found = true } } if !found { t.Fatalf("按 userId 过滤应能查到登录审计,得到 %+v", items) } } // 跨站登录(login CSRF)必须被同源校验挡住(P2-6)。 func TestLoginRejectsCrossOrigin(t *testing.T) { env := newTestEnv(t) env.createUser("recorder", "recorder-pass", "recorder") client, _ := env.client() body := map[string]any{"username": "recorder", "password": "recorder-pass"} status, env2 := env.doH(client, http.MethodPost, "/api/auth/login", body, false, map[string]string{"Origin": "http://evil.example"}) if status != http.StatusForbidden || env2.Error == nil || env2.Error.Code != "FORBIDDEN" { t.Fatalf("跨站登录应 403 FORBIDDEN,得到 %d %+v", status, env2.Error) } // 同源(Origin 与 Host 一致)仍可登录。 status, env2 = env.doH(client, http.MethodPost, "/api/auth/login", body, false, map[string]string{"Origin": env.server.URL}) if status != http.StatusOK || !env2.OK { t.Fatalf("同源登录应 200,得到 %d %+v", status, env2.Error) } } // 超过 bcrypt 72 字节上限的新密码必须 400,而不是被静默截断(P2-6)。 func TestChangePasswordRejectsTooLong(t *testing.T) { env := newTestEnv(t) env.createUser("recorder", "recorder-pass", "recorder") client, _ := env.client() env.login(client, "recorder", "recorder-pass") tooLong := strings.Repeat("a", auth.MaxPasswordBytes+1) status, env2 := env.do(client, http.MethodPost, "/api/auth/password", map[string]any{ "currentPassword": "recorder-pass", "newPassword": tooLong, }, true) if status != http.StatusBadRequest { t.Fatalf("超长新密码应 400,得到 %d %+v", status, env2.Error) } }