Files
bookhoard/cmd/server/tests/device_cap_test.go
T
john-okeefe 786e809631 fix(tests): resolve type assertion and request body issues in tests
- Fix float64 type assertions for JSON numbers in conflicts bulk operations
- Create fresh HTTP request body for duplicate book tests
- Add nil checks for type assertions in device cap tests
- Properly extract user_id from JWT for existing users
- Trim trailing whitespace from response bodies
- All 3 previously failing tests now passing

Test results: 19/22 passing (86.4%)
Fixes: TestCollectionsBulkOperations, TestConflictsBulkDismiss, TestUpdateUserMaxDevices
2026-02-06 21:59:04 -05:00

473 lines
14 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestUpdateUserMaxDevices tests the admin endpoint for updating user device cap
func TestUpdateUserMaxDevices(t *testing.T) {
ts, db, _ := setupTestServer(t)
defer ts.Close()
// Create test user with admin role
loginTestUser(t, ts, db)
adminUserID := getTestUserID(t, db)
adminToken := getAdminToken(t, ts, adminUserID)
// Create a test user
userID := createTestUserForMaxDevices(t, ts, adminToken)
tests := []struct {
name string
maxDevices int32
expectedStatus int
expectedBody string
}{
{
name: "Update to 5 devices",
maxDevices: 5,
expectedStatus: http.StatusOK,
expectedBody: `{"message":"max devices updated"}`,
},
{
name: "Update to 10 devices (default)",
maxDevices: 10,
expectedStatus: http.StatusOK,
expectedBody: `{"message":"max devices updated"}`,
},
{
name: "Update to 50 devices",
maxDevices: 50,
expectedStatus: http.StatusOK,
expectedBody: `{"message":"max devices updated"}`,
},
{
name: "Update to 100 devices (maximum)",
maxDevices: 100,
expectedStatus: http.StatusOK,
expectedBody: `{"message":"max devices updated"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
payload := map[string]interface{}{
"max_devices": tt.maxDevices,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", ts.URL+"/api/auth/users/"+userID.String()+"/max-devices", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+adminToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tt.expectedStatus, resp.StatusCode, "expected status code")
body := new(bytes.Buffer)
body.ReadFrom(resp.Body)
// Trim trailing whitespace/newline from response body
assert.Equal(t, tt.expectedBody, strings.TrimSpace(body.String()), "expected response body")
})
}
}
// TestUpdateUserMaxDevicesValidation tests validation of max_devices parameter
func TestUpdateUserMaxDevicesValidation(t *testing.T) {
ts, db, _ := setupTestServer(t)
defer ts.Close()
// Create admin user and get token
adminToken := loginTestUser(t, ts, db)
createAdminUser(t, ts, adminToken)
adminUserID := getTestUserID(t, db)
adminToken = getAdminToken(t, ts, adminUserID)
// Create test user
userID := createTestUserForMaxDevices(t, ts, adminToken)
tests := []struct {
name string
maxDevices int32
expectedStatus int
}{
{
name: "Zero devices (below minimum)",
maxDevices: 0,
expectedStatus: http.StatusBadRequest,
},
{
name: "Negative devices",
maxDevices: -1,
expectedStatus: http.StatusBadRequest,
},
{
name: "101 devices (above maximum)",
maxDevices: 101,
expectedStatus: http.StatusBadRequest,
},
{
name: "1000 devices (far above maximum)",
maxDevices: 1000,
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
payload := map[string]interface{}{
"max_devices": tt.maxDevices,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", ts.URL+"/api/auth/users/"+userID.String()+"/max-devices", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+adminToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tt.expectedStatus, resp.StatusCode, "expected validation error")
})
}
}
// TestUpdateUserMaxDevicesAuth tests authentication requirements
func TestUpdateUserMaxDevicesAuth(t *testing.T) {
ts, db, _ := setupTestServer(t)
defer ts.Close()
// Create admin user
adminToken := loginTestUser(t, ts, db)
createAdminUser(t, ts, adminToken)
adminUserID := getTestUserID(t, db)
adminToken = getAdminToken(t, ts, adminUserID)
// Create regular user
userID := createTestUserForMaxDevices(t, ts, adminToken)
t.Run("No authorization", func(t *testing.T) {
payload := map[string]interface{}{
"max_devices": 10,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", ts.URL+"/api/auth/users/"+userID.String()+"/max-devices", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("Non-admin user", func(t *testing.T) {
// Create another regular user and get their token
_ = createTestUserForMaxDevices(t, ts, adminToken)
// Login as the maxdevices user (who is a regular user, not admin)
regularToken := loginTestUserByCredentials(t, ts, "maxdevices@example.com", "Test@Pass123!")
require.NotEmpty(t, regularToken, "Failed to login as regular user")
payload := map[string]interface{}{
"max_devices": 10,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", ts.URL+"/api/auth/users/"+userID.String()+"/max-devices", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+regularToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
}
// TestUpdateUserMaxDevicesNonExistentUser tests with non-existent user ID
func TestUpdateUserMaxDevicesNonExistentUser(t *testing.T) {
ts, db, _ := setupTestServer(t)
defer ts.Close()
// Create admin user
adminToken := loginTestUser(t, ts, db)
createAdminUser(t, ts, adminToken)
adminUserID := getTestUserID(t, db)
adminToken = getAdminToken(t, ts, adminUserID)
// Use a non-existent user ID
nonExistentUserID := uuid.New()
payload := map[string]interface{}{
"max_devices": 10,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", ts.URL+"/api/auth/users/"+nonExistentUserID.String()+"/max-devices", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+adminToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Should return 500 or 404 depending on implementation
assert.True(t, resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusNotFound)
}
// TestUpdateUserMaxDevicesMissingUserID tests with missing user ID in URL
func TestUpdateUserMaxDevicesMissingUserID(t *testing.T) {
ts, db, _ := setupTestServer(t)
defer ts.Close()
// Create admin user
adminToken := loginTestUser(t, ts, db)
createAdminUser(t, ts, adminToken)
adminUserID := getTestUserID(t, db)
adminToken = getAdminToken(t, ts, adminUserID)
payload := map[string]interface{}{
"max_devices": 10,
}
jsonData, _ := json.Marshal(payload)
// Missing user ID in URL
req, _ := http.NewRequest("PUT", ts.URL+"/api/auth/users//max-devices", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+adminToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
// TestListUsersIncludesMaxDevices tests that List Users returns max_devices field
func TestListUsersIncludesMaxDevices(t *testing.T) {
ts, db, _ := setupTestServer(t)
defer ts.Close()
// Create admin user
adminToken := loginTestUser(t, ts, db)
createAdminUser(t, ts, adminToken)
adminUserID := getTestUserID(t, db)
adminToken = getAdminToken(t, ts, adminUserID)
req, _ := http.NewRequest("GET", ts.URL+"/api/auth/users", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var users []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&users)
// Verify max_devices field is present in response
if len(users) > 0 {
_, hasMaxDevices := users[0]["max_devices"]
assert.True(t, hasMaxDevices, "max_devices field should be present in user list")
}
}
// Helper function to create admin user
func createAdminUser(t *testing.T, ts *httptest.Server, token string) {
createUserPayload := map[string]interface{}{
"email": "admin@example.com",
"username": "adminuser",
"password": "Admin@Pass123!",
"first_name": "Admin",
"last_name": "User",
"role": "admin",
}
jsonData, _ := json.Marshal(createUserPayload)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/register", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
resp, _ := client.Do(req)
resp.Body.Close()
}
// Helper function to create test user for max devices tests
func createTestUserForMaxDevices(t *testing.T, ts *httptest.Server, adminToken string) uuid.UUID {
createUserPayload := map[string]interface{}{
"email": "maxdevices@example.com",
"username": "maxdevicesuser",
"password": "Test@Pass123!",
"first_name": "Test",
"last_name": "User",
}
jsonData, _ := json.Marshal(createUserPayload)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/register", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+adminToken)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check if user creation succeeded or already exists (409 Conflict)
if resp.StatusCode == http.StatusConflict {
// User already exists, try to login to get their access token
t.Logf("User maxdevices@example.com already exists, logging in to get ID")
loginPayload := map[string]interface{}{
"login": "maxdevices@example.com",
"password": "Test@Pass123!",
}
loginData, _ := json.Marshal(loginPayload)
loginReq, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(loginData))
loginReq.Header.Set("Content-Type", "application/json")
loginResp, err := client.Do(loginReq)
require.NoError(t, err)
defer loginResp.Body.Close()
var loginResult map[string]interface{}
json.NewDecoder(loginResp.Body).Decode(&loginResult)
// Extract user_id from JWT or response
// The access_token contains the user ID in the JWT claims
if accessToken, ok := loginResult["access_token"].(string); ok {
// Simple JWT parsing to get user_id
// JWT format: header.payload.signature
parts := strings.Split(accessToken, ".")
if len(parts) >= 2 {
// Decode payload (base64url)
payload := parts[1]
// Add padding if needed
for len(payload)%4 != 0 {
payload += "="
}
// Decode base64
decodedBytes, err := base64.StdEncoding.DecodeString(payload)
if err == nil {
var claims map[string]interface{}
if err := json.Unmarshal(decodedBytes, &claims); err == nil {
t.Logf("JWT claims: %+v", claims)
if userIDStr, ok := claims["user_id"].(string); ok {
userID, _ := uuid.Parse(userIDStr)
t.Logf("Extracted userID from JWT: %s", userID)
return userID
} else {
t.Logf("user_id not found in JWT claims")
}
} else {
t.Logf("Failed to unmarshal JWT claims: %v", err)
}
} else {
t.Logf("Failed to decode base64: %v", err)
}
} else {
t.Logf("JWT doesn't have enough parts: %d", len(parts))
}
} else {
t.Logf("access_token not found in login result")
}
// If JWT parsing fails, return empty UUID
return uuid.UUID{}
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
// Check if user creation was successful
if result["user"] == nil {
// User creation failed for another reason
t.Logf("User creation failed, response: %+v", result)
return uuid.UUID{}
}
userIDStr := result["user"].(map[string]interface{})["id"].(string)
userID, _ := uuid.Parse(userIDStr)
return userID
}
// Helper function to get admin token
func getAdminToken(t *testing.T, ts *httptest.Server, userID uuid.UUID) string {
// First, try to login as the admin user (testuser who was created with admin role)
loginPayload := map[string]interface{}{
"login": "testuser@example.com",
"password": "Test@Pass123!",
}
jsonData, _ := json.Marshal(loginPayload)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
// Safe type assertion with check
if accessToken, ok := result["access_token"].(string); ok {
return accessToken
}
// Handle error case - if login failed, return empty string
return ""
}
// Helper function to login user by credentials
func loginTestUserByCredentials(t *testing.T, ts *httptest.Server, email, password string) string {
loginPayload := map[string]interface{}{
"login": email,
"password": password,
}
jsonData, _ := json.Marshal(loginPayload)
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
// Safe type assertion with check
if accessToken, ok := result["access_token"].(string); ok {
return accessToken
}
// Login failed - return empty string
return ""
}