From 9deae1b0d7bbddf6647e3d9686eb1e1fed692f2c Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 29 May 2026 16:06:19 +0100 Subject: [PATCH] feat: referral code validation in registration with tests Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- backend/handlers/auth/auth_test.go | 125 +++++++++++++++++++++++++++++ backend/handlers/auth/local.go | 36 +++++++++ 2 files changed, 161 insertions(+) diff --git a/backend/handlers/auth/auth_test.go b/backend/handlers/auth/auth_test.go index a7402f2..88e27c6 100644 --- a/backend/handlers/auth/auth_test.go +++ b/backend/handlers/auth/auth_test.go @@ -980,5 +980,130 @@ func TestRegister_EmptyPassword(t *testing.T) { } } +// TestRegister_WithValidReferralCode verifies that registration succeeds when +// a valid existing referral code is provided, and the referral relationship +// is recorded in the user_referrals table. +func TestRegister_WithValidReferralCode(t *testing.T) { + resetTestData(t) + + handler := http.HandlerFunc(RegisterHandler) + + // Create a referrer user with a known referral code + referrerID, err := fixtures.CreateTestUserWithEmail(db.DB, "referrer@test.com", "verified_email") + if err != nil { + t.Fatalf("failed to create referrer user: %v", err) + } + defer fixtures.DeleteUser(db.DB, referrerID) + + // Set a known referral code for the referrer + knownCode := "abc123def456" + _, err = db.DB.Exec(context.Background(), + "UPDATE users SET referral_code = $1 WHERE id = $2", knownCode, referrerID) + if err != nil { + t.Fatalf("failed to set referral code: %v", err) + } + + body := RegisterRequest{ + FirstName: "Referred", + LastName: "User", + Email: "referred@test.com", + Password: "password123", + Phone: "07123456789", + DateOfBirth: "1990-01-15", + AgreedToPolicy: true, + ReferralCode: knownCode, + } + + w := makeRequest(handler, "POST", "/api/register", body) + + if w.Code != http.StatusCreated { + t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String()) + } + + // Verify referral relationship was created + var referredID string + err = db.DB.QueryRow(context.Background(), + "SELECT id FROM users WHERE email = $1", "referred@test.com").Scan(&referredID) + if err != nil { + t.Fatalf("failed to find referred user: %v", err) + } + defer fixtures.DeleteUser(db.DB, referredID) + + var count int + err = db.DB.QueryRow(context.Background(), + "SELECT COUNT(*) FROM user_referrals WHERE referrer_id = $1 AND referred_id = $2", + referrerID, referredID).Scan(&count) + if err != nil { + t.Fatalf("failed to query user_referrals: %v", err) + } + if count != 1 { + t.Errorf("expected 1 referral record, got %d", count) + } +} + +// TestRegister_WithInvalidReferralCode verifies that registration fails with +// 400 when a non-existent referral code is provided. +func TestRegister_WithInvalidReferralCode(t *testing.T) { + resetTestData(t) + + handler := http.HandlerFunc(RegisterHandler) + + body := RegisterRequest{ + FirstName: "Test", + LastName: "User", + Email: "invalid-referral@test.com", + Password: "password123", + Phone: "07123456789", + DateOfBirth: "1990-01-15", + AgreedToPolicy: true, + ReferralCode: "nonexistent1234", // 12 chars but doesn't exist + } + + w := makeRequest(handler, "POST", "/api/register", body) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) + } +} + +// TestRegister_WithInvalidReferralCodeFormat verifies that registration fails +// when the referral code is not exactly 12 characters. +func TestRegister_WithInvalidReferralCodeFormat(t *testing.T) { + resetTestData(t) + + handler := http.HandlerFunc(RegisterHandler) + + tests := []struct { + name string + code string + desc string + }{ + {"too_short", "abc123", "less than 12 chars"}, + {"too_long", "abc123def456ghi", "more than 12 chars"}, + {"special_chars", "abc123def4!!", "contains special chars"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body := RegisterRequest{ + FirstName: "Test", + LastName: "User", + Email: fmt.Sprintf("format-test-%s@test.com", tt.name), + Password: "password123", + Phone: "07123456789", + DateOfBirth: "1990-01-15", + AgreedToPolicy: true, + ReferralCode: tt.code, + } + + w := makeRequest(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()) + } + }) + } +} + // Ensure test compilation - import pgxpool to avoid unused import var _ = func() *pgxpool.Pool { return nil } diff --git a/backend/handlers/auth/local.go b/backend/handlers/auth/local.go index 0f1c8ec..42100b6 100644 --- a/backend/handlers/auth/local.go +++ b/backend/handlers/auth/local.go @@ -63,6 +63,7 @@ type RegisterRequest struct { Phone string `json:"phone"` DateOfBirth string `json:"dateOfBirth"` AgreedToPolicy bool `json:"agreedToPolicy"` + ReferralCode string `json:"referralCode,omitempty"` } type LoginRequest struct { @@ -160,6 +161,28 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } + // Validate referral code if provided + var referrerID *string + req.ReferralCode = strings.TrimSpace(req.ReferralCode) + if req.ReferralCode != "" { + if len(req.ReferralCode) != 12 { + http.Error(w, "referral code must be exactly 12 characters", http.StatusBadRequest) + return + } + referralCodeRegex := regexp.MustCompile(`^[a-zA-Z0-9]{12}$`) + if !referralCodeRegex.MatchString(req.ReferralCode) { + http.Error(w, "referral code must be alphanumeric", http.StatusBadRequest) + return + } + // Look up referrer by referral code + err := db.DB.QueryRow(r.Context(), + "SELECT id FROM users WHERE referral_code = $1", req.ReferralCode).Scan(&referrerID) + if err != nil { + http.Error(w, "invalid referral code", http.StatusBadRequest) + return + } + } + // Hash password hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { @@ -197,6 +220,19 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } + // Record referral relationship if referral code was provided + if referrerID != nil { + _, err = tx.Exec(r.Context(), ` + INSERT INTO user_referrals (referrer_id, referred_id) + VALUES ($1, $2) + ON CONFLICT DO NOTHING + `, *referrerID, userID) + if err != nil { + http.Error(w, "could not process referral", http.StatusInternalServerError) + return + } + } + if err := tx.Commit(r.Context()); err != nil { http.Error(w, "server error", http.StatusInternalServerError) return