Files
2025-12-26 13:38:04 +01:00

1719 lines
48 KiB
Go

// +build integration
package main
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
_ "github.com/lib/pq"
)
// Test database configuration - use test schema to isolate from dev/prod
var testDB *sql.DB
var testServer *httptest.Server
func TestMain(m *testing.M) {
// Setup
if err := setupTestEnvironment(); err != nil {
fmt.Printf("Failed to setup test environment: %v\n", err)
os.Exit(1)
}
// Run tests
code := m.Run()
// Cleanup
cleanupTestEnvironment()
os.Exit(code)
}
func setupTestEnvironment() error {
// Set required environment variables
os.Setenv("JWT_SECRET", "test-jwt-secret-minimum-64-characters-long-for-security-purposes-in-testing")
os.Setenv("DB_SSL_MODE", "disable")
os.Setenv("CORS_ALLOW_ORIGIN", "*")
// Use env vars or defaults (for local testing with podman-compose)
dbUser := os.Getenv("TEST_DB_USER")
dbPassword := os.Getenv("TEST_DB_PASSWORD")
dbName := os.Getenv("TEST_DB_NAME")
dbHost := os.Getenv("TEST_DB_HOST")
if dbUser == "" {
dbUser = "user"
}
if dbPassword == "" {
dbPassword = "password"
}
if dbName == "" {
dbName = "coppertone_db"
}
if dbHost == "" {
dbHost = "localhost"
}
os.Setenv("DB_USER", dbUser)
os.Setenv("DB_PASSWORD", dbPassword)
os.Setenv("DB_NAME", dbName)
os.Setenv("DB_HOST", dbHost)
// First connect WITHOUT schema to create the testing schema
os.Setenv("DB_SCHEMA", "")
// Create testing schema before initializing with it
connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s sslmode=disable",
dbUser, dbPassword, dbName, dbHost)
tempDB, err := sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("failed to connect to database: %v", err)
}
defer tempDB.Close()
// Create testing schema if not exists
_, err = tempDB.Exec(`CREATE SCHEMA IF NOT EXISTS testing`)
if err != nil {
return fmt.Errorf("failed to create testing schema: %v", err)
}
// Now set the schema and initialize
os.Setenv("DB_SCHEMA", "testing")
// Load config
loadConfig()
// Initialize database with testing schema
testDB = initDB()
if testDB == nil {
return fmt.Errorf("failed to initialize database")
}
// Set the global db
db = testDB
// Clean up any existing test data
cleanupTestData()
// Create test tables
if err := createTestTables(); err != nil {
return fmt.Errorf("failed to create test tables: %v", err)
}
return nil
}
func cleanupTestEnvironment() {
if testDB != nil {
cleanupTestData()
testDB.Close()
}
}
func cleanupTestData() {
if testDB == nil {
return
}
// Clean tables in reverse order of dependencies
testDB.Exec("DELETE FROM superuser_transfers")
testDB.Exec("DELETE FROM csrf_tokens")
testDB.Exec("DELETE FROM refresh_tokens")
testDB.Exec("DELETE FROM user_roles")
testDB.Exec("DELETE FROM identities")
testDB.Exec("DELETE FROM users")
// Reset rate limiters for tests
if loginLimiter != nil {
loginLimiter = &rateLimiter{attempts: make(map[string]*attemptInfo)}
}
if registerLimiter != nil {
registerLimiter = &rateLimiter{attempts: make(map[string]*attemptInfo)}
}
}
func createTestTables() error {
tables := []string{
`CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(254) UNIQUE,
is_initial_superuser BOOLEAN DEFAULT FALSE,
is_protected BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS identities (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
identifier VARCHAR(255) NOT NULL,
credential TEXT,
is_primary_login BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(type, identifier)
)`,
`CREATE TABLE IF NOT EXISTS user_roles (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, role)
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
client_ip VARCHAR(45),
created_at TIMESTAMP DEFAULT NOW(),
revoked_at TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS csrf_tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
client_ip VARCHAR(45),
created_at TIMESTAMP DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS superuser_transfers (
id SERIAL PRIMARY KEY,
from_user_id INTEGER REFERENCES users(id),
to_user_id INTEGER REFERENCES users(id),
reason TEXT,
created_at TIMESTAMP DEFAULT NOW()
)`,
}
for _, table := range tables {
if _, err := testDB.Exec(table); err != nil {
return fmt.Errorf("failed to create table: %v\nSQL: %s", err, table)
}
}
return nil
}
// Helper function to make HTTP requests
func makeRequest(method, path string, body interface{}, headers map[string]string) (*http.Response, []byte, error) {
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, nil, err
}
reqBody = bytes.NewReader(jsonBody)
}
req := httptest.NewRequest(method, path, reqBody)
req.Header.Set("Content-Type", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}
rr := httptest.NewRecorder()
// Route to appropriate handler
handler := getHandler(path)
if handler == nil {
return nil, nil, fmt.Errorf("no handler for path: %s", path)
}
handler(rr, req)
respBody, _ := io.ReadAll(rr.Body)
return rr.Result(), respBody, nil
}
func getHandler(path string) http.HandlerFunc {
switch {
case path == "/register-email-password":
return handleRegisterEmailPassword
case path == "/register-blockchain":
return handleRegisterBlockchain
case path == "/login-email-password":
return handleLoginEmailPassword
case path == "/login-blockchain":
return handleLoginBlockchain
case path == "/auth/refresh":
return handleRefreshToken
case path == "/auth/logout":
return handleLogout
case path == "/auth/logout-all":
return authenticate(requireCSRF(handleLogoutAll))
case path == "/profile":
return authenticate(handleProfile)
case path == "/identities":
return authenticate(handleGetIdentities)
case path == "/link-identity":
return authenticate(requireCSRF(requireRole(handleLinkIdentity, "CLIENT", "STAFF", "ADMIN")))
case path == "/unlink-identity":
return authenticate(requireCSRF(requireRole(handleUnlinkIdentity, "CLIENT", "STAFF", "ADMIN")))
case path == "/admin/users":
return authenticate(requireRole(handleGetAllUsers, "ADMIN", "SUPERUSER"))
case path == "/admin/users/promote-role":
return authenticate(requireCSRF(requireRole(handlePromoteUserRole, "ADMIN", "SUPERUSER")))
case path == "/admin/users/demote-role":
return authenticate(requireCSRF(requireRole(handleDemoteUserRole, "ADMIN", "SUPERUSER")))
case path == "/superuser/promote":
return authenticate(requireCSRF(requireRole(handlePromoteSuperuser, "SUPERUSER")))
case path == "/superuser/demote":
return authenticate(requireCSRF(requireRole(handleDemoteSuperuser, "SUPERUSER")))
case path == "/superuser/transfer":
return authenticate(requireCSRF(requireRole(handleTransferInitialSuperuser, "SUPERUSER")))
case path == "/healthz":
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
}
default:
return nil
}
}
// ==================== REGISTRATION TESTS ====================
func TestRegisterEmailPassword_Success(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"email": "test@example.com",
"password": "TestPassword123",
"name": "Test User",
}
resp, body, err := makeRequest("POST", "/register-email-password", payload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusCreated {
t.Errorf("Expected status 201, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.Unmarshal(body, &result)
if result["userId"] == nil {
t.Error("Expected userId in response")
}
// Verify user was created in database
var count int
testDB.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", "test@example.com").Scan(&count)
if count != 1 {
t.Errorf("Expected 1 user, found %d", count)
}
}
func TestRegisterEmailPassword_FirstUserIsSuperuser(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"email": "admin@example.com",
"password": "AdminPassword123",
"name": "Admin User",
}
resp, _, err := makeRequest("POST", "/register-email-password", payload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusCreated {
t.Errorf("Expected status 201, got %d", resp.StatusCode)
}
// Verify first user got SUPERUSER role
var role string
err = testDB.QueryRow(`
SELECT r.role FROM user_roles r
JOIN users u ON u.id = r.user_id
WHERE u.email = $1
`, "admin@example.com").Scan(&role)
if err != nil {
t.Fatalf("Failed to query role: %v", err)
}
if role != "SUPERUSER" {
t.Errorf("Expected first user to have SUPERUSER role, got %s", role)
}
// Verify is_initial_superuser flag
var isInitialSU bool
testDB.QueryRow("SELECT is_initial_superuser FROM users WHERE email = $1", "admin@example.com").Scan(&isInitialSU)
if !isInitialSU {
t.Error("Expected first user to have is_initial_superuser = true")
}
}
func TestRegisterEmailPassword_DuplicateEmail(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"email": "duplicate@example.com",
"password": "TestPassword123",
"name": "Test User",
}
// First registration
makeRequest("POST", "/register-email-password", payload, nil)
// Second registration with same email
resp, body, _ := makeRequest("POST", "/register-email-password", payload, nil)
if resp.StatusCode != http.StatusConflict {
t.Errorf("Expected status 409 Conflict, got %d: %s", resp.StatusCode, string(body))
}
}
func TestRegisterEmailPassword_InvalidEmail(t *testing.T) {
cleanupTestData()
testCases := []struct {
email string
desc string
}{
{"", "empty email"},
{"notanemail", "invalid format"},
{"test@", "missing domain"},
{"@example.com", "missing local part"},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
payload := map[string]string{
"email": tc.email,
"password": "TestPassword123",
"name": "Test User",
}
resp, _, _ := makeRequest("POST", "/register-email-password", payload, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for %s, got %d", tc.desc, resp.StatusCode)
}
})
}
}
func TestRegisterEmailPassword_WeakPassword(t *testing.T) {
cleanupTestData()
testCases := []struct {
password string
desc string
}{
{"short", "too short"},
{"alllowercase123", "no uppercase"},
{"ALLUPPERCASE123", "no lowercase"},
{"NoNumbersHere", "no numbers"},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
payload := map[string]string{
"email": "test@example.com",
"password": tc.password,
"name": "Test User",
}
resp, _, _ := makeRequest("POST", "/register-email-password", payload, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for %s, got %d", tc.desc, resp.StatusCode)
}
})
}
}
// ==================== LOGIN TESTS ====================
func TestLoginEmailPassword_Success(t *testing.T) {
cleanupTestData()
// First register a user
regPayload := map[string]string{
"email": "login@example.com",
"password": "LoginPassword123",
"name": "Login User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
// Now login
loginPayload := map[string]string{
"email": "login@example.com",
"password": "LoginPassword123",
}
resp, body, err := makeRequest("POST", "/login-email-password", loginPayload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", resp.StatusCode, string(body))
}
var result AuthTokenResponse
if err := json.Unmarshal(body, &result); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
if result.AccessToken == "" {
t.Error("Expected accessToken in response")
}
if result.RefreshToken == "" {
t.Error("Expected refreshToken in response")
}
if result.CsrfToken == "" {
t.Error("Expected csrfToken in response")
}
if result.TokenType != "Bearer" {
t.Errorf("Expected tokenType 'Bearer', got %s", result.TokenType)
}
}
func TestLoginEmailPassword_InvalidCredentials(t *testing.T) {
cleanupTestData()
// Register a user
regPayload := map[string]string{
"email": "test@example.com",
"password": "CorrectPassword123",
"name": "Test User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
testCases := []struct {
email string
password string
desc string
}{
{"test@example.com", "WrongPassword123", "wrong password"},
{"nonexistent@example.com", "CorrectPassword123", "non-existent user"},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
payload := map[string]string{
"email": tc.email,
"password": tc.password,
}
resp, body, _ := makeRequest("POST", "/login-email-password", payload, nil)
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 401 for %s, got %d: %s", tc.desc, resp.StatusCode, string(body))
}
})
}
}
// ==================== TOKEN REFRESH TESTS ====================
func TestRefreshToken_Success(t *testing.T) {
cleanupTestData()
// Register and login
regPayload := map[string]string{
"email": "refresh@example.com",
"password": "RefreshPassword123",
"name": "Refresh User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "refresh@example.com",
"password": "RefreshPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// Wait a moment to ensure different token
time.Sleep(100 * time.Millisecond)
// Refresh token
refreshPayload := map[string]string{
"refreshToken": loginResult.RefreshToken,
}
resp, body, err := makeRequest("POST", "/auth/refresh", refreshPayload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", resp.StatusCode, string(body))
}
var refreshResult AuthTokenResponse
json.Unmarshal(body, &refreshResult)
if refreshResult.AccessToken == "" {
t.Error("Expected new accessToken")
}
if refreshResult.RefreshToken == "" {
t.Error("Expected new refreshToken")
}
}
func TestRefreshToken_InvalidToken(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"refreshToken": "invalid-token-12345",
}
resp, _, _ := makeRequest("POST", "/auth/refresh", payload, nil)
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 401, got %d", resp.StatusCode)
}
}
// ==================== LOGOUT TESTS ====================
func TestLogout_Success(t *testing.T) {
cleanupTestData()
// Register and login
regPayload := map[string]string{
"email": "logout@example.com",
"password": "LogoutPassword123",
"name": "Logout User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "logout@example.com",
"password": "LogoutPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// Logout
logoutPayload := map[string]string{
"refreshToken": loginResult.RefreshToken,
}
resp, body, _ := makeRequest("POST", "/auth/logout", logoutPayload, nil)
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected 200, got %d: %s", resp.StatusCode, string(body))
}
// Verify refresh token is revoked (can't use it again)
resp2, _, _ := makeRequest("POST", "/auth/refresh", logoutPayload, nil)
if resp2.StatusCode != http.StatusUnauthorized {
t.Error("Expected refresh to fail after logout")
}
}
// ==================== PROFILE TESTS ====================
func TestProfile_Success(t *testing.T) {
cleanupTestData()
// Register and login
regPayload := map[string]string{
"email": "profile@example.com",
"password": "ProfilePassword123",
"name": "Profile User",
}
regResp, _, _ := makeRequest("POST", "/register-email-password", regPayload, nil)
if regResp.StatusCode != http.StatusCreated {
t.Fatalf("Registration failed with status %d", regResp.StatusCode)
}
loginPayload := map[string]string{
"email": "profile@example.com",
"password": "ProfilePassword123",
}
loginResp, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
if loginResp.StatusCode != http.StatusOK {
t.Fatalf("Login failed with status %d: %s", loginResp.StatusCode, string(loginBody))
}
var loginResult AuthTokenResponse
if err := json.Unmarshal(loginBody, &loginResult); err != nil {
t.Fatalf("Failed to parse login response: %v", err)
}
if loginResult.AccessToken == "" {
t.Fatal("Access token is empty")
}
// Get profile
headers := map[string]string{
"Authorization": "Bearer " + loginResult.AccessToken,
}
resp, body, _ := makeRequest("GET", "/profile", nil, headers)
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected 200, got %d: %s", resp.StatusCode, string(body))
}
var profile User
if err := json.Unmarshal(body, &profile); err != nil {
t.Fatalf("Failed to parse profile: %v", err)
}
if profile.Name != "Profile User" {
t.Errorf("Expected name 'Profile User', got '%s'", profile.Name)
}
if profile.Email == nil {
t.Error("Expected email to be set")
} else if *profile.Email != "profile@example.com" {
t.Errorf("Expected email 'profile@example.com', got '%s'", *profile.Email)
}
}
func TestProfile_Unauthorized(t *testing.T) {
cleanupTestData()
// Try without token
resp, _, _ := makeRequest("GET", "/profile", nil, nil)
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 401 without token, got %d", resp.StatusCode)
}
// Try with invalid token
headers := map[string]string{
"Authorization": "Bearer invalid-token",
}
resp2, _, _ := makeRequest("GET", "/profile", nil, headers)
if resp2.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 401 with invalid token, got %d", resp2.StatusCode)
}
}
// ==================== RATE LIMITING TESTS ====================
func TestRateLimit_Login(t *testing.T) {
cleanupTestData()
// Reset rate limiter
loginLimiter = &rateLimiter{attempts: make(map[string]*attemptInfo)}
// Register a user
regPayload := map[string]string{
"email": "ratelimit@example.com",
"password": "RateLimitPassword123",
"name": "Rate Limit User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
// Make many failed login attempts
wrongPayload := map[string]string{
"email": "ratelimit@example.com",
"password": "WrongPassword123",
}
var lastStatus int
for i := 0; i < 10; i++ {
resp, _, _ := makeRequest("POST", "/login-email-password", wrongPayload, nil)
lastStatus = resp.StatusCode
if resp.StatusCode == http.StatusTooManyRequests {
// Rate limited - expected
return
}
}
t.Errorf("Expected rate limiting after multiple failed attempts, last status: %d", lastStatus)
}
// ==================== SECOND USER IS CLIENT TESTS ====================
func TestSecondUserIsClient(t *testing.T) {
cleanupTestData()
// First user (will be SUPERUSER)
firstPayload := map[string]string{
"email": "first@example.com",
"password": "FirstPassword123",
"name": "First User",
}
makeRequest("POST", "/register-email-password", firstPayload, nil)
// Second user (should be CLIENT)
secondPayload := map[string]string{
"email": "second@example.com",
"password": "SecondPassword123",
"name": "Second User",
}
makeRequest("POST", "/register-email-password", secondPayload, nil)
// Check second user's role
var role string
err := testDB.QueryRow(`
SELECT r.role FROM user_roles r
JOIN users u ON u.id = r.user_id
WHERE u.email = $1
`, "second@example.com").Scan(&role)
if err != nil {
t.Fatalf("Failed to query role: %v", err)
}
if role != "CLIENT" {
t.Errorf("Expected second user to have CLIENT role, got %s", role)
}
}
// ==================== HEALTHCHECK TEST ====================
func TestHealthcheck(t *testing.T) {
resp, body, err := makeRequest("GET", "/healthz", nil, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected 200, got %d", resp.StatusCode)
}
if !strings.Contains(string(body), "ok") {
t.Errorf("Expected 'ok' in response, got: %s", string(body))
}
}
// ==================== BLOCKCHAIN REGISTRATION TESTS ====================
func TestRegisterBlockchain_Success(t *testing.T) {
cleanupTestData()
// Use a valid Ethereum address format
payload := map[string]string{
"blockchainAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f4aB23",
"name": "Blockchain User",
}
resp, body, err := makeRequest("POST", "/register-blockchain", payload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusCreated {
t.Errorf("Expected status 201, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.Unmarshal(body, &result)
if result["userId"] == nil {
t.Error("Expected userId in response")
}
// Verify identity was created
var count int
testDB.QueryRow("SELECT COUNT(*) FROM identities WHERE type = 'blockchain' AND identifier = $1",
strings.ToLower("0x742d35Cc6634C0532925a3b844Bc9e7595f4aB23")).Scan(&count)
if count != 1 {
t.Errorf("Expected 1 blockchain identity, found %d", count)
}
}
func TestRegisterBlockchain_FirstUserIsSuperuser(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"blockchainAddress": "0x1234567890123456789012345678901234567890",
"name": "First Blockchain User",
}
resp, _, err := makeRequest("POST", "/register-blockchain", payload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if resp.StatusCode != http.StatusCreated {
t.Errorf("Expected status 201, got %d", resp.StatusCode)
}
// Verify first user got SUPERUSER role
var role string
err = testDB.QueryRow(`
SELECT r.role FROM user_roles r
JOIN identities i ON i.user_id = r.user_id
WHERE i.identifier = $1
`, strings.ToLower("0x1234567890123456789012345678901234567890")).Scan(&role)
if err != nil {
t.Fatalf("Failed to query role: %v", err)
}
if role != "SUPERUSER" {
t.Errorf("Expected first blockchain user to have SUPERUSER role, got %s", role)
}
}
func TestRegisterBlockchain_DuplicateAddress(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"blockchainAddress": "0xDuplicateAddress12345678901234567890Dup",
"name": "First User",
}
// First registration
makeRequest("POST", "/register-blockchain", payload, nil)
// Second registration with same address
resp, body, _ := makeRequest("POST", "/register-blockchain", payload, nil)
if resp.StatusCode != http.StatusConflict {
t.Errorf("Expected status 409 Conflict, got %d: %s", resp.StatusCode, string(body))
}
}
func TestRegisterBlockchain_InvalidAddress(t *testing.T) {
cleanupTestData()
testCases := []struct {
address string
desc string
}{
{"", "empty address"},
{"short", "too short"},
{"0x", "just prefix"},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
payload := map[string]string{
"blockchainAddress": tc.address,
"name": "Test User",
}
resp, _, _ := makeRequest("POST", "/register-blockchain", payload, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for %s, got %d", tc.desc, resp.StatusCode)
}
})
}
}
func TestRegisterBlockchain_MissingName(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"blockchainAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f4aB23",
}
resp, _, _ := makeRequest("POST", "/register-blockchain", payload, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for missing name, got %d", resp.StatusCode)
}
}
// ==================== BLOCKCHAIN LOGIN TESTS ====================
func TestLoginBlockchain_Success(t *testing.T) {
cleanupTestData()
// Register blockchain user
regPayload := map[string]string{
"blockchainAddress": "0xLoginTest12345678901234567890123456789012",
"name": "Blockchain Login User",
}
makeRequest("POST", "/register-blockchain", regPayload, nil)
// Login - for test purposes, we skip signature verification or use mock
loginPayload := map[string]string{
"blockchainAddress": "0xLoginTest12345678901234567890123456789012",
"signature": "test-signature",
"message": "test-message",
}
resp, body, err := makeRequest("POST", "/login-blockchain", loginPayload, nil)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
// Check response - blockchain login may require valid signature
// This tests the endpoint exists and processes requests
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 200 or 401, got %d: %s", resp.StatusCode, string(body))
}
}
func TestLoginBlockchain_NonExistentAddress(t *testing.T) {
cleanupTestData()
payload := map[string]string{
"blockchainAddress": "0xNonExistent123456789012345678901234567890",
"signature": "test-signature",
"message": "test-message",
}
resp, _, _ := makeRequest("POST", "/login-blockchain", payload, nil)
if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusNotFound {
t.Errorf("Expected 401 or 404 for non-existent address, got %d", resp.StatusCode)
}
}
func TestLoginBlockchain_MissingFields(t *testing.T) {
cleanupTestData()
testCases := []struct {
payload map[string]string
desc string
}{
{map[string]string{"signature": "sig", "message": "msg"}, "missing address"},
{map[string]string{"blockchainAddress": "0x123"}, "missing signature and message"},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resp, _, _ := makeRequest("POST", "/login-blockchain", tc.payload, nil)
if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 400 or 401 for %s, got %d", tc.desc, resp.StatusCode)
}
})
}
}
// ==================== IDENTITY MANAGEMENT TESTS ====================
func TestGetIdentities_Success(t *testing.T) {
cleanupTestData()
// Register and login
regPayload := map[string]string{
"email": "identities@example.com",
"password": "IdentitiesPassword123",
"name": "Identity User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "identities@example.com",
"password": "IdentitiesPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// Get identities
headers := map[string]string{
"Authorization": "Bearer " + loginResult.AccessToken,
}
resp, body, _ := makeRequest("GET", "/identities", nil, headers)
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected 200, got %d: %s", resp.StatusCode, string(body))
}
var identities []Identity
if err := json.Unmarshal(body, &identities); err != nil {
t.Fatalf("Failed to parse identities: %v", err)
}
// Should have at least one email identity
hasEmailIdentity := false
for _, id := range identities {
if id.Type == "email" {
hasEmailIdentity = true
break
}
}
if !hasEmailIdentity {
t.Error("Expected email identity in response")
}
}
func TestGetIdentities_Unauthorized(t *testing.T) {
cleanupTestData()
resp, _, _ := makeRequest("GET", "/identities", nil, nil)
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 401, got %d", resp.StatusCode)
}
}
func TestLinkIdentity_Success(t *testing.T) {
cleanupTestData()
// Register with email
regPayload := map[string]string{
"email": "link@example.com",
"password": "LinkPassword123",
"name": "Link User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "link@example.com",
"password": "LinkPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// Link blockchain identity
linkPayload := map[string]string{
"type": "blockchain",
"identifier": "0xLinkedAddress123456789012345678901234567890",
}
headers := map[string]string{
"Authorization": "Bearer " + loginResult.AccessToken,
"X-CSRF-Token": loginResult.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/link-identity", linkPayload, headers)
// Check response - may succeed or fail depending on CSRF validation
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200, 201, or 403, got %d: %s", resp.StatusCode, string(body))
}
}
func TestLinkIdentity_DuplicateIdentity(t *testing.T) {
cleanupTestData()
// Create first user with email
reg1 := map[string]string{
"email": "user1@example.com",
"password": "User1Password123",
"name": "User 1",
}
makeRequest("POST", "/register-email-password", reg1, nil)
// Create second user with blockchain
reg2 := map[string]string{
"blockchainAddress": "0xExistingAddress12345678901234567890123456",
"name": "User 2",
}
makeRequest("POST", "/register-blockchain", reg2, nil)
// Login as user1
login1 := map[string]string{
"email": "user1@example.com",
"password": "User1Password123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", login1, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// Try to link the existing blockchain address to user1
linkPayload := map[string]string{
"type": "blockchain",
"identifier": "0xExistingAddress12345678901234567890123456",
}
headers := map[string]string{
"Authorization": "Bearer " + loginResult.AccessToken,
"X-CSRF-Token": loginResult.CsrfToken,
}
resp, _, _ := makeRequest("POST", "/link-identity", linkPayload, headers)
// Should fail as identity is already linked to another user
if resp.StatusCode != http.StatusConflict && resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 409, 400, or 403 for duplicate identity, got %d", resp.StatusCode)
}
}
func TestUnlinkIdentity_Success(t *testing.T) {
cleanupTestData()
// Register with email
regPayload := map[string]string{
"email": "unlink@example.com",
"password": "UnlinkPassword123",
"name": "Unlink User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
// Login
loginPayload := map[string]string{
"email": "unlink@example.com",
"password": "UnlinkPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// First link a blockchain identity
linkPayload := map[string]string{
"type": "blockchain",
"identifier": "0xToUnlink123456789012345678901234567890123",
}
headers := map[string]string{
"Authorization": "Bearer " + loginResult.AccessToken,
"X-CSRF-Token": loginResult.CsrfToken,
}
makeRequest("POST", "/link-identity", linkPayload, headers)
// Now unlink it
unlinkPayload := map[string]string{
"type": "blockchain",
"identifier": "0xToUnlink123456789012345678901234567890123",
}
resp, body, _ := makeRequest("POST", "/unlink-identity", unlinkPayload, headers)
// Should succeed or fail gracefully
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 200, 400, or 403, got %d: %s", resp.StatusCode, string(body))
}
}
func TestUnlinkIdentity_CannotUnlinkLastIdentity(t *testing.T) {
cleanupTestData()
// Register with email only
regPayload := map[string]string{
"email": "lastidentity@example.com",
"password": "LastIdentityPassword123",
"name": "Last Identity User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
// Login
loginPayload := map[string]string{
"email": "lastidentity@example.com",
"password": "LastIdentityPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var loginResult AuthTokenResponse
json.Unmarshal(loginBody, &loginResult)
// Try to unlink the only identity
unlinkPayload := map[string]string{
"type": "email",
"identifier": "lastidentity@example.com",
}
headers := map[string]string{
"Authorization": "Bearer " + loginResult.AccessToken,
"X-CSRF-Token": loginResult.CsrfToken,
}
resp, _, _ := makeRequest("POST", "/unlink-identity", unlinkPayload, headers)
// Should fail - can't unlink last identity
if resp.StatusCode == http.StatusOK {
t.Error("Should not be able to unlink last identity")
}
}
// ==================== LOGOUT ALL TESTS ====================
func TestLogoutAll_Success(t *testing.T) {
cleanupTestData()
// Register
regPayload := map[string]string{
"email": "logoutall@example.com",
"password": "LogoutAllPassword123",
"name": "Logout All User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
// Login multiple times to create multiple sessions
loginPayload := map[string]string{
"email": "logoutall@example.com",
"password": "LogoutAllPassword123",
}
// First session
_, loginBody1, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var session1 AuthTokenResponse
json.Unmarshal(loginBody1, &session1)
// Second session
_, loginBody2, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var session2 AuthTokenResponse
json.Unmarshal(loginBody2, &session2)
// Logout all using first session
headers := map[string]string{
"Authorization": "Bearer " + session1.AccessToken,
"X-CSRF-Token": session1.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/auth/logout-all", nil, headers)
// Check result
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200 or 403, got %d: %s", resp.StatusCode, string(body))
}
// If successful, both refresh tokens should be revoked
if resp.StatusCode == http.StatusOK {
// Try to refresh with second session
refreshPayload := map[string]string{
"refreshToken": session2.RefreshToken,
}
resp2, _, _ := makeRequest("POST", "/auth/refresh", refreshPayload, nil)
if resp2.StatusCode != http.StatusUnauthorized {
t.Error("Second session should be revoked after logout-all")
}
}
}
// ==================== ADMIN ENDPOINT TESTS ====================
func getAdminLogin(t *testing.T) AuthTokenResponse {
t.Helper()
cleanupTestData()
// First user is SUPERUSER (which has admin privileges)
regPayload := map[string]string{
"email": "admin@example.com",
"password": "AdminPassword123",
"name": "Admin User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "admin@example.com",
"password": "AdminPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var result AuthTokenResponse
json.Unmarshal(loginBody, &result)
return result
}
func getClientLogin(t *testing.T) AuthTokenResponse {
t.Helper()
// Second user is CLIENT
regPayload := map[string]string{
"email": "client@example.com",
"password": "ClientPassword123",
"name": "Client User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "client@example.com",
"password": "ClientPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var result AuthTokenResponse
json.Unmarshal(loginBody, &result)
return result
}
func TestAdminGetUsers_Success(t *testing.T) {
adminToken := getAdminLogin(t)
headers := map[string]string{
"Authorization": "Bearer " + adminToken.AccessToken,
}
resp, body, _ := makeRequest("GET", "/admin/users", nil, headers)
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected 200, got %d: %s", resp.StatusCode, string(body))
}
}
func TestAdminGetUsers_Unauthorized(t *testing.T) {
getAdminLogin(t)
clientToken := getClientLogin(t)
// Client should not have admin access
headers := map[string]string{
"Authorization": "Bearer " + clientToken.AccessToken,
}
resp, _, _ := makeRequest("GET", "/admin/users", nil, headers)
if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 403 or 401, got %d", resp.StatusCode)
}
}
func TestAdminPromoteRole_Success(t *testing.T) {
adminToken := getAdminLogin(t)
getClientLogin(t)
// Get client user ID
var clientID int
testDB.QueryRow("SELECT id FROM users WHERE email = $1", "client@example.com").Scan(&clientID)
// Promote client to STAFF
promotePayload := map[string]interface{}{
"userId": clientID,
"role": "STAFF",
}
headers := map[string]string{
"Authorization": "Bearer " + adminToken.AccessToken,
"X-CSRF-Token": adminToken.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/admin/users/promote-role", promotePayload, headers)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200 or 403, got %d: %s", resp.StatusCode, string(body))
}
// Verify if promotion succeeded
if resp.StatusCode == http.StatusOK {
var role string
err := testDB.QueryRow("SELECT role FROM user_roles WHERE user_id = $1 AND role = 'STAFF'", clientID).Scan(&role)
if err != nil {
t.Error("Client should have STAFF role after promotion")
}
}
}
func TestAdminDemoteRole_Success(t *testing.T) {
adminToken := getAdminLogin(t)
getClientLogin(t)
// First promote to STAFF
var clientID int
testDB.QueryRow("SELECT id FROM users WHERE email = $1", "client@example.com").Scan(&clientID)
// Add STAFF role directly
testDB.Exec("INSERT INTO user_roles (user_id, role) VALUES ($1, 'STAFF') ON CONFLICT DO NOTHING", clientID)
// Now demote from STAFF
demotePayload := map[string]interface{}{
"userId": clientID,
"role": "STAFF",
}
headers := map[string]string{
"Authorization": "Bearer " + adminToken.AccessToken,
"X-CSRF-Token": adminToken.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/admin/users/demote-role", demotePayload, headers)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200 or 403, got %d: %s", resp.StatusCode, string(body))
}
}
func TestAdminPromoteRole_ClientCannotPromote(t *testing.T) {
getAdminLogin(t)
clientToken := getClientLogin(t)
promotePayload := map[string]interface{}{
"userId": 1,
"role": "STAFF",
}
headers := map[string]string{
"Authorization": "Bearer " + clientToken.AccessToken,
"X-CSRF-Token": clientToken.CsrfToken,
}
resp, _, _ := makeRequest("POST", "/admin/users/promote-role", promotePayload, headers)
if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 403 or 401, got %d", resp.StatusCode)
}
}
// ==================== SUPERUSER ENDPOINT TESTS ====================
func TestSuperuserPromote_Success(t *testing.T) {
adminToken := getAdminLogin(t)
getClientLogin(t)
var clientID int
testDB.QueryRow("SELECT id FROM users WHERE email = $1", "client@example.com").Scan(&clientID)
// Promote client to ADMIN (superuser can promote to any role including ADMIN)
promotePayload := map[string]interface{}{
"userId": clientID,
"role": "ADMIN",
}
headers := map[string]string{
"Authorization": "Bearer " + adminToken.AccessToken,
"X-CSRF-Token": adminToken.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/superuser/promote", promotePayload, headers)
// Superuser endpoint may succeed or require CSRF
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200 or 403, got %d: %s", resp.StatusCode, string(body))
}
}
func TestSuperuserDemote_Success(t *testing.T) {
adminToken := getAdminLogin(t)
getClientLogin(t)
var clientID int
testDB.QueryRow("SELECT id FROM users WHERE email = $1", "client@example.com").Scan(&clientID)
// Add ADMIN role to client
testDB.Exec("INSERT INTO user_roles (user_id, role) VALUES ($1, 'ADMIN') ON CONFLICT DO NOTHING", clientID)
// Demote from ADMIN
demotePayload := map[string]interface{}{
"userId": clientID,
"role": "ADMIN",
}
headers := map[string]string{
"Authorization": "Bearer " + adminToken.AccessToken,
"X-CSRF-Token": adminToken.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/superuser/demote", demotePayload, headers)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200 or 403, got %d: %s", resp.StatusCode, string(body))
}
}
func TestSuperuserTransfer_Success(t *testing.T) {
adminToken := getAdminLogin(t)
getClientLogin(t)
var clientID int
testDB.QueryRow("SELECT id FROM users WHERE email = $1", "client@example.com").Scan(&clientID)
// Transfer superuser to client
transferPayload := map[string]interface{}{
"toUserId": clientID,
"reason": "Testing transfer",
}
headers := map[string]string{
"Authorization": "Bearer " + adminToken.AccessToken,
"X-CSRF-Token": adminToken.CsrfToken,
}
resp, body, _ := makeRequest("POST", "/superuser/transfer", transferPayload, headers)
// Transfer may succeed or be blocked by CSRF
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected 200 or 403, got %d: %s", resp.StatusCode, string(body))
}
}
func TestSuperuserTransfer_ClientCannotTransfer(t *testing.T) {
getAdminLogin(t)
clientToken := getClientLogin(t)
var adminID int
testDB.QueryRow("SELECT id FROM users WHERE email = $1", "admin@example.com").Scan(&adminID)
transferPayload := map[string]interface{}{
"toUserId": adminID,
"reason": "Malicious transfer attempt",
}
headers := map[string]string{
"Authorization": "Bearer " + clientToken.AccessToken,
"X-CSRF-Token": clientToken.CsrfToken,
}
resp, _, _ := makeRequest("POST", "/superuser/transfer", transferPayload, headers)
if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected 403 or 401, got %d", resp.StatusCode)
}
}
// ==================== VALIDATION TESTS ====================
func TestRegistration_EmptyPayload(t *testing.T) {
cleanupTestData()
resp, _, _ := makeRequest("POST", "/register-email-password", map[string]string{}, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for empty payload, got %d", resp.StatusCode)
}
}
func TestLogin_EmptyPayload(t *testing.T) {
cleanupTestData()
resp, _, _ := makeRequest("POST", "/login-email-password", map[string]string{}, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for empty login payload, got %d", resp.StatusCode)
}
}
func TestRegister_LongName(t *testing.T) {
cleanupTestData()
// Name that's too long (> 100 chars based on schema)
longName := strings.Repeat("a", 150)
payload := map[string]string{
"email": "longname@example.com",
"password": "LongNamePassword123",
"name": longName,
}
resp, _, _ := makeRequest("POST", "/register-email-password", payload, nil)
// Should either truncate or reject
if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusCreated {
t.Errorf("Expected 400 or 201 for long name, got %d", resp.StatusCode)
}
}
func TestRegister_SQLInjection(t *testing.T) {
cleanupTestData()
// Attempt SQL injection in email
payload := map[string]string{
"email": "test@example.com'; DROP TABLE users;--",
"password": "SQLInjectionTest123",
"name": "SQL Injection",
}
resp, _, _ := makeRequest("POST", "/register-email-password", payload, nil)
// Should fail validation (invalid email format)
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected 400 for SQL injection attempt, got %d", resp.StatusCode)
}
// Verify users table still exists
var count int
err := testDB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
t.Error("Users table should still exist after SQL injection attempt")
}
}
func TestRegister_XSSAttempt(t *testing.T) {
cleanupTestData()
// Attempt XSS in name field
payload := map[string]string{
"email": "xss@example.com",
"password": "XSSTestPassword123",
"name": "<script>alert('xss')</script>",
}
resp, _, _ := makeRequest("POST", "/register-email-password", payload, nil)
// Should either sanitize or accept (XSS prevention is on output, not input)
if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusCreated {
t.Errorf("Expected 400 or 201 for XSS attempt, got %d", resp.StatusCode)
}
}
// ==================== TOKEN EXPIRY TESTS ====================
func TestAccessToken_Expiry(t *testing.T) {
cleanupTestData()
// This test would require waiting for token expiry or mocking time
// For now, verify token contains proper claims
regPayload := map[string]string{
"email": "expiry@example.com",
"password": "ExpiryPassword123",
"name": "Expiry User",
}
makeRequest("POST", "/register-email-password", regPayload, nil)
loginPayload := map[string]string{
"email": "expiry@example.com",
"password": "ExpiryPassword123",
}
_, loginBody, _ := makeRequest("POST", "/login-email-password", loginPayload, nil)
var result AuthTokenResponse
json.Unmarshal(loginBody, &result)
// Verify we got tokens
if result.AccessToken == "" {
t.Error("Should receive access token")
}
if result.RefreshToken == "" {
t.Error("Should receive refresh token")
}
if result.ExpiresIn <= 0 {
t.Error("ExpiresIn should be positive")
}
}
// ==================== CONCURRENT REQUEST TESTS ====================
func TestConcurrentRegistrations(t *testing.T) {
cleanupTestData()
// Test that concurrent registrations with same email properly handle conflicts
done := make(chan bool, 10)
successCount := 0
for i := 0; i < 10; i++ {
go func(idx int) {
payload := map[string]string{
"email": "concurrent@example.com",
"password": "ConcurrentPassword123",
"name": fmt.Sprintf("Concurrent User %d", idx),
}
resp, _, _ := makeRequest("POST", "/register-email-password", payload, nil)
if resp.StatusCode == http.StatusCreated {
successCount++
}
done <- true
}(i)
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
// Only one should succeed
if successCount > 1 {
t.Errorf("Expected at most 1 successful registration, got %d", successCount)
}
// Verify only one user exists
var count int
testDB.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", "concurrent@example.com").Scan(&count)
if count != 1 {
t.Errorf("Expected exactly 1 user, found %d", count)
}
}
// ==================== HTTP METHOD TESTS ====================
func TestEndpoints_WrongMethod(t *testing.T) {
cleanupTestData()
// POST endpoints should reject GET
testCases := []struct {
path string
method string
}{
{"/register-email-password", "GET"},
{"/login-email-password", "GET"},
{"/auth/refresh", "GET"},
{"/auth/logout", "GET"},
}
for _, tc := range testCases {
t.Run(tc.path+"_"+tc.method, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, nil)
rr := httptest.NewRecorder()
handler := getHandler(tc.path)
if handler != nil {
handler(rr, req)
if rr.Code == http.StatusOK || rr.Code == http.StatusCreated {
// Handler may not check method, that's implementation-specific
// Just verify endpoint exists
}
}
})
}
}
// ==================== CONTENT-TYPE TESTS ====================
func TestRegister_WrongContentType(t *testing.T) {
cleanupTestData()
req := httptest.NewRequest("POST", "/register-email-password", strings.NewReader("email=test@example.com&password=Test123&name=Test"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
handleRegisterEmailPassword(rr, req)
// Should reject non-JSON content type
if rr.Code == http.StatusCreated {
// Some implementations may accept form data, just verify it's handled
t.Log("Handler accepts form-urlencoded data")
}
}