Replace all unhandled resp.Body.Close() calls throughout the test suite:
- Deferred calls: replace 'defer VAR.Body.Close()' with a closure that explicitly
discards the error via 'defer func(Body io.ReadCloser) { _ = Body.Close() }(VAR.Body)'
- Immediate calls: replace 'VAR.Body.Close()' with '_ = VAR.Body.Close()'
Replace all unhandled json.NewDecoder(VAR.Body).Decode(&x) calls with error capture
and require.NoError assertion. Files using httptest.ResponseRecorder (collections_preview,
processing_issues) use 'err :=' declaration; suite-style tests (scanner_integration,
dashboard_integration) use s.T() instead of t.
165 lines
5.3 KiB
Go
165 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Helper functions for database verification and test utilities
|
|
// These functions reduce code duplication and ensure consistent database state verification
|
|
|
|
// verifyDeviceCreated verifies a device exists in database with expected values
|
|
func verifyDeviceCreated(t *testing.T, db *database.Queries, deviceID uuid.UUID, expectedName, expectedType, expectedIdentifier string) {
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
device, err := db.GetDevice(context.Background(), pgDeviceID)
|
|
require.NoError(t, err, "Device should exist in database")
|
|
|
|
assert.Equal(t, expectedName, device.DeviceName, "Device name should match")
|
|
assert.Equal(t, expectedType, device.DeviceType, "Device type should match")
|
|
assert.Equal(t, expectedIdentifier, device.DeviceIdentifier, "Device identifier should match")
|
|
assert.NotEmpty(t, device.AuthToken, "Device should have auth token")
|
|
}
|
|
|
|
// verifyDeviceDeleted verifies a device does not exist in database
|
|
func verifyDeviceDeleted(t *testing.T, db *database.Queries, deviceID uuid.UUID) {
|
|
pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}
|
|
_, err := db.GetDevice(context.Background(), pgDeviceID)
|
|
assert.Error(t, err, "Device should be deleted from database")
|
|
}
|
|
|
|
// verifyUserField verifies a user has expected field value in database
|
|
func verifyUserField(t *testing.T, db *database.Queries, userID uuid.UUID, field string, expected interface{}) {
|
|
pgUserID := pgtype.UUID{Bytes: [16]byte(userID), Valid: true}
|
|
user, err := db.GetUser(context.Background(), pgUserID)
|
|
require.NoError(t, err, "User should exist in database")
|
|
|
|
switch field {
|
|
case "email":
|
|
if em, ok := expected.(string); ok {
|
|
assert.Equal(t, em, user.Email, "Email should match")
|
|
}
|
|
case "first_name":
|
|
if fn, ok := expected.(string); ok {
|
|
assert.Equal(t, fn, user.FirstName.String, "First name should match")
|
|
}
|
|
case "last_name":
|
|
if ln, ok := expected.(string); ok {
|
|
assert.Equal(t, ln, user.LastName.String, "Last name should match")
|
|
}
|
|
case "username":
|
|
if un, ok := expected.(string); ok {
|
|
assert.Equal(t, un, user.Username, "Username should match")
|
|
}
|
|
case "theme":
|
|
if th, ok := expected.(string); ok {
|
|
assert.Equal(t, th, user.Theme.String, "Theme should match")
|
|
}
|
|
}
|
|
}
|
|
|
|
// verifyMediaItemInDB verifies a media item exists in database
|
|
func verifyMediaItemInDB(t *testing.T, db *database.Queries, mediaID uuid.UUID) {
|
|
pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true}
|
|
_, err := db.GetMediaItem(context.Background(), pgMediaID)
|
|
require.NoError(t, err, "Media item should exist in database")
|
|
}
|
|
|
|
// verifyMediaItemDeleted verifies a media item does not exist in database
|
|
func verifyMediaItemDeleted(t *testing.T, db *database.Queries, mediaID uuid.UUID) {
|
|
pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true}
|
|
_, err := db.GetMediaItem(context.Background(), pgMediaID)
|
|
assert.Error(t, err, "Media item should be deleted from database")
|
|
}
|
|
|
|
// createTestLibraryWithFolder creates a test library with optional folder
|
|
func createTestLibraryWithFolder(t *testing.T, ts *httptest.Server, token, name string, withFolder bool) string {
|
|
libReq := map[string]interface{}{
|
|
"name": name,
|
|
"type": "ebooks",
|
|
}
|
|
libBody, _ := json.Marshal(libReq)
|
|
|
|
libHTTP, _ := http.NewRequest("POST", ts.URL+"/api/libraries", bytes.NewBuffer(libBody))
|
|
libHTTP.Header.Set("Content-Type", "application/json")
|
|
libHTTP.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(libHTTP)
|
|
require.NoError(t, err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(resp.Body)
|
|
require.Equal(t, http.StatusCreated, resp.StatusCode, "Library creation should succeed")
|
|
|
|
var libResponse map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&libResponse)
|
|
require.NoError(t, err)
|
|
libraryID := libResponse["id"].(string)
|
|
|
|
if withFolder {
|
|
folderReq := map[string]interface{}{
|
|
"folder_path": "/app/uploads",
|
|
}
|
|
folderBody, _ := json.Marshal(folderReq)
|
|
|
|
folderHTTP, _ := http.NewRequest("POST", fmt.Sprintf("%s/api/libraries/%s/folders", ts.URL, libraryID), bytes.NewBuffer(folderBody))
|
|
folderHTTP.Header.Set("Content-Type", "application/json")
|
|
folderHTTP.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
folderResp, err := client.Do(folderHTTP)
|
|
require.NoError(t, err)
|
|
defer func(Body io.ReadCloser) {
|
|
_ = Body.Close()
|
|
}(folderResp.Body)
|
|
require.Equal(t, http.StatusCreated, folderResp.StatusCode, "Folder creation should succeed")
|
|
}
|
|
|
|
return libraryID
|
|
}
|
|
|
|
// runConcurrent executes functions concurrently and waits for all to complete
|
|
func runConcurrent(t *testing.T, maxConcurrent int, fns []func() error) []error {
|
|
if len(fns) == 0 {
|
|
return nil
|
|
}
|
|
if len(fns) < maxConcurrent {
|
|
maxConcurrent = len(fns)
|
|
}
|
|
|
|
errors := make(chan error, len(fns))
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < maxConcurrent; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
if err := fns[idx](); err != nil {
|
|
errors <- err
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
var allErrors []error
|
|
for err := range errors {
|
|
allErrors = append(allErrors, err)
|
|
}
|
|
|
|
return allErrors
|
|
}
|