test: improve test isolation and setup management
Add Token and RegularToken fields to TestServerSetup for pre-authenticated access. Update setupTestServer to create fresh users with valid tokens at initialization time. Simplify createTestMediaItemID to use setup.Token. Remove loginTestUser, loginRegularUser, loginAdminUser functions in favor of setup.Token/setup.RegularToken. Update createTestUserOnce and getTestUserID/getRegularUserID to be idempotent. Update all test files to use setup.Token instead of calling login helpers.
This commit is contained in:
+143
-153
@@ -84,6 +84,8 @@ type TestServerSetup struct {
|
||||
CleanupCancel context.CancelFunc
|
||||
QueueCtx context.Context
|
||||
QueueCancel context.CancelFunc
|
||||
Token string
|
||||
RegularToken string
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
@@ -207,39 +209,13 @@ func setupDeviceTest(t *testing.T) *TestDeviceSetup {
|
||||
}
|
||||
}
|
||||
|
||||
// createTestUserOnce creates a test user with deterministic UUID
|
||||
// createTestUserOnce returns the pre-created test user info
|
||||
func createTestUserOnce(t *testing.T, db *database.Queries) UserTestData {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if user exists and delete for fresh state
|
||||
existingUser, err := db.GetUserByEmail(ctx, "testuser@example.com")
|
||||
if err == nil {
|
||||
// User exists, delete them to ensure fresh password
|
||||
err = db.DeleteUser(ctx, existingUser.ID)
|
||||
if err != nil {
|
||||
// If delete fails (user might be referenced elsewhere), log and continue
|
||||
t.Logf("Warning: Could not delete existing test user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a fresh test user with a valid password
|
||||
// Password: "Test@Pass123!" meets complexity requirements
|
||||
// This is a bcrypt hash for "Test@Pass123!"
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
newUser, err := db.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testuser@example.com",
|
||||
Username: "testuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Test", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "admin",
|
||||
})
|
||||
require.NoError(t, err, "Should create test user")
|
||||
|
||||
// Get the user ID from created user
|
||||
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[0:16])
|
||||
user, err := db.GetUserByEmail(ctx, "testuser@example.com")
|
||||
require.NoError(t, err, "Test user should exist (created by setupTestServer)")
|
||||
userUUID, err := uuid.FromBytes(user.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse user UUID")
|
||||
|
||||
return UserTestData{
|
||||
ID: userUUID,
|
||||
Email: "testuser@example.com",
|
||||
@@ -248,6 +224,79 @@ func createTestUserOnce(t *testing.T, db *database.Queries) UserTestData {
|
||||
}
|
||||
}
|
||||
|
||||
// createRegularUserOnce creates a regular (non-admin) test user with unique credentials
|
||||
func createRegularUserOnce(t *testing.T, db *database.Queries) UserTestData {
|
||||
ctx := context.Background()
|
||||
uniqueID := uuid.New().String()[:8]
|
||||
email := fmt.Sprintf("regularuser-%s@example.com", uniqueID)
|
||||
username := fmt.Sprintf("regularuser-%s", uniqueID)
|
||||
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
newUser, err := db.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: email,
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Regular", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "user",
|
||||
})
|
||||
require.NoError(t, err, "Should create regular test user")
|
||||
|
||||
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[0:16])
|
||||
require.NoError(t, err, "Should parse user UUID")
|
||||
|
||||
createDefaultCollectionsForUser(t, db, pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx := context.Background()
|
||||
db.DeleteUser(ctx, pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true})
|
||||
})
|
||||
|
||||
return UserTestData{
|
||||
ID: userUUID,
|
||||
Email: email,
|
||||
Username: username,
|
||||
Password: "Test@Pass123!",
|
||||
}
|
||||
}
|
||||
|
||||
// uuidToPGType converts uuid.UUID to pgtype.UUID
|
||||
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
||||
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
|
||||
}
|
||||
|
||||
// createDefaultCollectionsForUser creates the 4 default system collections for a user
|
||||
func createDefaultCollectionsForUser(t *testing.T, db *database.Queries, userID pgtype.UUID) {
|
||||
ctx := context.Background()
|
||||
defaultCollections := []struct {
|
||||
Name string
|
||||
Description string
|
||||
Icon string
|
||||
Color string
|
||||
QueryType string
|
||||
Priority int32
|
||||
}{
|
||||
{"continue-reading", "Books you're currently reading (0 < progress < 1)", "📖", "#7aa2f7", "continue-reading", 1},
|
||||
{"recently-added", "Newly added items to this library", "🆕", "#9ece6a", "recently-added", 2},
|
||||
{"recently-read", "Books you've finished (progress >= 1)", "✅", "#e0af68", "recently-read", 3},
|
||||
{"not-started", "Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", "not-started", 4},
|
||||
}
|
||||
|
||||
for _, col := range defaultCollections {
|
||||
_, err := db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
|
||||
UserID: userID,
|
||||
Name: col.Name,
|
||||
Description: pgtype.Text{String: col.Description, Valid: true},
|
||||
Icon: pgtype.Text{String: col.Icon, Valid: true},
|
||||
Color: pgtype.Text{String: col.Color, Valid: true},
|
||||
ShowOnDashboard: pgtype.Bool{Bool: true, Valid: true},
|
||||
QueryType: pgtype.Text{String: col.QueryType, Valid: true},
|
||||
Priority: pgtype.Int4{Int32: col.Priority, Valid: true},
|
||||
})
|
||||
require.NoError(t, err, "Should create default collection: "+col.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// loginUserWithCredentials performs explicit login with provided credentials
|
||||
func loginUserWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
|
||||
loginRequest := map[string]interface{}{
|
||||
@@ -467,6 +516,51 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
// Create test server
|
||||
ts := httptest.NewServer(e)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Delete existing test users if they exist (cascades to delete collections, media items, etc.)
|
||||
for _, email := range []string{"testuser@example.com", "testregularuser@example.com"} {
|
||||
existingUser, err := queries.GetUserByEmail(ctx, email)
|
||||
if err == nil {
|
||||
queries.DeleteUser(ctx, existingUser.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Create fresh admin test user
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
adminUser, err := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testuser@example.com",
|
||||
Username: "testuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Test", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "admin",
|
||||
})
|
||||
require.NoError(t, err, "Failed to create admin test user")
|
||||
|
||||
adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse admin user UUID")
|
||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||
|
||||
// Create fresh regular test user
|
||||
regularUser, err := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testregularuser@example.com",
|
||||
Username: "testregularuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Regular", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "user",
|
||||
})
|
||||
require.NoError(t, err, "Failed to create regular test user")
|
||||
|
||||
regularUUID, err := uuid.FromBytes(regularUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse regular user UUID")
|
||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: regularUUID, Valid: true})
|
||||
|
||||
// Login to get tokens
|
||||
adminToken := loginWithCredentials(t, ts, "testuser@example.com", "Test@Pass123!")
|
||||
regularToken := loginWithCredentials(t, ts, "testregularuser@example.com", "Test@Pass123!")
|
||||
|
||||
// Create TestServerSetup struct with all resources
|
||||
setup := &TestServerSetup{
|
||||
Server: ts,
|
||||
@@ -478,6 +572,8 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
CleanupCancel: cleanupCancel,
|
||||
QueueCtx: queueCtx,
|
||||
QueueCancel: queueCancel,
|
||||
Token: adminToken,
|
||||
RegularToken: regularToken,
|
||||
}
|
||||
|
||||
// Register cleanup function to run automatically when test completes
|
||||
@@ -490,14 +586,10 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
return setup
|
||||
}
|
||||
|
||||
// loginTestUser logs in a test user and returns the JWT token
|
||||
func loginTestUser(t *testing.T, ts *httptest.Server, db *database.Queries) string {
|
||||
// Ensure test user exists first
|
||||
_ = getTestUserID(t, db)
|
||||
|
||||
func loginWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
|
||||
loginRequest := map[string]interface{}{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
"login": email,
|
||||
"password": password,
|
||||
}
|
||||
body, _ := json.Marshal(loginRequest)
|
||||
|
||||
@@ -506,7 +598,7 @@ func loginTestUser(t *testing.T, ts *httptest.Server, db *database.Queries) stri
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err, "Failed to login test user")
|
||||
require.NoError(t, err, "Failed to login")
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed")
|
||||
@@ -523,125 +615,27 @@ func loginTestUser(t *testing.T, ts *httptest.Server, db *database.Queries) stri
|
||||
|
||||
func getTestUserID(t *testing.T, db *database.Queries) uuid.UUID {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if test user exists and delete them first to ensure fresh state
|
||||
user, err := db.GetUserByEmail(ctx, "testuser@example.com")
|
||||
if err == nil {
|
||||
// User exists, delete them to ensure fresh password
|
||||
err = db.DeleteUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
// If delete fails (user might be referenced elsewhere), log and continue
|
||||
t.Logf("Warning: Could not delete existing test user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a fresh test user with a valid password
|
||||
// Password: "Test@Pass123!" meets complexity requirements
|
||||
// This is the bcrypt hash for "Test@Pass123!"
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
|
||||
newUser, err := db.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testuser@example.com",
|
||||
Username: "testuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Test", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "admin",
|
||||
})
|
||||
require.NoError(t, err, "Failed to create test user")
|
||||
|
||||
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Test user should exist")
|
||||
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse user UUID")
|
||||
return userUUID
|
||||
}
|
||||
|
||||
func loginRegularUser(t *testing.T, ts *httptest.Server, db *database.Queries) string {
|
||||
// Ensure test user exists first
|
||||
_ = getRegularUserID(t, db)
|
||||
|
||||
loginRequest := map[string]interface{}{
|
||||
"login": "testregularuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
body, _ := json.Marshal(loginRequest)
|
||||
|
||||
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err, "Failed to login test user")
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed")
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
token, ok := result["access_token"].(string)
|
||||
require.True(t, ok, "Should have access_token")
|
||||
require.NotEmpty(t, token, "Access token should not be empty")
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func getRegularUserID(t *testing.T, db *database.Queries) uuid.UUID {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if test user exists and delete them first to ensure fresh state
|
||||
user, err := db.GetUserByEmail(ctx, "testregularuser@example.com")
|
||||
if err == nil {
|
||||
// User exists, delete them to ensure fresh password
|
||||
err = db.DeleteUser(ctx, user.ID)
|
||||
if err != nil {
|
||||
// If delete fails (user might be referenced elsewhere), log and continue
|
||||
t.Logf("Warning: Could not delete existing test user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a fresh test user with a valid password
|
||||
// Password: "Test@Pass123!" meets complexity requirements
|
||||
// This is the bcrypt hash for "Test@Pass123!"
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
|
||||
newUser, err := db.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testregularuser@example.com",
|
||||
Username: "testregularuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Test", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "user",
|
||||
})
|
||||
require.NoError(t, err, "Failed to create test user")
|
||||
|
||||
userUUID, err := uuid.FromBytes(newUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Regular user should exist")
|
||||
userUUID, err := uuid.FromBytes(user.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse user UUID")
|
||||
return userUUID
|
||||
}
|
||||
|
||||
// createTestMediaItemID creates a test media item and returns its ID
|
||||
func createTestMediaItemID(t *testing.T, ts *httptest.Server) string {
|
||||
// Generate unique library name to avoid conflicts between tests
|
||||
func createTestMediaItemID(t *testing.T, setup *TestServerSetup) string {
|
||||
uniqueName := fmt.Sprintf("Test Library %d", time.Now().UnixNano())
|
||||
|
||||
// Get a fresh token to ensure we have a valid user
|
||||
// (previous tests may have deleted/recreated the test user)
|
||||
loginReq := map[string]interface{}{
|
||||
"login": "testuser@example.com",
|
||||
"password": "Test@Pass123!",
|
||||
}
|
||||
loginBody, _ := json.Marshal(loginReq)
|
||||
loginReqHTTP, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||
loginReqHTTP.Header.Set("Content-Type", "application/json")
|
||||
httpClient := &http.Client{}
|
||||
loginResp, err := httpClient.Do(loginReqHTTP)
|
||||
require.NoError(t, err)
|
||||
defer loginResp.Body.Close()
|
||||
var loginResult map[string]interface{}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
validToken := loginResult["access_token"].(string)
|
||||
|
||||
// First create a library
|
||||
libReq := map[string]interface{}{
|
||||
"name": uniqueName,
|
||||
"description": "A test library for media items",
|
||||
@@ -649,9 +643,9 @@ func createTestMediaItemID(t *testing.T, ts *httptest.Server) string {
|
||||
}
|
||||
libBody, _ := json.Marshal(libReq)
|
||||
|
||||
req, _ := http.NewRequest("POST", ts.URL+"/api/libraries", bytes.NewBuffer(libBody))
|
||||
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+validToken)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
@@ -664,23 +658,20 @@ func createTestMediaItemID(t *testing.T, ts *httptest.Server) string {
|
||||
|
||||
libData := libResult["id"].(string)
|
||||
|
||||
// Add a folder to the library (required before adding media items)
|
||||
// Use /app/uploads which is already mounted in the test container
|
||||
folderReq := map[string]interface{}{
|
||||
"folder_path": "/app/uploads",
|
||||
}
|
||||
folderBody, _ := json.Marshal(folderReq)
|
||||
|
||||
folderReqHTTP, _ := http.NewRequest("POST", ts.URL+"/api/libraries/"+libData+"/folders", bytes.NewBuffer(folderBody))
|
||||
folderReqHTTP, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries/"+libData+"/folders", bytes.NewBuffer(folderBody))
|
||||
folderReqHTTP.Header.Set("Content-Type", "application/json")
|
||||
folderReqHTTP.Header.Set("Authorization", "Bearer "+validToken)
|
||||
folderReqHTTP.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
folderResp, err := httpClient.Do(folderReqHTTP)
|
||||
require.NoError(t, err)
|
||||
defer folderResp.Body.Close()
|
||||
require.Equal(t, http.StatusCreated, folderResp.StatusCode, "Library folder creation is required before adding media items")
|
||||
|
||||
// Create a test media item
|
||||
mediaItemReq := map[string]interface{}{
|
||||
"library_id": libData,
|
||||
"title": "Test Media Item",
|
||||
@@ -691,9 +682,9 @@ func createTestMediaItemID(t *testing.T, ts *httptest.Server) string {
|
||||
}
|
||||
mediaItemBody, _ := json.Marshal(mediaItemReq)
|
||||
|
||||
req2, _ := http.NewRequest("POST", ts.URL+"/api/media-items", bytes.NewBuffer(mediaItemBody))
|
||||
req2, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaItemBody))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("Authorization", "Bearer "+validToken)
|
||||
req2.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
|
||||
resp2, err := httpClient.Do(req2)
|
||||
require.NoError(t, err)
|
||||
@@ -706,10 +697,9 @@ func createTestMediaItemID(t *testing.T, ts *httptest.Server) string {
|
||||
|
||||
mediaItemID := mediaItemResult["id"].(string)
|
||||
|
||||
// Cleanup: delete the library after the test
|
||||
t.Cleanup(func() {
|
||||
deleteReq, _ := http.NewRequest("DELETE", ts.URL+"/api/libraries/"+libData, nil)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+validToken)
|
||||
deleteReq, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/libraries/"+libData, nil)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
httpClient.Do(deleteReq)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user