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
This commit is contained in:
2026-02-13 12:12:17 -05:00
parent b55e5df251
commit 8321149957
15 changed files with 504 additions and 1 deletions
@@ -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
}
@@ -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)
}
@@ -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
}
+73
View File
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
+38
View File
@@ -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
}
@@ -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
}
+62
View File
@@ -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
}
+1 -1
View File
@@ -1,4 +1,4 @@
package main package bookhoard/cmd/server/tests
import ( import (
"bookhoard/internal/handlers" "bookhoard/internal/handlers"
+1
View File
@@ -271,6 +271,7 @@ type Querier interface {
// Update collection // Update collection
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error) UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error)
UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Devices, error) UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Devices, error)
UpdateDeviceAuthToken(ctx context.Context, arg UpdateDeviceAuthTokenParams) (Devices, error)
// Update device catalog availability // Update device catalog availability
UpdateDeviceCatalogAvailability(ctx context.Context, arg UpdateDeviceCatalogAvailabilityParams) error UpdateDeviceCatalogAvailability(ctx context.Context, arg UpdateDeviceCatalogAvailabilityParams) error
// Update device file alias // Update device file alias
+36
View File
@@ -6573,6 +6573,42 @@ func (q *Queries) UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Dev
return i, err 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 const UpdateDeviceCatalogAvailability = `-- name: UpdateDeviceCatalogAvailability :exec
UPDATE device_catalogs UPDATE device_catalogs
SET available = $2 SET available = $2
+8
View File
@@ -769,6 +769,14 @@ SET
WHERE id = $1 WHERE id = $1
RETURNING *; RETURNING *;
-- name: UpdateDeviceAuthToken :one
UPDATE devices
SET
auth_token = $2,
updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: RevokeDevice :exec -- name: RevokeDevice :exec
UPDATE devices UPDATE devices
SET SET