refactor(backend): replace resetTestData with SetupTestDB and add new tests

Migrate all test files from resetTestData(t) to testutils.SetupTestDB(t) for isolated per-package test databases.

- Add new feature tests: name history assertions, referral discount preview,
  time blockers, email validation, GDPR export, loyalty manual redemption
- Update existing tests to use batch queries and SetupTestDB
- Remove test_helpers.go resetTestData infrastructure
- Add comprehensive user profile tests (442 new lines)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-20 16:57:36 +01:00
co-authored by Sisyphus
parent 9c68918c20
commit b03c4f6247
36 changed files with 1735 additions and 859 deletions
+43 -80
View File
@@ -22,11 +22,9 @@ package auth
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
@@ -35,55 +33,20 @@ import (
"crussell/db"
"crussell/internal/dav"
"crussell/mw"
"crussell/testutils"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestMain(m *testing.M) {
pool, err := testdb.NewPool("")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create test pool: %v\n", err)
os.Exit(1)
}
testdb.Migrate(&testing.T{}, pool)
db.DB = pool
jwt.Init()
dav.Service = &dav.BaseService{}
code := m.Run()
pool.Close()
os.Exit(code)
}
func resetTestData(t *testing.T) {
t.Helper()
testdb.TruncateTables(t, db.DB)
testutils.SetupTestDB(t)
dav.Service = &dav.BaseService{}
}
// helper function to make JSON request
func makeRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(method, path, nil)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
// Helper to parse response body
func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
return json.Unmarshal(w.Body.Bytes(), dest)
}
// =============================================================================
// Register Handler Tests
// =============================================================================
@@ -107,7 +70,7 @@ func TestRegister_Success(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
@@ -165,7 +128,7 @@ func TestRegister_InvalidInput_MissingFields(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := makeRequest(handler, "POST", "/api/register", tt.body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", tt.body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
@@ -190,7 +153,7 @@ func TestRegister_InvalidInput_InvalidEmail(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -214,7 +177,7 @@ func TestRegister_InvalidInput_InvalidPhone(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -256,7 +219,7 @@ func TestRegister_ValidUKPhoneNumbers(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201 for %s, got %d. body: %s", tc.phone, w.Code, w.Body.String())
@@ -298,7 +261,7 @@ func TestRegister_InvalidPhoneNumbers(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for invalid phone %s, got %d. body: %s", tc.phone, w.Code, w.Body.String())
@@ -327,7 +290,7 @@ func TestRegister_InvalidInput_Under16(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -361,7 +324,7 @@ func TestRegister_DuplicateEmail(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusConflict {
t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
@@ -397,7 +360,7 @@ func TestLogin_Success(t *testing.T) {
Password: "testpassword123",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -406,7 +369,7 @@ func TestLogin_Success(t *testing.T) {
var resp struct {
Token string `json:"token"`
}
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
@@ -434,7 +397,7 @@ func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) {
Password: "wrongpassword",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
@@ -453,7 +416,7 @@ func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) {
Password: "password123",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
@@ -500,7 +463,7 @@ func TestRefreshToken_Success(t *testing.T) {
var resp struct {
Token string `json:"token"`
}
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
@@ -551,14 +514,14 @@ func TestVerifyGenerate_ValidEmail(t *testing.T) {
Email: "user@test.com",
}
w := makeRequest(handler, "POST", "/api/verify/generate", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/generate", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp VerificationResponse
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
@@ -591,14 +554,14 @@ func TestVerifyGenerate_NonExistentEmail(t *testing.T) {
Email: "nonexistent@test.com",
}
w := makeRequest(handler, "POST", "/api/verify/generate", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/generate", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp VerificationResponse
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
@@ -642,14 +605,14 @@ func TestVerifyCheck_ValidCode(t *testing.T) {
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp VerificationResponse
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
@@ -677,7 +640,7 @@ func TestVerifyCheck_InvalidCode(t *testing.T) {
Code: "nonexistent-code-12345",
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -713,7 +676,7 @@ func TestVerifyCheck_ExpiredCode(t *testing.T) {
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -761,7 +724,7 @@ func TestRegister_NameTooLong(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -786,7 +749,7 @@ func TestRegister_InvalidNameCharacters(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -822,13 +785,13 @@ func TestVerifyCheck_AlreadyUsed(t *testing.T) {
body := VerifyCodeRequest{
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusOK {
t.Errorf("first verification: expected status 200, got %d", w.Code)
}
// Second verification with same code should return 403 (already used)
w = makeRequest(handler, "POST", "/api/verify/check", body)
w = testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusForbidden {
t.Errorf("second verification: expected status 403, got %d", w.Code)
}
@@ -875,7 +838,7 @@ func TestVerifyCheck_RoleChangeToVerified(t *testing.T) {
body := VerifyCodeRequest{
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -953,7 +916,7 @@ func TestRegister_PasswordLength_Minimum(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if tt.expectError {
if w.Code == http.StatusCreated {
@@ -985,7 +948,7 @@ func TestRegister_EmptyPassword(t *testing.T) {
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
// Empty password fails because it's a required field
if w.Code != http.StatusBadRequest {
@@ -1027,7 +990,7 @@ func TestRegister_WithValidReferralCode(t *testing.T) {
ReferralCode: knownCode,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
@@ -1072,7 +1035,7 @@ func TestRegister_WithInvalidReferralCode(t *testing.T) {
ReferralCode: "nonexistent1234", // 12 chars but doesn't exist
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -1109,7 +1072,7 @@ func TestRegister_WithInvalidReferralCodeFormat(t *testing.T) {
ReferralCode: tt.code,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for %s (%s), got %d. body: %s", tt.name, tt.desc, w.Code, w.Body.String())
@@ -1163,7 +1126,7 @@ func TestRegister_ReferralCodeCaseInsensitive(t *testing.T) {
ReferralCode: tt.inputCode,
}
w := makeRequest(handler, "POST", "/api/register", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/register", body)
if w.Code != tt.wantStatus {
t.Errorf("%s: expected status %d, got %d. body: %s", tt.name, tt.wantStatus, w.Code, w.Body.String())
@@ -1232,7 +1195,7 @@ func TestLogoutHandler_Success(t *testing.T) {
var resp struct {
Success bool `json:"success"`
}
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if !resp.Success {
@@ -1417,7 +1380,7 @@ func TestLoginResponse_IncludesJTI(t *testing.T) {
Password: "testpassword123",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -1427,7 +1390,7 @@ func TestLoginResponse_IncludesJTI(t *testing.T) {
Token string `json:"token"`
JTI string `json:"jti"`
}
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
@@ -1479,7 +1442,7 @@ func TestLoginInProgress_Cap(t *testing.T) {
Password: "testpassword123",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusTooManyRequests {
t.Errorf("expected status 429, got %d. body: %s", w.Code, w.Body.String())
@@ -1563,7 +1526,7 @@ func TestLogin_AccountLockout_After5Failures(t *testing.T) {
Email: "user@test.com",
Password: "wrongpassword",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusUnauthorized {
t.Fatalf("attempt %d: expected 401, got %d", i+1, w.Code)
}
@@ -1573,7 +1536,7 @@ func TestLogin_AccountLockout_After5Failures(t *testing.T) {
Email: "user@test.com",
Password: "wrongpassword",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusTooManyRequests {
t.Errorf("expected 429 after 5 failures, got %d. body: %s", w.Code, w.Body.String())
}
@@ -1623,7 +1586,7 @@ func TestLogin_AccountLockout_ResetsOnSuccess(t *testing.T) {
Password: "testpassword123",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -1790,7 +1753,7 @@ func TestLogin_ResponseIncludesRefreshToken(t *testing.T) {
Password: "testpassword123",
}
w := makeRequest(handler, "POST", "/api/login", body)
w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
@@ -1801,7 +1764,7 @@ func TestLogin_ResponseIncludesRefreshToken(t *testing.T) {
JTI string `json:"jti"`
RefreshToken string `json:"refreshToken"`
}
if err := parseResponseBody(w, &resp); err != nil {
if err := testutils.ParseResponseBody(w, &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}