diff --git a/cmd/server/tests/device_test.go b/cmd/server/tests/device_test.go index 0aa82ed..095434c 100644 --- a/cmd/server/tests/device_test.go +++ b/cmd/server/tests/device_test.go @@ -322,3 +322,207 @@ func TestRejectDeviceRegistration(t *testing.T) { assert.Equal(t, "device registration rejected", rejectResponse["message"], "Should confirm rejection message") } + +func TestRegenerateDeviceToken_Success(t *testing.T) { + setup := setupDeviceTest(t) + defer setup.Server.Close() + + // Create a device + device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") + oldToken := device.AuthToken + + // Regenerate token + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", 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.StatusOK, rec.Code, "Should regenerate token") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + + assert.True(t, response["message"].(string) != "", "Should have success message") + + newToken, ok := response["auth_token"].(string) + assert.True(t, ok, "Should have auth_token") + assert.NotEmpty(t, newToken, "New token should not be empty") + assert.NotEqual(t, oldToken, newToken, "New token should be different from old token") + + // Verify device info is returned + deviceInfo, ok := response["device"].(map[string]interface{}) + assert.True(t, ok, "Should have device info") + assert.Equal(t, "Test Device", deviceInfo["device_name"], "Should return device name") + + // Verify sync URLs are returned + syncURLs, ok := response["sync_urls"].(map[string]interface{}) + assert.True(t, ok, "Should have sync_urls") + assert.Contains(t, syncURLs, "progress", "Should have progress URL") + assert.Contains(t, syncURLs, "metadata", "Should have metadata URL") +} + +func TestRegenerateDeviceToken_OldTokenInvalidated(t *testing.T) { + setup := setupDeviceTest(t) + defer setup.Server.Close() + + // Create a device + device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-invalidated") + + // Test old token works initially + req1 := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil) + req1.Header.Set("Authorization", "Bearer "+device.AuthToken) + req1.Header.Set("Content-Type", "application/json") + rec1 := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec1, req1) + // May fail for other reasons (no data), but should not be unauthorized + assert.NotEqual(t, http.StatusUnauthorized, rec1.Code, "Old token should work initially") + + // Regenerate token + req2 := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device.ID.String()), nil) + req2.Header.Set("Authorization", "Bearer "+setup.UserToken) + rec2 := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec2, req2) + assert.Equal(t, http.StatusOK, rec2.Code, "Should regenerate token") + + var response map[string]interface{} + json.Unmarshal(rec2.Body.Bytes(), &response) + newToken := response["auth_token"].(string) + + // Test old token no longer works + req3 := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil) + req3.Header.Set("Authorization", "Bearer "+device.AuthToken) + req3.Header.Set("Content-Type", "application/json") + rec3 := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec3, req3) + assert.Equal(t, http.StatusUnauthorized, rec3.Code, "Old token should be invalid after regeneration") + + // Test new token works + req4 := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil) + req4.Header.Set("Authorization", "Bearer "+newToken) + req4.Header.Set("Content-Type", "application/json") + rec4 := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec4, req4) + // May fail for other reasons, but should not be unauthorized + assert.NotEqual(t, http.StatusUnauthorized, rec4.Code, "New token should work") +} + +func TestRegenerateDeviceToken_Unauthorized(t *testing.T) { + setup := setupDeviceTest(t) + defer setup.Server.Close() + + device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-unauth") + + // Try to regenerate without JWT token + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device.ID.String()), nil) + rec := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusUnauthorized, rec.Code, "Should require authentication") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + assert.Contains(t, response, "error", "Should return error message") +} + +func TestRegenerateDeviceToken_Forbidden(t *testing.T) { + setup := setupDeviceTest(t) + defer setup.Server.Close() + + // Create device for user 1 + device1 := setup.CreateDevice(t, "User1 Device", "koreader", "user1-device") + + // Create a second user with different credentials + ctx := context.Background() + passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq" + _, err := setup.DB.CreateUser(ctx, database.CreateUserParams{ + Email: "differentuser@example.com", + Username: "differentuser", + PasswordHash: passwordHash, + FirstName: pgtype.Text{String: "Different", Valid: true}, + LastName: pgtype.Text{String: "User", Valid: true}, + Role: "user", + }) + assert.NoError(t, err) + + // Login as user 2 + loginRequest := map[string]interface{}{ + "login": "differentuser@example.com", + "password": "Test@Pass123!", + } + loginBody, _ := json.Marshal(loginRequest) + loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginBody)) + loginReq.Header.Set("Content-Type", "application/json") + loginRec := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(loginRec, loginReq) + + var loginResponse map[string]interface{} + json.Unmarshal(loginRec.Body.Bytes(), &loginResponse) + user2Token := loginResponse["access_token"].(string) + + // Try to regenerate user 1's device with user 2's token + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", device1.ID.String()), nil) + req.Header.Set("Authorization", "Bearer "+user2Token) + rec := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusForbidden, rec.Code, "Should forbid access to other user's device") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + assert.Contains(t, response, "error", "Should return error message") +} + +func TestRegenerateDeviceToken_NotFound(t *testing.T) { + setup := setupDeviceTest(t) + defer setup.Server.Close() + + fakeDeviceID, _ := uuid.NewUUID() + + // Try to regenerate non-existent device + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", fakeDeviceID.String()), nil) + req.Header.Set("Authorization", "Bearer "+setup.UserToken) + rec := httptest.NewRecorder() + setup.Server.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code, "Should return not found") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + assert.Contains(t, response, "error", "Should return error message") +} + +func TestRegenerateDeviceToken_KoboDevice(t *testing.T) { + setup := setupDeviceTest(t) + defer setup.Server.Close() + + // Create a Kobo device + koboDevice := setup.CreateDevice(t, "Test Kobo", "kobo", "test-kobo-regen") + oldToken := koboDevice.AuthToken + + // Regenerate token + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s/regenerate-token", koboDevice.ID.String()), 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 regenerate token") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + + newToken := response["auth_token"].(string) + assert.NotEqual(t, oldToken, newToken, "New token should be different") + + // Verify Kobo sync URLs are returned + syncURLs, ok := response["sync_urls"].(map[string]interface{}) + assert.True(t, ok, "Should have sync_urls") + assert.Contains(t, syncURLs, "sync_url", "Should have sync_url") + assert.Contains(t, syncURLs, "markup", "Should have markup URL") + assert.Contains(t, syncURLs, "bookmark", "Should have bookmark URL") + assert.Contains(t, syncURLs, "init", "Should have init URL") + + // Verify URLs contain new token + syncURL := syncURLs["sync_url"].(string) + assert.Contains(t, syncURL, newToken, "Sync URL should contain new token") + assert.NotContains(t, syncURL, oldToken, "Sync URL should not contain old token") +}