package main import ( "bookhoard/internal/database" "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" ) func TestDeviceRegistrationFlow(t *testing.T) { setup := setupTestServer(t) // Step 1: Initiate device registration regRequest := map[string]interface{}{ "device_name": "Test Kindle Paperwhite", "device_type": "koreader", "device_identifier": "kindle-test-hw-id-12345", } regBody, _ := json.Marshal(regRequest) req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration") var regResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), ®Response) registrationID, ok := regResponse["registration_id"].(string) assert.True(t, ok, "Should have registration_id") assert.NotEmpty(t, registrationID, "Registration ID should not be empty") authURL, ok := regResponse["auth_url"].(string) assert.True(t, ok, "Should have auth_url") assert.NotEmpty(t, authURL, "Auth URL should not be empty") // Step 2: Check registration status (should be pending initially) statusRequest := map[string]interface{}{ "registration_id": registrationID, } statusBody, _ := json.Marshal(statusRequest) req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody)) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should check registration status") var statusResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &statusResponse) status, ok := statusResponse["status"].(string) assert.True(t, ok, "Should have status") assert.Equal(t, "pending", status, "Should be pending initially") // Step 3: Login as user to approve device loginRequest := map[string]interface{}{ "login": "testuser@example.com", "password": "Test@Pass123!", } loginBody, _ := json.Marshal(loginRequest) req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginBody)) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should login successfully") var loginResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &loginResponse) token, ok := loginResponse["access_token"].(string) assert.True(t, ok, "Should have access_token") // Step 4: Approve the device req = httptest.NewRequest("GET", fmt.Sprintf("/api/devices/approve/%s", registrationID), nil) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should approve device") // Step 5: Check registration status again (should be approved now) req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody)) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should check registration status after approval") var approvedStatus map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &approvedStatus) status, ok = approvedStatus["status"].(string) assert.True(t, ok, "Should have status") assert.Equal(t, "approved", status, "Should be approved after user approval") authToken, ok := approvedStatus["auth_token"].(string) assert.True(t, ok, "Should have auth_token after approval") assert.NotEmpty(t, authToken, "Auth token should not be empty") } func TestListDevices(t *testing.T) { setup := setupDeviceTest(t) defer setup.Server.Close() // Create a device using the setup helper _ = setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") // List devices req := httptest.NewRequest("GET", "/api/devices", nil) req.Header.Set("Authorization", "Bearer "+setup.UserToken) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should list devices") var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) devices, ok := response["devices"].([]interface{}) assert.True(t, ok, "Should have devices array") assert.GreaterOrEqual(t, len(devices), 1, "Should have at least one device") firstDevice := devices[0].(map[string]interface{}) deviceName, ok := firstDevice["device_name"].(string) assert.True(t, ok, "Should have device_name") assert.Equal(t, "Test Device", deviceName, "Should match created device name") } func TestUpdateDevice(t *testing.T) { setup := setupDeviceTest(t) defer setup.Server.Close() // Create a device using the setup helper device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") // Update device updateRequest := map[string]interface{}{ "device_name": "Updated Device Name", "sync_enabled": false, "sync_frequency_minutes": int32(10), } updateBody, _ := json.Marshal(updateRequest) req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s", device.ID.String()), bytes.NewReader(updateBody)) req.Header.Set("Authorization", "Bearer "+setup.UserToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should update device") var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) assert.True(t, response["device_updated"].(bool), "Should confirm device updated") updatedDevice := response["device"].(map[string]interface{}) assert.Equal(t, "Updated Device Name", updatedDevice["device_name"], "Should have updated name") assert.Equal(t, false, updatedDevice["sync_enabled"], "Should be disabled") assert.Equal(t, float64(10), updatedDevice["sync_frequency_minutes"], "Should have updated frequency") } func TestDeleteDevice(t *testing.T) { setup := setupDeviceTest(t) defer setup.Server.Close() // Create a device using the setup helper device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") // Delete device req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/devices/%s", device.ID.String()), nil) req.Header.Set("Authorization", "Bearer "+setup.UserToken) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete device") // Verify device is deleted pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true} _, err := setup.DB.GetDevice(context.Background(), pgDeviceID) assert.Error(t, err, "Device should be deleted") } func TestDeviceAuthentication(t *testing.T) { setup := setupTestServer(t) // Create a device directly in the database userID := getTestUserID(t, setup.DB) deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) _, err := setup.DB.CreateDevice(context.Background(), database.CreateDeviceParams{ UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, DeviceName: "Test Device", DeviceType: "koreader", DeviceIdentifier: "test-device-123", AuthToken: deviceToken, SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, AutoSync: pgtype.Bool{Bool: true, Valid: true}, SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, DeviceMetadata: []byte("{}"), }) assert.NoError(t, err, "Should create device") // Test device authentication req := httptest.NewRequest("GET", "/api/devices", nil) req.Header.Set("Authorization", "Bearer "+deviceToken) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) // This should fail because device auth middleware is not applied to /api/devices // Device auth is for sync endpoints only assert.Equal(t, http.StatusUnauthorized, rec.Code, "Should require user auth for device management") } func TestListPendingRegistrations(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) req := httptest.NewRequest("GET", "/api/devices/pending", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should list pending registrations") var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) pending, ok := response["pending_registrations"].([]interface{}) assert.True(t, ok, "Should have pending_registrations array") assert.NotNil(t, pending, "Pending registrations should not be nil") } func TestApproveDeviceRegistration(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) regRequest := map[string]interface{}{ "device_name": "Test Device for Approval", "device_type": "koreader", "device_identifier": "test-approval-12345", } regBody, _ := json.Marshal(regRequest) req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration") var regResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), ®Response) registrationID, ok := regResponse["registration_id"].(string) assert.True(t, ok, "Should have registration_id") req = httptest.NewRequest("GET", fmt.Sprintf("/api/devices/approve/%s", registrationID), nil) req.Header.Set("Authorization", "Bearer "+token) rec = httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should approve device registration") var approveResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &approveResponse) assert.True(t, approveResponse["approved"].(bool), "Should confirm approval") } func TestRejectDeviceRegistration(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) regRequest := map[string]interface{}{ "device_name": "Test Device for Rejection", "device_type": "koreader", "device_identifier": "test-rejection-12345", } regBody, _ := json.Marshal(regRequest) req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration") var regResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), ®Response) registrationID, ok := regResponse["registration_id"].(string) assert.True(t, ok, "Should have registration_id") req = httptest.NewRequest("POST", fmt.Sprintf("/api/devices/reject/%s", registrationID), nil) req.Header.Set("Authorization", "Bearer "+token) rec = httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should reject device registration") var rejectResponse map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &rejectResponse) assert.Equal(t, "device registration rejected", rejectResponse["message"], "Should confirm rejection message") }