//go:build test package user // Package user contains tests for guest user creation endpoints. // // Test Coverage: // - CreateGuestUserHandler: POST /api/user/guest - Create a new guest user // // Edge case tests for validation: // - Invalid phone number format // - Empty first name // - Name exceeds 50 character limit // - Invalid email format import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "crussell/testutils" "crussell/testutils/fixtures" ) // TestGuestUser_Create_InvalidPhone verifies that an invalid phone number returns 400 Bad Request. func TestGuestUser_Create_InvalidPhone(t *testing.T) { t.Parallel() reqBody := CreateGuestUserRequest{ FirstName: "Test", LastName: "User", Email: "test@test.com", Phone: "not-a-phone", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() CreateGuestUserHandler(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } } // TestGuestUser_Create_EmptyFirstName verifies that an empty first name returns 400 Bad Request. func TestGuestUser_Create_EmptyFirstName(t *testing.T) { t.Parallel() reqBody := CreateGuestUserRequest{ FirstName: "", LastName: "User", Email: "test@test.com", Phone: "07123456789", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() CreateGuestUserHandler(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } } // TestGuestUser_Create_NameTooLong verifies that a first name exceeding 50 characters returns 400 Bad Request. func TestGuestUser_Create_NameTooLong(t *testing.T) { t.Parallel() reqBody := CreateGuestUserRequest{ FirstName: strings.Repeat("a", 51), LastName: "User", Email: "test@test.com", Phone: "07123456789", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() CreateGuestUserHandler(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } } // TestGuestUser_Create_InvalidEmail verifies that an invalid email format returns 400 Bad Request. func TestGuestUser_Create_InvalidEmail(t *testing.T) { t.Parallel() reqBody := CreateGuestUserRequest{ FirstName: "Test", LastName: "User", Email: "not-an-email", Phone: "07123456789", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() CreateGuestUserHandler(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } } // TestCheckEmail_NotRegistered verifies that querying a non-existent email returns suggestion null. func TestCheckEmail_NotRegistered(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/api/check-email?email=nobody@example.com&firstName=Jane&lastName=Doe&phone=%2B447123456789", nil) rr := httptest.NewRecorder() CheckEmailHandler(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if resp["suggestion"] != nil { t.Errorf("expected suggestion null, got %v", resp["suggestion"]) } } // TestCheckEmail_Registered_MatchingDetails verifies that querying an existing registered user's email // with matching first name, last name, and phone returns suggestion "login". func TestCheckEmail_Registered_MatchingDetails(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUserWithEmail(tx, "jane@example.com", "verified_email") if err != nil { t.Fatalf("failed to create test user: %v", err) } _, err = tx.Exec(ctx, ` UPDATE users SET n_first_name = 'Jane', n_last_name = 'Doe' WHERE id = $1 `, userID) if err != nil { t.Fatalf("failed to update user name: %v", err) } req := httptest.NewRequest(http.MethodGet, `/api/check-email?email=jane@example.com&firstName=Jane&lastName=Doe&phone=%2B447123456789`, nil) req = req.WithContext(ctx) rr := httptest.NewRecorder() CheckEmailHandler(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } suggestion, ok := resp["suggestion"].(string) if !ok || suggestion != "login" { t.Errorf("expected suggestion 'login', got %v", resp["suggestion"]) } } // TestCheckEmail_Registered_PartialMatch verifies that when the email exists but details don't fully match, // the handler returns suggestion "check". func TestCheckEmail_Registered_PartialMatch(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUserWithEmail(tx, "jane@example.com", "verified_email") if err != nil { t.Fatalf("failed to create test user: %v", err) } _, err = tx.Exec(ctx, ` UPDATE users SET n_first_name = 'Jane', n_last_name = 'Doe' WHERE id = $1 `, userID) if err != nil { t.Fatalf("failed to update user name: %v", err) } req := httptest.NewRequest(http.MethodGet, `/api/check-email?email=jane@example.com&firstName=Wrong&lastName=Doe&phone=%2B447123456789`, nil) req = req.WithContext(ctx) rr := httptest.NewRecorder() CheckEmailHandler(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } suggestion, ok := resp["suggestion"].(string) if !ok || suggestion != "check" { t.Errorf("expected suggestion 'check', got %v", resp["suggestion"]) } } // TestCheckEmail_GuestUser verifies that a guest user's email is treated as not found // (suggestion null) because the query excludes account_role = 'guest'. func TestCheckEmail_GuestUser(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) _, err := fixtures.CreateTestGuestUser(tx) if err != nil { t.Fatalf("failed to create guest user: %v", err) } req := httptest.NewRequest(http.MethodGet, `/api/check-email?email=guest@test.com&firstName=Guest&lastName=User&phone=%2B447123456789`, nil) req = req.WithContext(ctx) rr := httptest.NewRecorder() CheckEmailHandler(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if resp["suggestion"] != nil { t.Errorf("expected suggestion null for guest user, got %v", resp["suggestion"]) } } // TestCheckEmail_InvalidEmail verifies that an invalid email format returns 400 Bad Request. func TestCheckEmail_InvalidEmail(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/api/check-email?email=not-an-email", nil) rr := httptest.NewRecorder() CheckEmailHandler(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } } // TestCheckEmail_MissingEmail verifies that omitting the email query parameter returns 400 Bad Request // with the appropriate error message. func TestCheckEmail_MissingEmail(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/api/check-email", nil) rr := httptest.NewRecorder() CheckEmailHandler(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } if !strings.Contains(rr.Body.String(), "email query parameter required") { t.Errorf("expected 'email query parameter required' in body, got %s", rr.Body.String()) } } // TestGuestUser_Create_Success verifies that a valid guest user request // creates a guest user and returns 201 with the user's details. func TestGuestUser_Create_Success(t *testing.T) { // NOT parallel — uses db.Conn state ctx, tx := testutils.SetupTestTx(t) reqBody := CreateGuestUserRequest{ FirstName: "Jane", LastName: "Guest", Email: "jane.guest.success@example.com", Phone: "07123456789", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req = req.WithContext(ctx) rr := httptest.NewRecorder() CreateGuestUserHandler(rr, req) if rr.Code != http.StatusCreated { t.Errorf("expected status 201, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } // Verify user was created in DB var count int err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE email = $1 AND account_role = 'guest'`, "jane.guest.success@example.com").Scan(&count) if err != nil { t.Fatalf("failed to query users: %v", err) } if count != 1 { t.Errorf("expected 1 guest user (account_role='guest'), got %d", count) } // Verify response body contains ID and role var resp CreateGuestUserResponse if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if resp.ID == "" { t.Error("expected non-empty user ID in response") } if resp.Role != "guest" { t.Errorf("expected role 'guest', got '%s'", resp.Role) } }