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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
package main
|
||||
package bookhoard/cmd/server/tests
|
||||
|
||||
import (
|
||||
"bookhoard/internal/handlers"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user