feat: referral code validation in registration with tests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user