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 (
"bookhoard/internal/handlers"
+1
View File
@@ -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
+36
View File
@@ -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
+8
View File
@@ -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