From 83211499571ad60d370165de91a6b2abab0fe020 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 13 Feb 2026 12:12:17 -0500 Subject: [PATCH] test: add Bruno API test collections for device authentication - Device token regeneration tests (success, forbidden, not found, unauthorized) - OPDS authentication tests (Bearer token, query token) - Kobo sync tests with token authentication - Test various authentication methods and error cases --- bruno/devices/regenerate-token-forbidden.bru | 26 +++++++ bruno/devices/regenerate-token-notfound.bru | 26 +++++++ .../devices/regenerate-token-unauthorized.bru | 24 ++++++ bruno/devices/regenerate-token.bru | 73 +++++++++++++++++++ bruno/opds/Download Book - Query Token.bru | 39 ++++++++++ bruno/opds/Get Device Catalog - Bearer.bru | 36 +++++++++ .../opds/Get Device Catalog - Query Token.bru | 37 ++++++++++ .../get-initialization-url-token.bru | 40 ++++++++++ bruno/sync-kobo/get-library-url-token.bru | 38 ++++++++++ bruno/sync-kobo/sync-bookmark-url-token.bru | 57 +++++++++++++++ bruno/sync-kobo/sync-markup-url-token.bru | 62 ++++++++++++++++ cmd/server/tests/kobo_test.go | 2 +- internal/database/querier.go | 1 + internal/database/queries.sql.go | 36 +++++++++ internal/database/queries/queries.sql | 8 ++ 15 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 bruno/devices/regenerate-token-forbidden.bru create mode 100644 bruno/devices/regenerate-token-notfound.bru create mode 100644 bruno/devices/regenerate-token-unauthorized.bru create mode 100644 bruno/devices/regenerate-token.bru create mode 100644 bruno/opds/Download Book - Query Token.bru create mode 100644 bruno/opds/Get Device Catalog - Bearer.bru create mode 100644 bruno/opds/Get Device Catalog - Query Token.bru create mode 100644 bruno/sync-kobo/get-initialization-url-token.bru create mode 100644 bruno/sync-kobo/get-library-url-token.bru create mode 100644 bruno/sync-kobo/sync-bookmark-url-token.bru create mode 100644 bruno/sync-kobo/sync-markup-url-token.bru diff --git a/bruno/devices/regenerate-token-forbidden.bru b/bruno/devices/regenerate-token-forbidden.bru new file mode 100644 index 0000000..cc33f00 --- /dev/null +++ b/bruno/devices/regenerate-token-forbidden.bru @@ -0,0 +1,26 @@ +meta { + name: Regenerate Device Token - Forbidden + type: http + seq: 3 +} + +put { + url: {{base_url}}/api/devices/{{other_device_id}}/regenerate-token + body: none + auth: inherit +} + +docs { + ## Regenerate Device Token - Forbidden + + Tests that users cannot regenerate tokens for devices belonging to other users. + + **Expected Behavior:** Returns 403 Forbidden when trying to regenerate token for another user's device + + **Status Codes:** + - 403: Forbidden (device belongs to different user) + + **Use Case:** Verify authorization - users can only manage their own devices + + **Setup:** Use Bearer token from user A, try to regenerate token for user B's device +} diff --git a/bruno/devices/regenerate-token-notfound.bru b/bruno/devices/regenerate-token-notfound.bru new file mode 100644 index 0000000..59d3a76 --- /dev/null +++ b/bruno/devices/regenerate-token-notfound.bru @@ -0,0 +1,26 @@ +meta { + name: Regenerate Device Token - Not Found + type: http + seq: 4 +} + +put { + url: {{base_url}}/api/devices/00000000-0000-0000-0000-000000000000/regenerate-token + body: none + auth: bearer +} + +docs { + ## Regenerate Device Token - Not Found + + Tests that token regeneration returns 404 for non-existent devices. + + **Expected Behavior:** Returns 404 Not Found when device UUID doesn't exist + + **Status Codes:** + - 404: Device not found + + **Use Case:** Verify proper error handling for invalid device IDs + + **Setup:** Use all-zero UUID (guaranteed to not exist in database) +} diff --git a/bruno/devices/regenerate-token-unauthorized.bru b/bruno/devices/regenerate-token-unauthorized.bru new file mode 100644 index 0000000..8680bd3 --- /dev/null +++ b/bruno/devices/regenerate-token-unauthorized.bru @@ -0,0 +1,24 @@ +meta { + name: Regenerate Device Token - Unauthorized + type: http + seq: 2 +} + +put { + url: {{base_url}}/api/devices/{{device_id}}/regenerate-token + body: none + auth: none +} + +docs { + ## Regenerate Device Token - Unauthorized + + Tests that token regeneration requires authentication. + + **Expected Behavior:** Returns 401 Unauthorized when no Bearer token is provided + + **Status Codes:** + - 401: Unauthorized (missing or invalid token) + + **Use Case:** Verify authentication is required for token regeneration +} diff --git a/bruno/devices/regenerate-token.bru b/bruno/devices/regenerate-token.bru new file mode 100644 index 0000000..9e79fe2 --- /dev/null +++ b/bruno/devices/regenerate-token.bru @@ -0,0 +1,73 @@ +meta { + name: Regenerate Device Token + type: http + seq: 1 +} + +put { + url: {{base_url}}/api/devices/{{device_id}}/regenerate-token + body: none + auth: inherit +} + +docs { + ## Regenerate Device Token + + Regenerates auth token for a device, invalidating old token immediately. + + **Method:** PUT + + **Endpoint:** /api/devices/{device_id}/regenerate-token + + **Authentication:** Bearer token (JWT) + + **Path Parameters:** + - `device_id` (string): Device UUID + + **Response:** + - `message` (string): Success message + - `auth_token` (string): New auth token + - `device` (object): Updated device details + - `sync_urls` (object): Device-specific sync URLs with new token + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 403: Forbidden (device belongs to different user) + - 404: Device not found + - 500: Internal server error + + **Important Notes:** + - Old token stops working immediately + - Device must be updated with new token to resume syncing + - No data loss - device ID remains same + + **Example Response:** + ```json + { + "message": "Token regenerated successfully", + "auth_token": "dev_abc123...", + "device": { + "id": "uuid-here", + "device_name": "My Kobo Clara", + "device_type": "kobo", + "sync_enabled": true, + "auto_sync": true, + "sync_frequency_minutes": 5, + "created_at": "2026-02-12T10:00:00Z", + "device_metadata": "{...}" + }, + "sync_urls": { + "sync_url": "http://localhost:8765/api/sync/kobo/dev_new_token", + "markup": "http://localhost:8765/api/sync/kobo/dev_new_token/markup", + "bookmark": "http://localhost:8765/api/sync/kobo/dev_new_token/bookmark", + "init": "http://localhost:8765/api/sync/kobo/dev_new_token/v1/initialization" + } + } + ``` + + **Important Notes:** + - Old token stops working immediately + - Device must be updated with new token to resume syncing + - No data loss - device ID remains same +} diff --git a/bruno/opds/Download Book - Query Token.bru b/bruno/opds/Download Book - Query Token.bru new file mode 100644 index 0000000..f09c0ba --- /dev/null +++ b/bruno/opds/Download Book - Query Token.bru @@ -0,0 +1,39 @@ +meta { + name: Download Book - Query Token + type: http + seq: 3 +} + +get { + url: {{opds_base_url}}/opds/devices/{{device_id}}/download/{{book_id}}?token={{device_token}} +} + +docs { + ## Download Book - Query Token + + Tests OPDS book download using query parameter authentication. + + **Method:** GET + + **Endpoint:** /opds/devices/{device_id}/download/{book_id}?token={device_token} + + **Authentication:** Query parameter (for Kobo devices) + + **Path Parameters:** + - `device_id` (string): Device UUID + - `book_id` (string): Book UUID + - `token` (string): Device auth_token + + **Response:** Book file (EPUB) + + **Status Codes:** + - 200: Success (book file) + - 401: Unauthorized (missing or invalid token) + - 403: Device sync disabled + - 404: Device not found + - 500: Book not found + + **Important:** Kobo devices use query parameter for OPDS downloads + + **Use Case:** Kobo devices downloading books from OPDS +} diff --git a/bruno/opds/Get Device Catalog - Bearer.bru b/bruno/opds/Get Device Catalog - Bearer.bru new file mode 100644 index 0000000..a01b27c --- /dev/null +++ b/bruno/opds/Get Device Catalog - Bearer.bru @@ -0,0 +1,36 @@ +meta { + name: Get Device Catalog - Bearer + type: http + seq: 1 +} + +get { + url: {{opds_base_url}}/opds/devices/{{device_id}}/catalog +} + +docs { + ## Get Device OPDS Catalog - Bearer + + Tests OPDS device catalog retrieval using Bearer token authentication. + + **Method:** GET + + **Endpoint:** /opds/devices/{device_id}/catalog + + **Authentication:** Bearer token (for KOReader, API clients, other devices) + + **Path Parameters:** + - `device_id` (string): Device UUID + + **Response:** OPDS Atom feed catalog + + **Status Codes:** + - 200: Success (OPDS catalog) + - 401: Unauthorized (missing or invalid token) + - 403: Device sync disabled + - 404: Device not found + + **Important:** Bearer token in Authorization header + + **Use Case:** KOReader devices, API clients, or any device configured with Bearer token +} diff --git a/bruno/opds/Get Device Catalog - Query Token.bru b/bruno/opds/Get Device Catalog - Query Token.bru new file mode 100644 index 0000000..f971554 --- /dev/null +++ b/bruno/opds/Get Device Catalog - Query Token.bru @@ -0,0 +1,37 @@ +meta { + name: Get Device Catalog - Query Token + type: http + seq: 2 +} + +get { + url: {{opds_base_url}}/opds/devices/{{device_id}}/catalog?token={{device_token}} +} + +docs { + ## Get Device OPDS Catalog - Query Token + + Tests OPDS device catalog retrieval using query parameter authentication. + + **Method:** GET + + **Endpoint:** /opds/devices/{device_id}/catalog?token={device_token} + + **Authentication:** Query parameter (for Kobo devices) + + **Path Parameters:** + - `device_id` (string): Device UUID + - `token` (string): Device auth_token + + **Response:** OPDS Atom feed catalog + + **Status Codes:** + - 200: Success (OPDS catalog) + - 401: Unauthorized (missing or invalid token) + - 403: Device sync disabled + - 404: Device not found + + **Important:** Kobo devices use query parameter for OPDS catalog + + **Use Case:** Kobo devices accessing OPDS catalog +} diff --git a/bruno/sync-kobo/get-initialization-url-token.bru b/bruno/sync-kobo/get-initialization-url-token.bru new file mode 100644 index 0000000..e399cfd --- /dev/null +++ b/bruno/sync-kobo/get-initialization-url-token.bru @@ -0,0 +1,40 @@ +meta { + name: Kobo Initialization - URL Path Token + type: http + seq: 4 +} + +get { + url: {{base_url}}/sync/kobo/{{kobo_device_token}}/v1/initialization + body: none + auth: none +} + +docs { + ## Kobo Initialization - URL Path Token + + Returns initialization data for Kobo device using token in URL path. + + **Method:** GET + + **Endpoint:** /sync/kobo/{kobo_device_token}/v1/initialization + + **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) + + **Path Parameters:** + - `kobo_device_token` (string): Device auth token from devices.auth_token + + **Response:** Initialization configuration and settings + + **Status Codes:** + - 200: Success (initialization data) + - 401: Unauthorized + - 403: Device sync disabled + - 404: Device not found + + **Important:** Kobo devices use URL path token exclusively, never Bearer header + + **Use Case:** Kobo devices initializing sync connection via stock firmware + + **Note:** This endpoint is called when Kobo first connects to sync server +} diff --git a/bruno/sync-kobo/get-library-url-token.bru b/bruno/sync-kobo/get-library-url-token.bru new file mode 100644 index 0000000..c124cc4 --- /dev/null +++ b/bruno/sync-kobo/get-library-url-token.bru @@ -0,0 +1,38 @@ +meta { + name: Get Library - URL Path Token + type: http + seq: 3 +} + +get { + url: {{base_url}}/sync/kobo/{{kobo_device_token}}/library + body: none + auth: none +} + +docs { + ## Get Kobo Library - URL Path Token + + Retrieves library metadata for Kobo device using token in URL path. + + **Method:** GET + + **Endpoint:** /sync/kobo/{kobo_device_token}/library + + **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) + + **Path Parameters:** + - `kobo_device_token` (string): Device auth token from devices.auth_token + + **Response:** Library metadata with book list + + **Status Codes:** + - 200: Success (library metadata) + - 401: Unauthorized + - 403: Device sync disabled + - 404: Device not found + + **Important:** Kobo devices use URL path token exclusively, never Bearer header + + **Use Case:** Kobo devices retrieving library information via stock firmware +} diff --git a/bruno/sync-kobo/sync-bookmark-url-token.bru b/bruno/sync-kobo/sync-bookmark-url-token.bru new file mode 100644 index 0000000..2ad5f09 --- /dev/null +++ b/bruno/sync-kobo/sync-bookmark-url-token.bru @@ -0,0 +1,57 @@ +meta { + name: Sync Bookmark - URL Path Token + type: http + seq: 2 +} + +post { + url: {{base_url}}/sync/kobo/{{kobo_device_token}}/bookmark + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "ContentId": "book-uuid", + "BookmarkText": "Highlighted text", + "BookmarkType": "annotation", + "BookmarkTitle": "Chapter 3" + } +} + +docs { + ## Sync Bookmark - URL Path Token + + Synchronizes bookmarks and annotations from Kobo device using token in URL path. + + **Method:** POST + + **Endpoint:** /sync/kobo/{kobo_device_token}/bookmark + + **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) + + **Path Parameters:** + - `kobo_device_token` (string): Device auth token from devices.auth_token + + **Request Body:** + - `ContentId` (string): Book UUID + - `BookmarkText` (string): Highlighted or annotated text + - `BookmarkType` (string): Type (annotation, bookmark, note) + - `BookmarkTitle` (string): Title for the bookmark + + **Response:** Success message + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 403: Device sync disabled + - 500: Internal server error + + **Important:** Kobo devices use URL path token exclusively, never Bearer header + + **Use Case:** Kobo devices syncing bookmarks and highlights via stock firmware +} diff --git a/bruno/sync-kobo/sync-markup-url-token.bru b/bruno/sync-kobo/sync-markup-url-token.bru new file mode 100644 index 0000000..6af2762 --- /dev/null +++ b/bruno/sync-kobo/sync-markup-url-token.bru @@ -0,0 +1,62 @@ +meta { + name: Sync Markup - URL Path Token + type: http + seq: 1 +} + +post { + url: {{base_url}}/sync/kobo/{{kobo_device_token}}/markup + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "ReadingSync": [ + { + "ContentId": "book-uuid", + "PercentRead": 45.6, + "EntitlementId": "entitlement-id", + "RemainingTimeMinutes": 120, + "FirstReadTime": "2026-01-25T10:00:00Z", + "LastModified": "2026-01-30T20:00:00Z" + } + ], + "BookmarkSync": [] + } +} + +docs { + ## Sync Reading Progress - URL Path Token + + Synchronizes reading progress from Kobo device using token in URL path. + + **Method:** POST + + **Endpoint:** /sync/kobo/{kobo_device_token}/markup + + **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) + + **Path Parameters:** + - `kobo_device_token` (string): Device auth token from devices.auth_token + + **Request Body:** + - `ReadingSync` (array): Reading progress data + - `BookmarkSync` (array): Bookmarks and annotations + + **Response:** Success message + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 403: Device sync disabled + - 500: Internal server error + + **Important:** Kobo firmware requires token in URL path, cannot use Bearer header + + **Use Case:** Kobo devices syncing reading progress via stock firmware +} diff --git a/cmd/server/tests/kobo_test.go b/cmd/server/tests/kobo_test.go index e3ad5d5..7f3fa3d 100644 --- a/cmd/server/tests/kobo_test.go +++ b/cmd/server/tests/kobo_test.go @@ -1,4 +1,4 @@ -package main +package bookhoard/cmd/server/tests import ( "bookhoard/internal/handlers" diff --git a/internal/database/querier.go b/internal/database/querier.go index cf8d644..dd39b7c 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -271,6 +271,7 @@ type Querier interface { // Update collection UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error) UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Devices, error) + UpdateDeviceAuthToken(ctx context.Context, arg UpdateDeviceAuthTokenParams) (Devices, error) // Update device catalog availability UpdateDeviceCatalogAvailability(ctx context.Context, arg UpdateDeviceCatalogAvailabilityParams) error // Update device file alias diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index c5c234f..3dc63e3 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -6573,6 +6573,42 @@ func (q *Queries) UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Dev return i, err } +const UpdateDeviceAuthToken = `-- name: UpdateDeviceAuthToken :one +UPDATE devices +SET + auth_token = $2, + updated_at = NOW() +WHERE id = $1 +RETURNING id, user_id, device_name, device_type, device_identifier, auth_token, last_sync, last_seen, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata, created_at, updated_at +` + +type UpdateDeviceAuthTokenParams struct { + ID pgtype.UUID `db:"id" json:"id"` + AuthToken string `db:"auth_token" json:"auth_token"` +} + +func (q *Queries) UpdateDeviceAuthToken(ctx context.Context, arg UpdateDeviceAuthTokenParams) (Devices, error) { + row := q.db.QueryRow(ctx, UpdateDeviceAuthToken, arg.ID, arg.AuthToken) + var i Devices + err := row.Scan( + &i.ID, + &i.UserID, + &i.DeviceName, + &i.DeviceType, + &i.DeviceIdentifier, + &i.AuthToken, + &i.LastSync, + &i.LastSeen, + &i.SyncEnabled, + &i.AutoSync, + &i.SyncFrequencyMinutes, + &i.DeviceMetadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const UpdateDeviceCatalogAvailability = `-- name: UpdateDeviceCatalogAvailability :exec UPDATE device_catalogs SET available = $2 diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index ba45e36..d3a295b 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -769,6 +769,14 @@ SET WHERE id = $1 RETURNING *; +-- name: UpdateDeviceAuthToken :one +UPDATE devices +SET + auth_token = $2, + updated_at = NOW() +WHERE id = $1 +RETURNING *; + -- name: RevokeDevice :exec UPDATE devices SET