diff --git a/bruno/analytics/Get Device Usage.bru b/bruno/analytics/Get Device Usage.bru index a634015..fcacf5a 100644 --- a/bruno/analytics/Get Device Usage.bru +++ b/bruno/analytics/Get Device Usage.bru @@ -1,35 +1,88 @@ -{ - "meta": { - "name": "Get Device Usage", - "type": "http", - "event": [ +meta { + name: Get Device Usage + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/api/analytics/device-usage + body: none + auth: inherit +} + +tests { + test("status must be 200", function() { + expect(res.status).to.eql(200); + }); + + test("response has devices array", function() { + const body = JSON.parse(res.body); + expect(body).to.have.property("devices"); + expect(body.devices).to.be.an("array"); + }); + + test("devices have required fields", function() { + const body = JSON.parse(res.body); + if (body.devices.length > 0) { + expect(body.devices[0]).to.have.property("device_name"); + expect(body.devices[0]).to.have.property("device_type"); + expect(body.devices[0]).to.have.property("sync_count"); + } + }); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + Get usage statistics for all devices. + + **Endpoint**: GET /api/analytics/device-usage + **Auth**: Required (Bearer token) + + ## Response Fields + + | Field | Type | Description | + |-------|------|-------------| + | devices | array | List of device usage statistics | + | devices[].id | string | Device ID | + | devices[].device_name | string | Device name | + | devices[].device_type | string | Device type (kobo, kindle, koreader) | + | devices[].sync_count | int | Number of sync operations | + | devices[].last_sync | string | Last sync timestamp | + | devices[].total_reading_minutes | int | Total reading time on device | + | devices[].books_read | int | Number of books completed on device | + + ## Example Response + + ```json + { + "devices": [ { - "listen": "test", - "script": { - "exec": [ - "// Test device usage response", - "if (response.status === 200) {", - " tests['Device usage loaded'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has devices array'] = Array.isArray(body.devices);", - " if (body.devices.length > 0) {", - " tests['Device has device_name'] = body.devices[0].device_name !== undefined;", - " tests['Device has device_type'] = body.devices[0].device_type !== undefined;", - " tests['Device has sync_count'] = body.devices[0].sync_count !== undefined;", - " }", - "} else {", - " tests['Device usage failed'] = false;", - "}" - ] - } + "id": "789e4567-e89b-12d3-a456-426614174001", + "device_name": "My Kobo Clara", + "device_type": "kobo", + "sync_count": 45, + "last_sync": "2026-02-08T17:25:00Z", + "total_reading_minutes": 1250, + "books_read": 3 } ] - }, - "req": { - "url": "{{baseUrl}}/api/analytics/device-usage", - "method": "GET", - "headers": { - "Authorization": "Bearer {{authToken}}" - } } + ``` + + ## Error Responses + + | Code | Description | + |------|-------------| + | 401 | Unauthorized | + | 500 | Internal server error | + + ## Notes + + - Only shows devices registered to the authenticated user + - Devices are sorted by sync_count in descending order + - Includes both active and inactive devices } diff --git a/bruno/analytics/Get Popular Books.bru b/bruno/analytics/Get Popular Books.bru index eef5883..e58db5c 100644 --- a/bruno/analytics/Get Popular Books.bru +++ b/bruno/analytics/Get Popular Books.bru @@ -1,36 +1,99 @@ -{ - "meta": { - "name": "Get Popular Books", - "type": "http", - "event": [ +meta { + name: Get Popular Books + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/api/analytics/popular-books?limit=10 + body: none + auth: inherit +} + +tests { + test("status must be 200", function() { + expect(res.status).to.eql(200); + }); + + test("response has books array", function() { + const body = JSON.parse(res.body); + expect(body).to.have.property("books"); + expect(body.books).to.be.an("array"); + }); + + test("books have required fields", function() { + const body = JSON.parse(res.body); + if (body.books.length > 0) { + expect(body.books[0]).to.have.property("title"); + expect(body.books[0]).to.have.property("author"); + expect(body.books[0]).to.have.property("read_count"); + expect(body.books[0]).to.have.property("avg_completion"); + } + }); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + Get popular books sorted by read count. + + **Endpoint**: GET /api/analytics/popular-books + **Auth**: Required (Bearer token) + + ## Query Parameters + + | Parameter | Type | Required | Description | + |-----------|------|-----------|-------------| + | limit | int | No | Maximum number of books to return (default: 10) | + + ## Response Fields + + | Field | Type | Description | + |-------|------|-------------| + | books | array | List of popular books | + | books[].media_item_id | string | Book ID | + | books[].title | string | Book title | + | books[].author | string | Book author | + | books[].read_count | int | Number of times read | + | books[].avg_completion | float | Average completion rate (0-1) | + | books[].cover_url | string | Cover image URL | + + ## Example Request + + ``` + GET /api/analytics/popular-books?limit=10 + ``` + + ## Example Response + + ```json + { + "books": [ { - "listen": "test", - "script": { - "exec": [ - "// Test popular books response", - "if (response.status === 200) {", - " tests['Popular books loaded'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has books array'] = Array.isArray(body.books);", - " if (body.books.length > 0) {", - " tests['Book has title'] = body.books[0].title !== undefined;", - " tests['Book has author'] = body.books[0].author !== undefined;", - " tests['Book has read_count'] = body.books[0].read_count !== undefined;", - " tests['Book has avg_completion'] = body.books[0].avg_completion !== undefined;", - " }", - "} else {", - " tests['Popular books failed'] = false;", - "}" - ] - } + "media_item_id": "323e4567-e89b-12d3-a456-426614174002", + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "read_count": 5, + "avg_completion": 0.85, + "cover_url": "/api/books/323e4567-e89b-12d3-a456-426614174002/cover" } ] - }, - "req": { - "url": "{{baseUrl}}/api/analytics/popular-books?limit=10", - "method": "GET", - "headers": { - "Authorization": "Bearer {{authToken}}" - } } + ``` + + ## Error Responses + + | Code | Description | + |------|-------------| + | 401 | Unauthorized | + | 500 | Internal server error | + + ## Notes + + - Books are sorted by read_count in descending order + - Only books owned by the authenticated user are included + - avg_completion is calculated from all reading sessions } diff --git a/bruno/analytics/Get Reading Stats Date Range.bru b/bruno/analytics/Get Reading Stats Date Range.bru index 94eff7a..e9375af 100644 --- a/bruno/analytics/Get Reading Stats Date Range.bru +++ b/bruno/analytics/Get Reading Stats Date Range.bru @@ -1,30 +1,95 @@ -{ - "meta": { - "name": "Get Reading Stats with Date Range", - "type": "http", - "event": [ +meta { + name: Get Reading Stats Date Range + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31 + body: none + auth: inherit +} + +tests { + test("status must be 200", function() { + expect(res.status).to.eql(200); + }); + + test("response has daily_reading_minutes array", function() { + const body = JSON.parse(res.body); + expect(body).to.have.property("daily_reading_minutes"); + expect(body.daily_reading_minutes).to.be.an("array"); + }); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + Get reading statistics for a specific date range. + + **Endpoint**: GET /api/analytics/reading-stats + **Auth**: Required (Bearer token) + + ## Query Parameters + + | Parameter | Type | Required | Description | + |-----------|------|-----------|-------------| + | start_date | string | No | Start date (ISO 8601 format, default: 30 days ago) | + | end_date | string | No | End date (ISO 8601 format, default: today) | + + ## Response Fields + + | Field | Type | Description | + |-------|------|-------------| + | total_books_read | int | Total books completed in range | + | total_pages_read | int | Total pages read in range | + | total_reading_time_minutes | int | Total reading time in minutes | + | completion_rate | float | Percentage of books completed (0-1) | + | daily_reading_minutes | array | Daily reading time per day | + | daily_reading_minutes[].date | string | Date (ISO 8601) | + | daily_reading_minutes[].minutes | int | Minutes read on that date | + + ## Example Request + + ``` + GET /api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31 + ``` + + ## Example Response + + ```json + { + "total_books_read": 2, + "total_pages_read": 450, + "total_reading_time_minutes": 720, + "completion_rate": 0.85, + "daily_reading_minutes": [ { - "listen": "test", - "script": { - "exec": [ - "// Test reading stats with custom date range", - "if (response.status === 200) {", - " tests['Date range stats loaded'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has daily_reading_minutes'] = Array.isArray(body.daily_reading_minutes);", - "} else {", - " tests['Date range stats failed'] = false;", - "}" - ] - } + "date": "2024-01-01", + "minutes": 30 + }, + { + "date": "2024-01-02", + "minutes": 45 } ] - }, - "req": { - "url": "{{baseUrl}}/api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31", - "method": "GET", - "headers": { - "Authorization": "Bearer {{authToken}}" - } } + ``` + + ## Error Responses + + | Code | Description | + |------|-------------| + | 400 | Invalid date format | + | 401 | Unauthorized | + | 500 | Internal server error | + + ## Notes + + - Dates must be in ISO 8601 format (YYYY-MM-DD) + - The range is inclusive of both start and end dates + - Daily data only includes days with reading activity > 0 } diff --git a/bruno/analytics/Get Reading Stats.bru b/bruno/analytics/Get Reading Stats.bru index 29de543..c25e4be 100644 --- a/bruno/analytics/Get Reading Stats.bru +++ b/bruno/analytics/Get Reading Stats.bru @@ -1,34 +1,104 @@ -{ - "meta": { - "name": "Get Reading Stats", - "type": "http", - "event": [ +meta { + name: Get Reading Stats + type: http + seq: 4 +} + +get { + url: {{baseUrl}}/api/analytics/reading-stats + body: none + auth: inherit +} + +tests { + test("status must be 200", function() { + expect(res.status).to.eql(200); + }); + + test("response has required stats fields", function() { + const body = JSON.parse(res.body); + expect(body).to.have.property("total_books_read"); + expect(body).to.have.property("total_pages_read"); + expect(body).to.have.property("total_reading_time_minutes"); + expect(body).to.have.property("completion_rate"); + expect(body).to.have.property("daily_reading_minutes"); + }); + + test("daily_reading_minutes is an array", function() { + const body = JSON.parse(res.body); + expect(body.daily_reading_minutes).to.be.an("array"); + }); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + Get overall reading statistics for the authenticated user. + + **Endpoint**: GET /api/analytics/reading-stats + **Auth**: Required (Bearer token) + + ## Query Parameters + + | Parameter | Type | Required | Description | + |-----------|------|-----------|-------------| + | start_date | string | No | Start date (ISO 8601 format) | + | end_date | string | No | End date (ISO 8601 format) | + + ## Response Fields + + | Field | Type | Description | + |-------|------|-------------| + | total_books_read | int | Total books completed | + | total_pages_read | int | Total pages read | + | total_reading_time_minutes | int | Total reading time in minutes | + | completion_rate | float | Average book completion rate (0-1) | + | daily_reading_minutes | array | Daily reading time breakdown | + | daily_reading_minutes[].date | string | Date (ISO 8601) | + | daily_reading_minutes[].minutes | int | Minutes read on that date | + + ## Example Request + + ``` + GET /api/analytics/reading-stats + ``` + + ## Example Response + + ```json + { + "total_books_read": 12, + "total_pages_read": 3450, + "total_reading_time_minutes": 5400, + "completion_rate": 0.78, + "daily_reading_minutes": [ { - "listen": "test", - "script": { - "exec": [ - "// Test reading stats response", - "if (response.status === 200) {", - " tests['Reading stats loaded'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has total_books_read'] = body.total_books_read !== undefined;", - " tests['Has total_pages_read'] = body.total_pages_read !== undefined;", - " tests['Has total_reading_time_minutes'] = body.total_reading_time_minutes !== undefined;", - " tests['Has completion_rate'] = body.completion_rate !== undefined;", - " tests['Has daily_reading_minutes array'] = Array.isArray(body.daily_reading_minutes);", - "} else {", - " tests['Reading stats failed'] = false;", - "}" - ] - } + "date": "2026-01-15", + "minutes": 45 + }, + { + "date": "2026-01-16", + "minutes": 60 } ] - }, - "req": { - "url": "{{baseUrl}}/api/analytics/reading-stats", - "method": "GET", - "headers": { - "Authorization": "Bearer {{authToken}}" - } } + ``` + + ## Error Responses + + | Code | Description | + |------|-------------| + | 400 | Invalid date format | + | 401 | Unauthorized | + | 500 | Internal server error | + + ## Notes + + - Without date parameters, returns stats for the last 30 days + - Dates must be in ISO 8601 format (YYYY-MM-DD) when provided + - Only includes reading activity from the authenticated user + - Daily data includes all days with reading activity } diff --git a/bruno/books/Bulk Delete Books.bru b/bruno/books/Bulk Delete Books.bru index 4af04c5..a415b0d 100644 --- a/bruno/books/Bulk Delete Books.bru +++ b/bruno/books/Bulk Delete Books.bru @@ -1,41 +1,85 @@ -{ - "meta": { - "name": "Bulk Delete Books", - "type": "http", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Test bulk delete response", - "if (response.status === 200) {", - " tests['Bulk delete successful'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has results array'] = Array.isArray(body.results);", - " tests['Has total count'] = body.total !== undefined;", - " tests['Has success count'] = body.success !== undefined;", - " tests['Has failed count'] = body.failed !== undefined;", - "} else {", - " tests['Bulk delete failed'] = false;", - "}" - ] - } - } +meta { + name: Bulk Delete Books + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/books/bulk-delete + body: json + auth: inherit +} + +headers { + Content-Type: application/json +} + +body:json { + { + "book_ids": [ + "{{bookId1}}", + "{{bookId2}}", + "{{bookId3}}" ] - }, - "req": { - "url": "{{baseUrl}}/api/books/bulk-delete", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer {{authToken}}" - }, - "body": { - "book_ids": [ - "{{bookId1}}", - "{{bookId2}}", - "{{bookId3}}" - ] - } } } + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Bulk delete successful'] = true; + const body = res.getBody(); + tests['Has results array'] = Array.isArray(body.results); + tests['Has total count'] = body.total !== undefined; + tests['Has success count'] = body.success !== undefined; + tests['Has failed count'] = body.failed !== undefined; + } else { + tests['Bulk delete failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Delete Books + + Deletes multiple books in a single request. + + **Method:** POST + + **Endpoint:** /api/books/bulk-delete + + **Authentication:** Required (Bearer token) + + **Request Body:** + - `book_ids` (array of strings): Array of book UUIDs to delete + + **Response:** + - `results` (array): Results for each deletion attempt + - `total` (number): Total number of books processed + - `success` (number): Number of successfully deleted books + - `failed` (number): Number of failed deletions + + **Status Codes:** + - 200: Success (with partial results if some failed) + - 400: Invalid request data + - 401: Unauthorized + - 403: Forbidden + - 500: Internal server error + + **Example:** + ```json + { + "book_ids": [ + "uuid-1", + "uuid-2", + "uuid-3" + ] + } + ``` +} diff --git a/bruno/books/Bulk Update Books.bru b/bruno/books/Bulk Update Books.bru index eb079ef..fa808b7 100644 --- a/bruno/books/Bulk Update Books.bru +++ b/bruno/books/Bulk Update Books.bru @@ -1,52 +1,112 @@ -{ - "meta": { - "name": "Bulk Update Books", - "type": "http", - "event": [ +meta { + name: Bulk Update Books + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/api/books/bulk-update + body: json + auth: inherit +} + +headers { + Content-Type: application/json +} + +body:json { + { + "updates": [ { - "listen": "test", - "script": { - "exec": [ - "// Test bulk update response", - "if (response.status === 200) {", - " tests['Bulk update successful'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has results array'] = Array.isArray(body.results);", - " tests['Has total count'] = body.total !== undefined;", - " tests['Has success count'] = body.success !== undefined;", - "} else {", - " tests['Bulk update failed'] = false;", - "}" - ] + "book_id": "{{bookId1}}", + "updates": { + "title": "Updated Title", + "genre": "Science Fiction", + "tags": ["science fiction", "non-fiction", "ACME CORP."] + } + }, + { + "book_id": "{{bookId2}}", + "updates": { + "author": "Updated Author", + "contributors": ["O'Reilly Media", "Penguin Random House"] } } ] - }, - "req": { - "url": "{{baseUrl}}/api/books/bulk-update", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer {{authToken}}" - }, - "body": { - "updates": [ - { - "book_id": "{{bookId1}}", - "updates": { - "title": "Updated Title", - "genre": "Science Fiction", - "tags": ["science fiction", "non-fiction", "ACME CORP."] - } - }, - { - "book_id": "{{bookId2}}", - "updates": { - "author": "Updated Author", - "contributors": ["O'Reilly Media", "Penguin Random House"] - } - } - ] - } } } + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Bulk update successful'] = true; + const body = res.getBody(); + tests['Has results array'] = Array.isArray(body.results); + tests['Has total count'] = body.total !== undefined; + tests['Has success count'] = body.success !== undefined; + } else { + tests['Bulk update failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Update Books + + Updates multiple books in a single request with different fields for each book. + + **Method:** POST + + **Endpoint:** /api/books/bulk-update + + **Authentication:** Required (Bearer token) + + **Request Body:** + - `updates` (array): Array of update objects + - `book_id` (string): Book UUID to update + - `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.) + + **Response:** + - `results` (array): Results for each update attempt + - `total` (number): Total number of books processed + - `success` (number): Number of successfully updated books + - `failed` (number): Number of failed updates + + **Status Codes:** + - 200: Success (with partial results if some failed) + - 400: Invalid request data + - 401: Unauthorized + - 403: Forbidden + - 500: Internal server error + + **Example:** + ```json + { + "updates": [ + { + "book_id": "uuid-1", + "updates": { + "title": "New Title", + "genre": "Fiction", + "tags": ["fiction", "adventure"] + } + }, + { + "book_id": "uuid-2", + "updates": { + "author": "Jane Doe", + "contributors": ["Publisher Inc."] + } + } + ] + } + ``` + + **Note:** Each book can have different fields updated. Only the specified fields are modified for each book. +} diff --git a/bruno/collection.bru b/bruno/collection.bru index e9a511f..ed964c2 100644 --- a/bruno/collection.bru +++ b/bruno/collection.bru @@ -2,9 +2,6 @@ auth { mode: bearer } -auth:bearer { - token: {{token}} -} docs { # Bruno API Tests for Bookhoard diff --git a/bruno/collections/Add Books to Collection.bru b/bruno/collections/Add Books to Collection.bru index 5472789..2cbd082 100644 --- a/bruno/collections/Add Books to Collection.bru +++ b/bruno/collections/Add Books to Collection.bru @@ -7,12 +7,9 @@ meta { post { url: {{base_url}}/api/collections/{{collection_id}}/books body: json - auth: bearer + auth: inherit } -auth:bearer { - token: {{token}} -} body:json { { diff --git a/bruno/collections/Bulk Add Books to Collections.bru b/bruno/collections/Bulk Add Books to Collections.bru index b6f4b53..05a0e0a 100644 --- a/bruno/collections/Bulk Add Books to Collections.bru +++ b/bruno/collections/Bulk Add Books to Collections.bru @@ -1,49 +1,103 @@ -{ - "meta": { - "name": "Bulk Add Books to Collections", - "type": "http", - "event": [ +meta { + name: Bulk Add Books to Collections + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/collections/bulk-add-books + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "operations": [ { - "listen": "test", - "script": { - "exec": [ - "// Test bulk add to collections response", - "if (response.status === 200) {", - " tests['Bulk add successful'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has results array'] = Array.isArray(body.results);", - " tests('All operations processed', body.results.length > 0);", - "} else {", - " tests['Bulk add failed'] = false;", - "}" - ] - } + "collection_id": "{{collectionId1}}", + "book_ids": [ + "{{bookId1}}", + "{{bookId2}}" + ] + }, + { + "collection_id": "{{collectionId2}}", + "book_ids": [ + "{{bookId3}}" + ] } ] - }, - "req": { - "url": "{{baseUrl}}/api/collections/bulk-add-books", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer {{authToken}}" - }, - "body": { - "operations": [ - { - "collection_id": "{{collectionId1}}", - "book_ids": [ - "{{bookId1}}", - "{{bookId2}}" - ] - }, - { - "collection_id": "{{collectionId2}}", - "book_ids": [ - "{{bookId3}}" - ] - } - ] - } } } + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Bulk add successful'] = true; + const body = res.getBody(); + tests['Has results array'] = Array.isArray(body.results); + tests['All operations processed'] = body.results.length > 0; + } else { + tests['Bulk add failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Add Books to Collections + + Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add. + + **Method:** POST + + **Endpoint:** /api/collections/bulk-add-books + + **Authentication:** Bearer token + + **Request Body:** + - `operations` (array): Array of collection-book operations + - `collection_id` (string): Collection UUID + - `book_ids` (array): Array of book UUIDs to add to the collection + + **Response:** + - `results` (array): Results for each operation + - `total` (number): Total number of operations + - `success` (number): Number of successful operations + - `failed` (number): Number of failed operations + + **Status Codes:** + - 200: Success (with partial results if some failed) + - 400: Invalid request data + - 401: Unauthorized + - 403: Forbidden + - 500: Internal server error + + **Example:** + ```json + { + "operations": [ + { + "collection_id": "collection-uuid-1", + "book_ids": ["book-1", "book-2"] + }, + { + "collection_id": "collection-uuid-2", + "book_ids": ["book-3"] + } + ] + } + ``` + + **Note:** Adding a book that's already in a collection is idempotent (no error). +} diff --git a/bruno/collections/Bulk Remove Books - All Books.bru b/bruno/collections/Bulk Remove Books - All Books.bru new file mode 100644 index 0000000..3d89d30 --- /dev/null +++ b/bruno/collections/Bulk Remove Books - All Books.bru @@ -0,0 +1,55 @@ +meta { + name: Bulk Remove Books - All Books + type: http + seq: 5 +} + +post { + url: {{baseUrl}}/api/collections/{{collection_id}}/books/bulk-remove + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "book_ids": [ + "{{bookId1}}", + "{{bookId2}}", + "{{bookId3}}" + ] + } +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['All books removed'] = true; + const body = res.getBody(); + tests['Removed equals total'] = body.removed === body.total; + tests['Total is 3'] = body.total === 3; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Remove Books - All Books + + Tests removing multiple books from a collection in a single request. + + **Expected Result:** 200 OK with removed: 3, total: 3 + + **Purpose:** Verifies the bulk remove functionality works correctly when removing multiple books that all exist in the collection. + + **Use Case:** Common scenario when a user wants to remove several books from a collection at once, such as when reorganizing their library or removing books they've finished reading. +} diff --git a/bruno/collections/Bulk Remove Books - Empty List.bru b/bruno/collections/Bulk Remove Books - Empty List.bru new file mode 100644 index 0000000..0e04247 --- /dev/null +++ b/bruno/collections/Bulk Remove Books - Empty List.bru @@ -0,0 +1,46 @@ +meta { + name: Bulk Remove Books - Empty List + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/api/collections/{{collection_id}}/books/bulk-remove + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "book_ids": [] + } +} + +script:post-response { + function onResponse(res) { + tests['Returns 400 for empty list'] = res.getStatus() === 400; + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Remove Books - Empty List Validation + + Tests validation behavior when providing an empty book_ids array. + + **Expected Result:** 400 Bad Request + + **Validation Rule:** book_ids array must contain at least one book UUID. + + **Purpose:** Ensures the API properly validates input and rejects empty removal requests. +} diff --git a/bruno/collections/Bulk Remove Books - Invalid IDs.bru b/bruno/collections/Bulk Remove Books - Invalid IDs.bru new file mode 100644 index 0000000..65d3643 --- /dev/null +++ b/bruno/collections/Bulk Remove Books - Invalid IDs.bru @@ -0,0 +1,59 @@ +meta { + name: Bulk Remove Books - Invalid IDs + type: http + seq: 4 +} + +post { + url: {{baseUrl}}/api/collections/{{collection_id}}/books/bulk-remove + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "book_ids": [ + "{{bookId1}}", + "invalid-uuid-format", + "{{bookId2}}" + ] + } +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Partial success accepted'] = true; + const body = res.getBody(); + tests['Has removed count'] = body.removed !== undefined; + tests['Has results array'] = Array.isArray(body.results); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Remove Books - Invalid IDs + + Tests behavior when the book_ids array contains invalid UUID formats or non-existent books. + + **Expected Result:** 200 OK with partial success + + **Purpose:** Verifies that: + - Invalid UUID formats don't crash the endpoint + - Non-existent book IDs are handled gracefully + - Valid IDs in the same request are still processed + - Response includes detailed results showing which succeeded/failed + + **Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately. +} diff --git a/bruno/collections/Bulk Remove Books - Single Book.bru b/bruno/collections/Bulk Remove Books - Single Book.bru new file mode 100644 index 0000000..0715090 --- /dev/null +++ b/bruno/collections/Bulk Remove Books - Single Book.bru @@ -0,0 +1,52 @@ +meta { + name: Bulk Remove Books - Single Book + type: http + seq: 3 +} + +post { + url: {{baseUrl}}/api/collections/{{collection_id}}/books/bulk-remove + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "book_ids": [ + "{{bookId1}}" + ] + } +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Single book removed'] = body.removed === 1; + tests['Total is 1'] = body.total === 1; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Remove Books - Single Book + + Tests that bulk remove endpoint works correctly with a single book. + + **Expected Result:** 200 OK with removed: 1, total: 1 + + **Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals. + + **Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format. +} diff --git a/bruno/collections/Bulk Remove Books.bru b/bruno/collections/Bulk Remove Books.bru index 406e4ed..db21a99 100644 --- a/bruno/collections/Bulk Remove Books.bru +++ b/bruno/collections/Bulk Remove Books.bru @@ -1,103 +1,116 @@ meta { - name: "Bulk Remove Books from Collection" + name: Bulk Remove Books from Collection type: http seq: 1 } -# Setup: Create a test collection first post { - url: {{baseUrl}}/api/collections - body: { - name: "Bulk Remove Test Collection" - description: "Collection for testing bulk remove" - color: "#FF5733" - icon: "📚" - } - assert { - res.status: 200 - } - # Store collection_id from response - # Note: In actual Bruno, you'd use variables + url: {{baseUrl}}/api/collections/{{collection_id}}/books/bulk-remove + body: json + auth: inherit } -# Add some books to the collection -post { - url: {{baseUrl}}/api/collections/{collection_id}/books - body: { - book_ids: [ - "book-id-1", - "book-id-2", - "book-id-3" +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "book_ids": [ + "{{bookId1}}", + "{{bookId2}}", + "{{bookId3}}" ] } - assert { - res.status: 204 - } } -# Test 1: Bulk remove all books -post { - url: {{baseUrl}}/api/collections/{collection_id}/books/bulk-remove - body: { - book_ids: [ - "book-id-1", - "book-id-2", - "book-id-3" +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Bulk remove successful'] = true; + const body = res.getBody(); + tests['Has removed count'] = body.removed !== undefined; + tests['Has total count'] = body.total !== undefined; + tests['Has results array'] = Array.isArray(body.results); + } else { + tests['Bulk remove failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Remove Books from Collection + + Removes multiple books from a collection in a single request. + + **Method:** POST + + **Endpoint:** /api/collections/{collection_id}/books/bulk-remove + + **Authentication:** Bearer token + + **Path Parameters:** + - `collection_id` (string): Collection UUID + + **Request Body:** + - `book_ids` (array): Array of book UUIDs to remove from the collection + + **Response:** + - `removed` (number): Number of books successfully removed + - `total` (number): Total number of books processed + - `results` (array): Results for each removal attempt + - `book_id` (string): Book UUID + - `success` (boolean): Whether the removal succeeded + - `error` (string, optional): Error message if failed + + **Status Codes:** + - 200: Success (with partial results if some failed) + - 400: Invalid request data (e.g., empty book_ids array) + - 401: Unauthorized + - 403: Forbidden + - 404: Collection not found + - 500: Internal server error + + **Example Request:** + ```json + { + "book_ids": [ + "book-uuid-1", + "book-uuid-2", + "book-uuid-3" ] } - assert { - res.status: 200 - res.body.removed: #number - res.body.total: 3 - } -} + ``` -# Test 2: Bulk remove with some invalid IDs -post { - url: {{baseUrl}}/api/collections/{collection_id}/books/bulk-remove - body: { - book_ids: [ - "book-id-4", - "invalid-id", - "book-id-5" + **Example Response:** + ```json + { + "removed": 2, + "total": 3, + "results": [ + { + "book_id": "book-uuid-1", + "success": true + }, + { + "book_id": "book-uuid-2", + "success": true + }, + { + "book_id": "book-uuid-3", + "success": false, + "error": "Book not in collection" + } ] } - assert { - res.status: 200 - res.body.removed: #number - } -} + ``` -# Test 3: Empty list (should fail validation) -post { - url: {{baseUrl}}/api/collections/{collection_id}/books/bulk-remove - body: { - book_ids: [] - } - assert { - res.status: 400 - } -} - -# Test 4: Single book (bulk remove should work for 1 book too) -post { - url: {{baseUrl}}/api/collections/{collection_id}/books/bulk-remove - body: { - book_ids: [ - "book-id-6" - ] - } - assert { - res.status: 200 - res.body.removed: 1 - res.body.total: 1 - } -} - -# Cleanup: Delete test collection -delete { - url: {{baseUrl}}/api/collections/{collection_id} - assert { - res.status: 204 - } + **Note:** Removing a book that's not in the collection returns success: false for that book but doesn't fail the entire request. Empty book_ids array returns 400. } diff --git a/bruno/collections/Create Collection.bru b/bruno/collections/Create Collection.bru index 1b0bde7..ae52209 100644 --- a/bruno/collections/Create Collection.bru +++ b/bruno/collections/Create Collection.bru @@ -7,12 +7,9 @@ meta { post { url: {{base_url}}/api/collections body: json - auth: bearer + auth: inherit } -auth:bearer { - token: {{token}} -} body:json { { diff --git a/bruno/collections/Create Device Mapping.bru b/bruno/collections/Create Device Mapping.bru index a3e6d8d..f41e98d 100644 --- a/bruno/collections/Create Device Mapping.bru +++ b/bruno/collections/Create Device Mapping.bru @@ -7,10 +7,9 @@ meta { post { url: {{base_url}}/api/devices/{{device_id}}/collections body: json - auth: bearer + auth: inherit } -auth:bearer { token: {{token}} } diff --git a/bruno/collections/Delete Collection.bru b/bruno/collections/Delete Collection.bru index 2f95562..c6a6f70 100644 --- a/bruno/collections/Delete Collection.bru +++ b/bruno/collections/Delete Collection.bru @@ -6,9 +6,5 @@ meta { delete { url: {{base_url}}/api/collections/{{collection_id}} - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Delete Device Mapping.bru b/bruno/collections/Delete Device Mapping.bru index 3aca44c..fbf42f7 100644 --- a/bruno/collections/Delete Device Mapping.bru +++ b/bruno/collections/Delete Device Mapping.bru @@ -6,9 +6,5 @@ meta { delete { url: {{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}} - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Get Book Collections.bru b/bruno/collections/Get Book Collections.bru index e02a265..7b5e730 100644 --- a/bruno/collections/Get Book Collections.bru +++ b/bruno/collections/Get Book Collections.bru @@ -6,9 +6,5 @@ meta { get { url: {{base_url}}/api/collections/books/{{book_id}} - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Get Collection.bru b/bruno/collections/Get Collection.bru index a7af5bd..477b301 100644 --- a/bruno/collections/Get Collection.bru +++ b/bruno/collections/Get Collection.bru @@ -6,9 +6,5 @@ meta { get { url: {{base_url}}/api/collections/{{collection_id}} - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Get Collections.bru b/bruno/collections/Get Collections.bru index 246b54f..4b90abd 100644 --- a/bruno/collections/Get Collections.bru +++ b/bruno/collections/Get Collections.bru @@ -6,9 +6,5 @@ meta { get { url: {{base_url}}/api/collections?include_auto=true&sort_by=name - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Get Device Mappings.bru b/bruno/collections/Get Device Mappings.bru index 27be071..de2c587 100644 --- a/bruno/collections/Get Device Mappings.bru +++ b/bruno/collections/Get Device Mappings.bru @@ -6,9 +6,5 @@ meta { get { url: {{base_url}}/api/devices/{{device_id}}/collections - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Remove Book from Collection.bru b/bruno/collections/Remove Book from Collection.bru index f9e7408..d20967b 100644 --- a/bruno/collections/Remove Book from Collection.bru +++ b/bruno/collections/Remove Book from Collection.bru @@ -6,9 +6,5 @@ meta { delete { url: {{base_url}}/api/collections/{{collection_id}}/books/{{book_id}} - auth: bearer -} - -auth:bearer { - token: {{token}} + auth: inherit } diff --git a/bruno/collections/Test Collection Rules - Author Contains.bru b/bruno/collections/Test Collection Rules - Author Contains.bru new file mode 100644 index 0000000..7a0ed11 --- /dev/null +++ b/bruno/collections/Test Collection Rules - Author Contains.bru @@ -0,0 +1,61 @@ +meta { + name: Test Collection Rules - Author Contains + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/api/collections/test-rules + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "rules": [ + { + "field": "author", + "operator": "contains", + "value": "Asimov" + } + ] + } +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Author search successful'] = true; + const body = res.getBody(); + tests['Has matches array'] = Array.isArray(body.matches); + tests['Found books by Asimov'] = body.matches.length > 0; + } else { + tests['Author search failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Test Collection Rules - Author Contains + + Tests the "contains" operator on the author field to find books by a specific author (partial match). + + **Example Use Case:** Finding all books by an author whose name contains "Asimov" (e.g., "Isaac Asimov"). + + **Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically). + + **Expected Result:** Returns all books where the author field contains "Asimov". + + **Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names. +} diff --git a/bruno/collections/Test Collection Rules - Copyright Year Greater Than.bru b/bruno/collections/Test Collection Rules - Copyright Year Greater Than.bru new file mode 100644 index 0000000..c7f0df9 --- /dev/null +++ b/bruno/collections/Test Collection Rules - Copyright Year Greater Than.bru @@ -0,0 +1,63 @@ +meta { + name: Test Collection Rules - Copyright Year Greater Than + type: http + seq: 3 +} + +post { + url: {{baseUrl}}/api/collections/test-rules + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "rules": [ + { + "field": "copyright_year", + "operator": "greater_than", + "value": "2000" + } + ] + } +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Year comparison successful'] = true; + const body = res.getBody(); + tests['Has matches array'] = Array.isArray(body.matches); + tests('Found books after 2000', body.matches.length >= 0); + } else { + tests['Year comparison failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Test Collection Rules - Copyright Year Greater Than + + Tests the "greater_than" operator on the copyright_year field to find books published after a specific year. + + **Example Use Case:** Creating a "Modern Books" collection with books published after 2000. + + **Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison). + + **Field:** `copyright_year` - The year the book was copyrighted/published. + + **Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later). + + **Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era. +} diff --git a/bruno/collections/Test Collection Rules - Empty Rules Array.bru b/bruno/collections/Test Collection Rules - Empty Rules Array.bru new file mode 100644 index 0000000..7d4d66e --- /dev/null +++ b/bruno/collections/Test Collection Rules - Empty Rules Array.bru @@ -0,0 +1,48 @@ +meta { + name: Test Collection Rules - Empty Rules Array + type: http + seq: 5 +} + +post { + url: {{baseUrl}}/api/collections/test-rules + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "rules": [] + } +} + +script:post-response { + function onResponse(res) { + tests['Empty rules rejected'] = res.getStatus() === 400; + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Test Collection Rules - Empty Rules Array + + Tests validation behavior when providing an empty rules array. + + **Expected Result:** 400 Bad Request + + **Validation Rule:** rules array must contain at least one rule object. + + **Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues. + + **Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests. +} diff --git a/bruno/collections/Test Collection Rules - No Matches.bru b/bruno/collections/Test Collection Rules - No Matches.bru new file mode 100644 index 0000000..7920fc7 --- /dev/null +++ b/bruno/collections/Test Collection Rules - No Matches.bru @@ -0,0 +1,63 @@ +meta { + name: Test Collection Rules - No Matches + type: http + seq: 4 +} + +post { + url: {{baseUrl}}/api/collections/test-rules + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "rules": [ + { + "field": "genre", + "operator": "equals", + "value": "NonExistentGenre123456" + } + ] + } +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['No matches returned'] = body.total === 0; + tests['Empty matches array'] = body.matches.length === 0; + tests['Success with zero results'] = true; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Test Collection Rules - No Matches + + Tests behavior when collection rules don't match any books in the library. + + **Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases. + + **Expected Result:** 200 OK with empty matches array and total: 0 + + **Purpose:** Verifies that the API handles zero-match scenarios gracefully: + - Returns 200 (success) not 404 + - Returns empty array, not null + - Returns total: 0 for clarity + - No errors thrown for no results + + **Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections. +} diff --git a/bruno/collections/Test Collection Rules.bru b/bruno/collections/Test Collection Rules.bru index cd6c311..2d62e01 100644 --- a/bruno/collections/Test Collection Rules.bru +++ b/bruno/collections/Test Collection Rules.bru @@ -1,83 +1,119 @@ meta { - name: "Test Collection Rules" + name: Test Collection Rules type: http seq: 1 } post { url: {{baseUrl}}/api/collections/test-rules - body: { - rules: [ + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "rules": [ { - field: "genre", - operator: "equals", - value: "Science Fiction" + "field": "genre", + "operator": "equals", + "value": "Science Fiction" } ] } - assert { - res.status: 200 - res.body.matches: #array - res.body.total: #number - } } -post { - url: {{baseUrl}}/api/collections/test-rules - body: { - rules: [ +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Rules test successful'] = true; + const body = res.getBody(); + tests['Has matches array'] = Array.isArray(body.matches); + tests['Has total count'] = body.total !== undefined; + } else { + tests['Rules test failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Test Collection Rules + + Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules. + + **Method:** POST + + **Endpoint:** /api/collections/test-rules + + **Authentication:** Bearer token + + **Request Body:** + - `rules` (array): Array of rule objects to test + - `field` (string): Field to test (genre, author, copyright_year, tags, etc.) + - `operator` (string): Comparison operator + - `equals`: Exact match + - `contains`: Contains substring (for text fields) + - `greater_than`: Greater than (for numeric fields) + - `less_than`: Less than (for numeric fields) + - `not_equals`: Not equal to + - `starts_with`: Starts with + - `ends_with`: Ends with + - `is_empty`: Field is empty or null + - `is_not_empty`: Field is not empty and not null + - `value` (string): Value to compare against (not required for is_empty/is_not_empty) + + **Response:** + - `matches` (array): Array of matching books + - `id` (string): Book UUID + - `title` (string): Book title + - `author` (string): Book author + - `genre` (string): Book genre + - Additional book metadata + - `total` (number): Total number of matching books + + **Status Codes:** + - 200: Success - returns matching books + - 400: Invalid request (empty rules array, invalid field/operator) + - 401: Unauthorized + - 500: Internal server error + + **Example Request:** + ```json + { + "rules": [ { - field: "author", - operator: "contains", - value: "Asimov" + "field": "genre", + "operator": "equals", + "value": "Science Fiction" } ] } - assert { - res.status: 200 - res.body.matches: #array - } -} + ``` -post { - url: {{baseUrl}}/api/collections/test-rules - body: { - rules: [ + **Example Response:** + ```json + { + "matches": [ { - field: "copyright_year", - operator: "greater_than", - value: "2000" + "id": "book-uuid-1", + "title": "Foundation", + "author": "Isaac Asimov", + "genre": "Science Fiction" } - ] + ], + "total": 1 } - assert { - res.status: 200 - } -} + ``` -post { - url: {{baseUrl}}/api/collections/test-rules - body: { - rules: [ - { - field: "genre", - operator: "equals", - value: "NonExistentGenre123456" - } - ] - } - assert { - res.status: 200 - res.body.total: 0 - } -} - -post { - url: {{baseUrl}}/api/collections/test-rules - body: { - rules: [] - } - assert { - res.status: 400 - } + **Note:** This endpoint is useful for validating collection rules before creating a collection, or for dynamically querying books based on criteria. } diff --git a/bruno/collections/Update Collection.bru b/bruno/collections/Update Collection.bru index a5a4859..2a21855 100644 --- a/bruno/collections/Update Collection.bru +++ b/bruno/collections/Update Collection.bru @@ -7,12 +7,9 @@ meta { put { url: {{base_url}}/api/collections/{{collection_id}} body: json - auth: bearer + auth: inherit } -auth:bearer { - token: {{token}} -} body:json { { diff --git a/bruno/collections/Update Device Mapping.bru b/bruno/collections/Update Device Mapping.bru index 506ecf7..973ccda 100644 --- a/bruno/collections/Update Device Mapping.bru +++ b/bruno/collections/Update Device Mapping.bru @@ -7,12 +7,9 @@ meta { put { url: {{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}} body: json - auth: bearer + auth: inherit } -auth:bearer { - token: {{token}} -} body:json { { diff --git a/bruno/conflicts/Bulk Dismiss Conflicts.bru b/bruno/conflicts/Bulk Dismiss Conflicts.bru index 222524a..c58c1f0 100644 --- a/bruno/conflicts/Bulk Dismiss Conflicts.bru +++ b/bruno/conflicts/Bulk Dismiss Conflicts.bru @@ -1,40 +1,84 @@ -{ - "meta": { - "name": "Bulk Dismiss Conflicts", - "type": "http", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Test bulk dismiss response", - "if (response.status === 200) {", - " tests['Bulk dismiss successful'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has results array'] = Array.isArray(body.results);", - " tests['Has total count'] = body.total !== undefined;", - " tests['Has success count'] = body.success !== undefined;", - " tests['Has failed count'] = body.failed !== undefined;", - "} else {", - " tests['Bulk dismiss failed'] = false;", - "}" - ] - } - } +meta { + name: Bulk Dismiss Conflicts + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/conflicts/bulk-dismiss + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "conflict_ids": [ + "{{conflictId1}}", + "{{conflictId2}}" ] - }, - "req": { - "url": "{{baseUrl}}/api/conflicts/bulk-dismiss", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer {{authToken}}" - }, - "body": { - "conflict_ids": [ - "{{conflictId1}}", - "{{conflictId2}}" - ] - } } } + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Bulk dismiss successful'] = true; + const body = res.getBody(); + tests['Has results array'] = Array.isArray(body.results); + tests['Has total count'] = body.total !== undefined; + tests['Has success count'] = body.success !== undefined; + tests['Has failed count'] = body.failed !== undefined; + } else { + tests['Bulk dismiss failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Dismiss Conflicts + + Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged. + + **Method:** POST + + **Endpoint:** /api/conflicts/bulk-dismiss + + **Authentication:** Bearer token + + **Request Body:** + - `conflict_ids` (array): Array of conflict UUIDs to dismiss + + **Response:** + - `results` (array): Results for each dismissal + - `total` (number): Total number of conflicts processed + - `success` (number): Number of successfully dismissed conflicts + - `failed` (number): Number of failed dismissals + + **Status Codes:** + - 200: Success + - 400: Invalid request data + - 401: Unauthorized + - 403: Forbidden + - 404: One or more conflicts not found + - 500: Internal server error + + **Example:** + ```json + { + "conflict_ids": ["uuid-1", "uuid-2"] + } + ``` + + **Note:** Dismissing a conflict removes it from the conflict list but does not merge or resolve the conflicting data. Use this when you want to ignore a conflict and handle it manually. +} diff --git a/bruno/conflicts/Bulk Resolve Conflicts.bru b/bruno/conflicts/Bulk Resolve Conflicts.bru index 8e51616..57403a2 100644 --- a/bruno/conflicts/Bulk Resolve Conflicts.bru +++ b/bruno/conflicts/Bulk Resolve Conflicts.bru @@ -1,43 +1,93 @@ -{ - "meta": { - "name": "Bulk Resolve Conflicts", - "type": "http", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Test bulk resolution response", - "if (response.status === 200) {", - " tests['Bulk resolve successful'] = true;", - " const body = JSON.parse(response.body);", - " tests['Has results array'] = Array.isArray(body.results);", - " tests['Has total count'] = body.total !== undefined;", - " tests['Has success count'] = body.success !== undefined;", - " tests['Has failed count'] = body.failed !== undefined;", - " tests['Total equals sum of success and failed'] = body.total === body.success + body.failed;", - "} else {", - " tests['Bulk resolve failed'] = false;", - "}" - ] - } - } - ] - }, - "req": { - "url": "{{baseUrl}}/api/conflicts/bulk-resolve", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer {{authToken}}" - }, - "body": { - "conflict_ids": [ - "{{conflictId1}}", - "{{conflictId2}}", - "{{conflictId3}}" - ], - "strategy": "most_recent" - } +meta { + name: Bulk Resolve Conflicts + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/conflicts/bulk-resolve + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "conflict_ids": [ + "{{conflictId1}}", + "{{conflictId2}}", + "{{conflictId3}}" + ], + "strategy": "most_recent" } } + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Bulk resolve successful'] = true; + const body = res.getBody(); + tests['Has results array'] = Array.isArray(body.results); + tests['Has total count'] = body.total !== undefined; + tests['Has success count'] = body.success !== undefined; + tests['Has failed count'] = body.failed !== undefined; + tests['Total equals sum of success and failed'] = body.total === body.success + body.failed; + } else { + tests['Bulk resolve failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Resolve Conflicts + + Resolves multiple sync conflicts in a single request using a specified resolution strategy. + + **Method:** POST + + **Endpoint:** /api/conflicts/bulk-resolve + + **Authentication:** Bearer token + + **Request Body:** + - `conflict_ids` (array): Array of conflict UUIDs to resolve + - `strategy` (string): Resolution strategy + - `most_recent`: Use the most recently updated progress + - `highest_progress`: Use the reading progress with the highest percent read + - `server`: Always prefer server-side data + - `device`: Always prefer device-side data + + **Response:** + - `results` (array): Results for each conflict resolution + - `total` (number): Total number of conflicts processed + - `success` (number): Number of successfully resolved conflicts + - `failed` (number): Number of failed resolutions + + **Status Codes:** + - 200: Success (with partial results if some failed) + - 400: Invalid request data + - 401: Unauthorized + - 403: Forbidden + - 404: One or more conflicts not found + - 500: Internal server error + + **Example:** + ```json + { + "conflict_ids": ["uuid-1", "uuid-2", "uuid-3"], + "strategy": "most_recent" + } + ``` + + **Note:** Conflicts are resolved atomically per conflict. If one resolution fails, others may still succeed. +} diff --git a/bruno/conflicts/Bulk Resolve Highest Progress.bru b/bruno/conflicts/Bulk Resolve Highest Progress.bru index 2478173..723b9c1 100644 --- a/bruno/conflicts/Bulk Resolve Highest Progress.bru +++ b/bruno/conflicts/Bulk Resolve Highest Progress.bru @@ -1,37 +1,74 @@ -{ - "meta": { - "name": "Bulk Resolve with Highest Progress Strategy", - "type": "http", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Test highest progress strategy", - "if (response.status === 200) {", - " tests['Highest progress strategy successful'] = true;", - " const body = JSON.parse(response.body);", - " tests['At least one conflict resolved'] = body.success > 0;", - "} else {", - " tests['Strategy failed'] = false;", - "}" - ] - } - } - ] - }, - "req": { - "url": "{{baseUrl}}/api/conflicts/bulk-resolve", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer {{authToken}}" - }, - "body": { - "conflict_ids": [ - "{{conflictId1}}" - ], - "strategy": "highest_progress" - } +meta { + name: Bulk Resolve with Highest Progress Strategy + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/conflicts/bulk-resolve + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "conflict_ids": [ + "{{conflictId1}}" + ], + "strategy": "highest_progress" } } + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + tests['Highest progress strategy successful'] = true; + const body = res.getBody(); + tests['At least one conflict resolved'] = body.success > 0; + } else { + tests['Strategy failed'] = false; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Resolve with Highest Progress Strategy + + Resolves multiple sync conflicts using the "highest_progress" strategy, which keeps the reading progress with the highest percentage read. + + **Method:** POST + + **Endpoint:** /api/conflicts/bulk-resolve + + **Authentication:** Bearer token + + **Request Body:** + - `conflict_ids` (array): Array of conflict UUIDs to resolve + - `strategy` (string): Must be "highest_progress" + + **Response:** + - `results` (array): Results for each conflict resolution + - `total` (number): Total number of conflicts processed + - `success` (number): Number of successfully resolved conflicts + - `failed` (number): Number of failed resolutions + + **Status Codes:** + - 200: Success + - 400: Invalid request data + - 401: Unauthorized + - 403: Forbidden + - 500: Internal server error + + **Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been reading on multiple devices and want to keep the furthest position. +} diff --git a/bruno/conflicts/Delete Conflict.bru b/bruno/conflicts/Delete Conflict.bru index cd4d334..f2ca131 100644 --- a/bruno/conflicts/Delete Conflict.bru +++ b/bruno/conflicts/Delete Conflict.bru @@ -7,27 +7,44 @@ meta { delete { url: {{baseUrl}}/api/conflicts/{{conflict_id}} body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } -docs: { - Deletes a specific conflict record. - - Path Parameters: - - conflict_id: UUID of the conflict to delete - - Use this when: - - A conflict was created in error - - You want to dismiss a conflict without resolving it - - The conflict is no longer relevant - - Response: 204 No Content on success - - Note: This permanently removes the conflict record. - Consider resolving the conflict instead if you want to - maintain an audit trail of what happened. +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Delete Conflict + + Permanently deletes a specific conflict record from the system. + + **Method:** DELETE + + **Endpoint:** /api/conflicts/{conflict_id} + + **Authentication:** Bearer token + + **Path Parameters:** + - `conflict_id` (string): Conflict UUID to delete + + **Response:** 204 No Content on success + + **Status Codes:** + - 204: Success - conflict deleted + - 401: Unauthorized + - 404: Conflict not found + - 500: Internal server error + + **Use Cases:** + - Conflict was created in error + - Dismissing a conflict without resolving it + - Conflict is no longer relevant (e.g., book deleted) + + **Note:** This permanently removes the conflict record with no undo option. Consider resolving the conflict instead if you want to maintain an audit trail of what happened. } diff --git a/bruno/conflicts/Dismiss All Resolved.bru b/bruno/conflicts/Dismiss All Resolved.bru index 44ee325..b22fdc6 100644 --- a/bruno/conflicts/Dismiss All Resolved.bru +++ b/bruno/conflicts/Dismiss All Resolved.bru @@ -7,29 +7,48 @@ meta { post { url: {{baseUrl}}/api/conflicts/dismiss-all body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } -docs: { - Deletes all resolved conflicts for the authenticated user. - - Use this to: - - Clean up your conflicts list after reviewing resolutions - - Remove old resolved conflicts that are no longer needed - - Maintain a clean conflict history - - Response includes: - - deleted: Number of conflict records that were deleted - - Example response: +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Dismiss All Resolved Conflicts + + Deletes all resolved conflicts for the authenticated user, cleaning up the conflict list. + + **Method:** POST + + **Endpoint:** /api/conflicts/dismiss-all + + **Authentication:** Bearer token + + **Response:** + - `deleted` (number): Number of conflict records that were deleted + + **Status Codes:** + - 200: Success - conflicts deleted + - 401: Unauthorized + - 500: Internal server error + + **Example Response:** + ```json { "deleted": 5 } - - Note: Only resolves conflicts with status "user_resolved" - are deleted. Unresolved conflicts are preserved. + ``` + + **Use Cases:** + - Clean up conflicts list after reviewing resolutions + - Remove old resolved conflicts no longer needed + - Maintain a clean conflict history + + **Note:** Only conflicts with status "user_resolved" or "auto_resolved" are deleted. Unresolved conflicts are preserved. } diff --git a/bruno/conflicts/Get Conflict Details.bru b/bruno/conflicts/Get Conflict Details.bru index 053179a..a443e89 100644 --- a/bruno/conflicts/Get Conflict Details.bru +++ b/bruno/conflicts/Get Conflict Details.bru @@ -7,52 +7,87 @@ meta { get { url: {{baseUrl}}/api/conflicts/{{conflict_id}} body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } -docs: { - Retrieves detailed information about a specific conflict. - - Path Parameters: - - conflict_id: UUID of the conflict - - Response includes: - - id: Conflict UUID - - media_item_id: Associated book UUID - - media_item_title: Book title - - conflict_type: Type of conflict - - conflict_data: Side-by-side comparison with sources: - * source: Device/source identifier (koreader, kobo, web, etc.) - * timestamp: When this progress was recorded - * data: The conflicting data (percentage, epubcfi, chapter, etc.) - - resolution_status: Current status - - resolution_data: If resolved, includes resolution details - - resolved_by: User ID who resolved it (if applicable) - - resolved_at: When it was resolved (if applicable) - - created_at: When conflict was detected - - Example conflict_data: +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Conflict Details + + Retrieves detailed information about a specific conflict, including side-by-side comparison of conflicting data from all sources. + + **Method:** GET + + **Endpoint:** /api/conflicts/{conflict_id} + + **Authentication:** Bearer token + + **Path Parameters:** + - `conflict_id` (string): Conflict UUID + + **Response:** + - `id` (string): Conflict UUID + - `media_item_id` (string): Associated book UUID + - `media_item_title` (string): Book title + - `conflict_type` (string): Type of conflict (progress, note, highlight) + - `conflict_data` (object): Side-by-side comparison from each source + - Each source includes: + - `source` (string): Device/source identifier (koreader, kobo, web, etc.) + - `timestamp` (string): When this data was recorded + - `data` (object): The conflicting data + - `percentage` (number): Reading progress + - `epubcfi` (string): EPUB location + - `chapter` (number): Chapter number + - `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved) + - `resolution_data` (object, optional): If resolved, includes resolution details + - `resolved_by` (string, optional): User ID who resolved it + - `resolved_at` (string, optional): When it was resolved + - `created_at` (string): When conflict was detected + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Conflict not found + - 500: Internal server error + + **Example Response:** + ```json { - "koreader": { - "source": "koreader", - "timestamp": "2026-01-30T20:10:00Z", - "data": { - "percentage": 0.45, - "epubcfi": "epubcfi(/6/4/2:15)", - "chapter": 3 + "id": "conflict-uuid", + "media_item_id": "book-uuid", + "media_item_title": "Foundation", + "conflict_type": "progress", + "conflict_data": { + "koreader": { + "source": "koreader", + "timestamp": "2026-01-30T20:10:00Z", + "data": { + "percentage": 0.45, + "epubcfi": "epubcfi(/6/4/2:15)", + "chapter": 3 + } + }, + "kobo": { + "source": "kobo", + "timestamp": "2026-01-30T20:05:00Z", + "data": { + "percentage": 0.42, + "location": "unknown" + } } }, - "kobo": { - "source": "kobo", - "timestamp": "2026-01-30T20:05:00Z", - "data": { - "percentage": 0.42, - "location": "unknown" - } - } + "resolution_status": "unresolved", + "created_at": "2026-01-30T20:10:00Z" } + ``` + + **Note:** Use this to get full details before resolving, showing exactly what data differs between sources. } diff --git a/bruno/conflicts/List Conflicts.bru b/bruno/conflicts/List Conflicts.bru index 829a9d2..b7c8d04 100644 --- a/bruno/conflicts/List Conflicts.bru +++ b/bruno/conflicts/List Conflicts.bru @@ -7,30 +7,88 @@ meta { get { url: {{baseUrl}}/api/conflicts?status=unresolved body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } -docs: { - Lists all sync conflicts for the authenticated user. - - Query Parameters: - - status: Filter by resolution status (unresolved, user_resolved, auto_resolved, all) - - Response includes: - - conflicts: Array of conflict details - - total: Total number of conflicts - - unresolved: Number of unresolved conflicts - - Each conflict includes: - - id: Conflict UUID - - media_item_id: Associated book UUID - - media_item_title: Book title - - conflict_type: Type of conflict (progress, note, highlight) - - conflict_data: Side-by-side comparison of conflicting data - - resolution_status: Current status - - created_at: When conflict was detected +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## List Conflicts + + Lists all sync conflicts for the authenticated user with optional filtering. + + **Method:** GET + + **Endpoint:** /api/conflicts + + **Authentication:** Bearer token + + **Query Parameters:** + - `status` (string, optional): Filter by resolution status + - `unresolved`: Only unresolved conflicts (default) + - `user_resolved`: Conflicts resolved by user + - `auto_resolved`: Automatically resolved conflicts + - `all`: All conflicts regardless of status + - `type` (string, optional): Filter by conflict type + - `progress`: Reading progress conflicts + - `note`: Bookmark/note conflicts + - `highlight`: Highlight conflicts + + **Response:** + - `conflicts` (array): Array of conflict objects + - `total` (number): Total number of conflicts matching filters + - `unresolved` (number): Number of unresolved conflicts + + **Each Conflict Object:** + - `id` (string): Conflict UUID + - `media_item_id` (string): Associated book UUID + - `media_item_title` (string): Book title + - `conflict_type` (string): Type of conflict (progress, note, highlight) + - `conflict_data` (object): Side-by-side comparison of conflicting data + - `koreader`: Data from KOReader device + - `kobo`: Data from Kobo device + - `web`: Data from web interface + - `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved) + - `created_at` (string): ISO 8601 timestamp when conflict was detected + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error + + **Example Request:** + ``` + GET /api/conflicts?status=unresolved&type=progress + ``` + + **Example Response:** + ```json + { + "conflicts": [ + { + "id": "conflict-uuid", + "media_item_id": "book-uuid", + "media_item_title": "Foundation", + "conflict_type": "progress", + "conflict_data": { + "koreader": { "percentage": 0.65, "epubcfi": "..." }, + "kobo": { "percentage": 0.43, "epubcfi": "..." } + }, + "resolution_status": "unresolved", + "created_at": "2026-01-31T12:00:00Z" + } + ], + "total": 1, + "unresolved": 1 + } + ``` + + **Note:** Conflicts occur when multiple devices update the same book data without syncing first. } diff --git a/bruno/conflicts/Resolve Conflict.bru b/bruno/conflicts/Resolve Conflict.bru index 292f35b..060a2d9 100644 --- a/bruno/conflicts/Resolve Conflict.bru +++ b/bruno/conflicts/Resolve Conflict.bru @@ -7,10 +7,10 @@ meta { post { url: {{baseUrl}}/api/conflicts/{{conflict_id}}/resolve body: json - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} Content-Type: application/json } @@ -24,25 +24,63 @@ body:json { } } -docs: { - Resolves a sync conflict by choosing which source to use. - - Path Parameters: - - conflict_id: UUID of the conflict to resolve - - Request Body: - - winner: Source to choose (koreader, kobo, web, manual) - - manual_data: Required if winner is "manual" - contains the merged data - - apply_to_all_future_conflicts: Whether to auto-resolve future conflicts from this source - - reason: Optional explanation for the resolution - - Example request body for choosing koreader: +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Resolve Conflict + + Resolves a sync conflict by choosing which source to use for the conflicting data. + + **Method:** POST + + **Endpoint:** /api/conflicts/{conflict_id}/resolve + + **Authentication:** Bearer token + + **Path Parameters:** + - `conflict_id` (string): Conflict UUID + + **Request Body:** + - `winner` (string): Source to choose + - `koreader`: Use KOReader device data + - `kobo`: Use Kobo device data + - `web`: Use web interface data + - `manual`: Use custom merged data (requires manual_data) + - `manual_data` (object, optional): Required if winner is "manual" + - `percentage` (number): Reading progress percentage (0-1) + - `epubcfi` (string): EPUB Canonical Fragment Identifier + - `chapter` (number, optional): Chapter number + - `page` (number, optional): Page number + - `apply_to_all_future_conflicts` (boolean): Auto-resolve future conflicts from this source + - `reason` (string, optional): Explanation for the resolution choice + + **Response:** + - `conflict_resolved` (boolean): True if successful + - `applied_to` (string): What was updated (progress, annotations, etc.) + - `devices_synced` (array): List of device IDs that were notified + + **Status Codes:** + - 200: Success - conflict resolved + - 400: Invalid request data + - 401: Unauthorized + - 404: Conflict not found + - 500: Internal server error + + **Example - Choose KOReader:** + ```json { "winner": "koreader", + "manual_data": null, + "apply_to_all_future_conflicts": false, "reason": "More recent progress" } - - Example request body for manual resolution: + ``` + + **Example - Manual Override:** + ```json { "winner": "manual", "manual_data": { @@ -50,17 +88,16 @@ docs: { "epubcfi": "epubcfi(/6/4/2:20)", "chapter": 3 }, + "apply_to_all_future_conflicts": false, "reason": "Custom merged position" } - - Response includes: - - conflict_resolved: true if successful - - applied_to: What was updated (progress, annotations) - - devices_synced: List of device IDs that were notified - - After resolution: - - The winning data is applied to the reading progress - - All connected devices are notified via WebSocket + ``` + + **After Resolution:** + - Winning data is applied to reading progress + - All connected devices notified via WebSocket - Conflict status changes to "user_resolved" - - Resolution data is stored for audit trail + - Resolution stored for audit trail + + **Note:** Manual override allows precise control when automatic resolution doesn't capture the correct state. } diff --git a/bruno/conflicts/api.bru b/bruno/conflicts/api.bru deleted file mode 100644 index ced8120..0000000 --- a/bruno/conflicts/api.bru +++ /dev/null @@ -1,127 +0,0 @@ -meta { - name: "Bookhoard Conflicts API" - type: "collection" - environment: { - development: { - base_url: "http://localhost:8765/api" - }, - production: { - base_url: "https://your-domain.com/api" - } - } -} - -# List Conflicts -@name("List Unresolved Conflicts") -GET {{environment.base_url}}/conflicts?status=unresolved -Authorization: Bearer {{jwt_token}} - -@name("List All Conflicts") -GET {{environment.base_url}}/conflicts?status=all -Authorization: Bearer {{jwt_token}} - -@name("List Resolved Conflicts") -GET {{environment.base_url}}/conflicts?status=resolved -Authorization: Bearer {{jwt_token}} - -@name("List Progress Conflicts") -GET {{environment.base_url}}/conflicts?status=unresolved&type=progress -Authorization: Bearer {{jwt_token}} - -@name("List Note Conflicts") -GET {{environment.base_url}}/conflicts?status=unresolved&type=note -Authorization: Bearer {{jwt_token}} - -@name("List Highlight Conflicts") -GET {{environment.base_url}}/conflicts?status=unresolved&type=highlight -Authorization: Bearer {{jwt_token}} - -# Get Conflict Details -@name("Get Conflict Details") -GET {{environment.base_url}}/conflicts/{{conflict_id}} -Authorization: Bearer {{jwt_token}} - -# Resolve Conflict -@name("Resolve - Keep KOReader Progress") -POST {{environment.base_url}}/conflicts/{{conflict_id}}/resolve -Authorization: Bearer {{jwt_token}} -Content-Type: application/json -{ - "winner": "koreader", - "apply_to_all_future_conflicts": false, - "reason": "KOReader was most recently updated" -} - -@name("Resolve - Keep Kobo Progress") -POST {{environment.base_url}}/conflicts/{{conflict_id}}/resolve -Authorization: Bearer {{jwt_token}} -Content-Type: application/json -{ - "winner": "kobo", - "apply_to_all_future_conflicts": false, - "reason": "Kobo has more progress" -} - -@name("Resolve - Keep Web Progress") -POST {{environment.base_url}}/conflicts/{{conflict_id}}/resolve -Authorization: Bearer {{jwt_token}} -Content-Type: application/json -{ - "winner": "web", - "apply_to_all_future_conflicts": false, - "reason": "Web interface has most recent data" -} - -@name("Resolve - Manual Override") -POST {{environment.base_url}}/conflicts/{{conflict_id}}/resolve -Authorization: Bearer {{jwt_token}} -Content-Type: application/json -{ - "winner": "manual", - "manual_data": { - "percentage": 0.43, - "epubcfi": "epubcfi(/6/4/2:20)", - "chapter": 3 - }, - "apply_to_all_future_conflicts": false, - "reason": "User specified custom progress" -} - -@name("Resolve - Manual Override with Page") -POST {{environment.base_url}}/conflicts/{{conflict_id}}/resolve -Authorization: Bearer {{jwt_token}} -Content-Type: application/json -{ - "winner": "manual", - "manual_data": { - "percentage": 0.43, - "page": 89, - "epubcfi": "epubcfi(/6/4/2:20)" - }, - "apply_to_all_future_conflicts": false, - "reason": "User specified page number" -} - -@name("Resolve - Apply to All Future Conflicts") -POST {{environment.base_url}}/conflicts/{{conflict_id}}/resolve -Authorization: Bearer {{jwt_token}} -Content-Type: application/json -{ - "winner": "koreader", - "apply_to_all_future_conflicts": true, - "reason": "Always prefer KOReader for future conflicts" -} - -# Delete Conflict -@name("Delete Conflict") -DELETE {{environment.base_url}}/conflicts/{{conflict_id}} -Authorization: Bearer {{jwt_token}} - -@name("Dismiss Conflict") -DELETE {{environment.base_url}}/conflicts/{{conflict_id}} -Authorization: Bearer {{jwt_token}} - -# Batch Operations -@name("Dismiss All Resolved Conflicts") -DELETE {{environment.base_url}}/conflicts/dismiss-resolved -Authorization: Bearer {{jwt_token}} diff --git a/bruno/devices/Add Books to Kobo Shelf.bru b/bruno/devices/Add Books to Kobo Shelf.bru index b88cbf1..71511b1 100644 --- a/bruno/devices/Add Books to Kobo Shelf.bru +++ b/bruno/devices/Add Books to Kobo Shelf.bru @@ -4,23 +4,62 @@ meta { seq: 1 } -POST {{baseURL}}/api/devices/{{deviceID}}/shelves -Authorization: Bearer {{userToken}} -Content-Type: application/json - -{ - "media_item_ids": [ - "{{bookUUID1}}", - "{{bookUUID2}}" - ], - "shelf_name": "Reading List", - "shelf_position": 0 +post { + url: {{baseURL}}/api/devices/{{deviceID}}/shelves + body: json + auth: inherit } -{ - "meta": { - "name": "Add Books to Kobo Shelf", - "description": "Add one or more books to a Kobo device shelf", - "documentation": "Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization." +headers { + Authorization: Bearer {{userToken}} + Content-Type: application/json +} + +body:json { + { + "media_item_ids": [ + "{{bookUUID1}}", + "{{bookUUID2}}" + ], + "shelf_name": "Reading List", + "shelf_position": 0 } } + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Add Books to Kobo Shelf + + Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization. + + **Method:** POST + + **Endpoint:** /api/devices/{deviceID}/shelves + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceID` (string): Device UUID + + **Request Body:** + - `media_item_ids` (array): Array of book UUIDs to add + - `shelf_name` (string): Name of the shelf (e.g., "Reading List", "Favorites") + - `shelf_position` (number, optional): Position on the shelf (default: 0) + + **Response:** + - `message` (string): Success message + - `added_count` (number): Number of books added + + **Status Codes:** + - 200: Success - books added to shelf + - 400: Invalid request data + - 401: Unauthorized + - 404: Device or one or more books not found + - 500: Internal server error + + **Note:** Adding a book that's already on the shelf updates its position if a new position is specified. Supports multiple shelves for organizing content on the Kobo device. +} diff --git a/bruno/devices/Approve Device Registration.bru b/bruno/devices/Approve Device Registration.bru index b9941c5..a47f591 100644 --- a/bruno/devices/Approve Device Registration.bru +++ b/bruno/devices/Approve Device Registration.bru @@ -7,36 +7,32 @@ meta { get { url: {{base_url}}/api/devices/approve/{{registration_id}} body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## Approve Device Registration - + Approves a pending device registration request. - + **Method:** GET - + **Endpoint:** /api/devices/approve/:registration_id - + **Authentication:** Bearer token - + **Path Parameters:** - `registration_id` (string): Registration request UUID - + **Response:** - Success message with approved device details - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Registration not found - 400: Invalid registration status - + **Example Response:** ```json { diff --git a/bruno/devices/Clear Kobo Shelf.bru b/bruno/devices/Clear Kobo Shelf.bru index 40eef92..eb70009 100644 --- a/bruno/devices/Clear Kobo Shelf.bru +++ b/bruno/devices/Clear Kobo Shelf.bru @@ -4,13 +4,47 @@ meta { seq: 1 } -DELETE {{baseURL}}/api/devices/{{deviceID}}/shelves/clear?shelf={{shelfName}} -Authorization: Bearer {{userToken}} - -{ - "meta": { - "name": "Clear Kobo Shelf", - "description": "Clear all books from a Kobo device shelf (or all shelves if no shelf name specified)", - "documentation": "Removes all books from the specified shelf. If no shelf name is provided, clears all shelves for the device." - } +delete { + url: {{baseURL}}/api/devices/{{deviceID}}/shelves/clear?shelf={{shelfName}} + body: none + auth: inherit +} + +headers { + Authorization: Bearer {{userToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Clear Kobo Shelf + + Clear all books from a Kobo device shelf, or all shelves if no shelf name is specified. + + **Method:** DELETE + + **Endpoint:** /api/devices/{deviceID}//shelves/clear?shelf={shelfName} + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceID` (string): Device UUID + + **Query Parameters:** + - `shelf` (string, optional): Shelf name to clear. If omitted, clears all shelves. + + **Response:** + - `message` (string): Success message + - `cleared_count` (number): Number of books removed from shelf + + **Status Codes:** + - 200: Success - shelf cleared + - 401: Unauthorized + - 404: Device not found + - 500: Internal server error + + **Note:** Removes all books from the specified shelf. If no shelf name is provided, clears all shelves for the device. This operation cannot be undone. } diff --git a/bruno/devices/Delete Device.bru b/bruno/devices/Delete Device.bru index 0ed175a..583f840 100644 --- a/bruno/devices/Delete Device.bru +++ b/bruno/devices/Delete Device.bru @@ -7,9 +7,39 @@ meta { delete { url: {{baseUrl}}/api/devices/{{deviceId}} body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Delete Device + + Deletes a device and unregisters it from the user's account. + + **Method:** DELETE + + **Endpoint:** /api/devices/{deviceId} + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceId` (string): Device UUID + + **Response:** 204 No Content on success + + **Status Codes:** + - 204: Success - device deleted + - 401: Unauthorized + - 404: Device not found + - 500: Internal server error + + **Note:** This action cannot be undone. All device data and sync history will be removed. +} diff --git a/bruno/devices/Get Device.bru b/bruno/devices/Get Device.bru index 9011b17..894565e 100644 --- a/bruno/devices/Get Device.bru +++ b/bruno/devices/Get Device.bru @@ -7,9 +7,44 @@ meta { get { url: {{baseUrl}}/api/devices/{{deviceId}} body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Device + + Retrieves detailed information about a specific device. + + **Method:** GET + + **Endpoint:** /api/devices/{deviceId} + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceId` (string): Device UUID + + **Response:** + - `id` (string): Device UUID + - `name` (string): Device name + - `device_type` (string): Type (kobo, koreader, web) + - `last_sync` (string): Last sync timestamp + - `is_active` (boolean): Whether device is active + - `created_at` (string): Registration timestamp + - `sync_settings` (object): Device-specific sync settings + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Device not found + - 500: Internal server error +} diff --git a/bruno/devices/Get Kobo Shelf.bru b/bruno/devices/Get Kobo Shelf.bru index 9579785..129dc87 100644 --- a/bruno/devices/Get Kobo Shelf.bru +++ b/bruno/devices/Get Kobo Shelf.bru @@ -1,16 +1,56 @@ meta { - name: Get Kobo Shelf + name: Get Kobo Shelf Books type: http seq: 1 } -GET {{baseURL}}/api/devices/{{deviceID}}/shelves?shelf={{shelfName}} -Authorization: Bearer {{userToken}} - -{ - "meta": { - "name": "Get Kobo Shelf Books", - "description": "Get all books on a Kobo device shelf, optionally filter by shelf name", - "documentation": "Returns a list of books with their shelf positions and Kobo-specific metadata (entitlement ID, revision number)." - } +get { + url: {{baseURL}}/api/devices/{{deviceID}}/shelves?shelf={{shelfName}} + body: none + auth: inherit +} + +headers { + Authorization: Bearer {{userToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Kobo Shelf Books + + Get all books on a Kobo device shelf, optionally filter by shelf name. + + **Method:** GET + + **Endpoint:** /api/devices/{deviceID}/shelves?shelf={shelfName} + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceID` (string): Device UUID + + **Query Parameters:** + - `shelf` (string, optional): Shelf name to filter by. If omitted, returns all shelves. + + **Response:** + - Array of books with: + - `id` (string): Book UUID + - `title` (string): Book title + - `author` (string): Book author + - `shelf_name` (string): Name of the shelf + - `shelf_position` (number): Position on the shelf + - `entitlement_id` (string): Kobo entitlement ID + - `revision_id` (string): Kobo revision number + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Device not found + - 500: Internal server error + + **Note:** Returns Kobo-specific metadata (entitlement ID, revision number) required for proper Kobo sync operations. } diff --git a/bruno/devices/List Devices.bru b/bruno/devices/List Devices.bru index 747328b..4d1fdef 100644 --- a/bruno/devices/List Devices.bru +++ b/bruno/devices/List Devices.bru @@ -7,9 +7,40 @@ meta { get { url: {{baseUrl}}/api/devices body: none - auth: bearer + auth: inherit } -headers: { +headers { Authorization: Bearer {{token}} } + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## List Devices + + Lists all devices registered to the authenticated user's account. + + **Method:** GET + + **Endpoint:** /api/devices + + **Authentication:** Bearer token + + **Response:** + - `devices` (array): Array of device objects + - `id` (string): Device UUID + - `name` (string): Device name + - `device_type` (string): Type (kobo, koreader, web) + - `last_sync` (string): Last sync timestamp + - `is_active` (boolean): Whether device is active + - `created_at` (string): Registration timestamp + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error +} diff --git a/bruno/devices/List Pending Registrations.bru b/bruno/devices/List Pending Registrations.bru index 3309249..ae63318 100644 --- a/bruno/devices/List Pending Registrations.bru +++ b/bruno/devices/List Pending Registrations.bru @@ -7,31 +7,27 @@ meta { get { url: {{base_url}}/api/devices/pending body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## List Pending Device Registrations - + Retrieves all pending device registration requests awaiting approval. - + **Method:** GET - + **Endpoint:** /api/devices/pending - + **Authentication:** Bearer token - + **Response:** - Array of pending device registrations - + **Status Codes:** - 200: Success - 401: Unauthorized - + **Example Response:** ```json [ diff --git a/bruno/devices/Reject Device Registration.bru b/bruno/devices/Reject Device Registration.bru index c66104a..2772e06 100644 --- a/bruno/devices/Reject Device Registration.bru +++ b/bruno/devices/Reject Device Registration.bru @@ -7,36 +7,32 @@ meta { post { url: {{base_url}}/api/devices/reject/{{registration_id}} body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## Reject Device Registration - + Rejects a pending device registration request. - + **Method:** POST - + **Endpoint:** /api/devices/reject/:registration_id - + **Authentication:** Bearer token - + **Path Parameters:** - `registration_id` (string): Registration request UUID - + **Response:** - Success message confirming rejection - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Registration not found - 400: Invalid registration status - + **Example Response:** ```json { diff --git a/bruno/devices/Remove Book from Kobo Shelf.bru b/bruno/devices/Remove Book from Kobo Shelf.bru index f3f9501..f3a6f16 100644 --- a/bruno/devices/Remove Book from Kobo Shelf.bru +++ b/bruno/devices/Remove Book from Kobo Shelf.bru @@ -4,13 +4,46 @@ meta { seq: 1 } -DELETE {{baseURL}}/api/devices/{{deviceID}}/shelves?media_item_id={{bookUUID}} -Authorization: Bearer {{userToken}} - -{ - "meta": { - "name": "Remove Book from Kobo Shelf", - "description": "Remove a specific book from a Kobo device shelf", - "documentation": "Removes the book from the device's shelf, preventing it from syncing to that device." - } +delete { + url: {{baseURL}}/api/devices/{{deviceID}}/shelves?media_item_id={{bookUUID}} + body: none + auth: inherit +} + +headers { + Authorization: Bearer {{userToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Remove Book from Kobo Shelf + + Remove a specific book from a Kobo device shelf, preventing it from syncing to that device. + + **Method:** DELETE + + **Endpoint:** /api/devices/{deviceID}/shelves?media_item_id={bookUUID} + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceID` (string): Device UUID + + **Query Parameters:** + - `media_item_id` (string): Book UUID to remove from shelf + + **Response:** + - `message` (string): Success message + + **Status Codes:** + - 200: Success - book removed from shelf + - 401: Unauthorized + - 404: Device or book not found + - 500: Internal server error + + **Note:** Removing a book from the device shelf prevents it from syncing to that device in future sync operations. } diff --git a/bruno/devices/Update Device.bru b/bruno/devices/Update Device.bru index 05ace83..0eade63 100644 --- a/bruno/devices/Update Device.bru +++ b/bruno/devices/Update Device.bru @@ -7,11 +7,7 @@ meta { put { url: {{baseUrl}}/api/devices/{{deviceId}} body: json - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } body:json { diff --git a/bruno/kobo/Kobo Initialization.bru b/bruno/kobo/Kobo Initialization.bru deleted file mode 100644 index 1b57e1e..0000000 --- a/bruno/kobo/Kobo Initialization.bru +++ /dev/null @@ -1,76 +0,0 @@ -meta { - name: Bookhoard Kobo Sync - type: collection - environment: Bookhoard -} - -### Kobo Initialization Endpoint - -GET {{baseURL}}/api/sync/kobo/v1/initialization -Authorization: Bearer {{koboToken}} -x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"} - -{ - "meta": { - "name": "Kobo Initialization", - "description": "Initialize Kobo sync by returning device resources and account page" - } -} - -### Kobo Library Sync Endpoint - -POST {{baseURL}}/api/sync/kobo/markup -Authorization: Bearer {{koboToken}} -x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"} - -{ - "ReadingSync": [ - { - "ContentId": "book-uuid-here", - "PercentRead": 45.6, - "EntitlementId": "entitlement-id", - "RemainingTimeMinutes": 120, - "LastModified": "2026-01-30T20:00:00Z" - } - ], - "BookmarkSync": [ - { - "BookmarkId": "bookmark-id-1", - "ContentId": "book-uuid-here", - "BookmarkText": "highlighted text here", - "BookmarkType": "annotation", - "BookmarkTitle": "Chapter 3" - } - ] -} - -### Kobo Bookmark Sync Endpoint - -POST {{baseURL}}/api/sync/kobo/bookmark -Authorization: Bearer {{koboToken}} -x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"} - -{ - "BookmarkSync": [ - { - "BookmarkId": "bookmark-id-2", - "ContentId": "book-uuid-here", - "BookmarkText": "This is my note abouts book", - "BookmarkType": "bookmark", - "BookmarkTitle": "Important note" - } - ] -} - -### Kobo Analytics Tests Endpoint - -POST {{baseURL}}/api/sync/kobo/v1/analytics/gettests -Authorization: Bearer {{koboToken}} -x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"} - -{ - "ContentId": "book-uuid-here", - "ReadingEvent": "Reading", - "RemainingTimeMin": 180, - "PercentRead": 67.8 -} diff --git a/bruno/kobo/Server Sync to Kobo.bru b/bruno/kobo/Server Sync to Kobo.bru index aed064c..177cddf 100644 --- a/bruno/kobo/Server Sync to Kobo.bru +++ b/bruno/kobo/Server Sync to Kobo.bru @@ -1,44 +1,90 @@ meta { - name: Server Sync to Kobo + name: Sync from Bookhoard to Kobo type: http seq: 1 } -POST {{baseURL}}/api/sync/kobo/sync-from-server -Authorization: Bearer {{koboToken}} -x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"} -Content-Type: application/json - -[ - { - "ContentId": "{{bookUUID}}", - "PercentRead": 65.4, - "LastModified": "2026-01-31T12:00:00Z", - "Bookmarks": [ - { - "BookmarkId": "bookmark-123", - "ContentId": "{{bookUUID}}", - "BookmarkText": "This is an important note", - "BookmarkType": "bookmark", - "BookmarkTitle": "Chapter 5 Note" - } - ], - "Highlights": [ - { - "BookmarkId": "highlight-456", - "ContentId": "{{bookUUID}}", - "BookmarkText": "highlighted passage text", - "BookmarkType": "annotation", - "BookmarkTitle": "Chapter 3 Highlight" - } - ] - } -] - -{ - "meta": { - "name": "Sync from Bookhoard to Kobo", - "description": "Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device", - "documentation": "Two-way sync endpoint. Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights." - } +post { + url: {{baseURL}}/api/sync/kobo/sync-from-server + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{koboToken}} + Content-Type: application/json + x-kobo-device: {"DeviceId":"kobo-clara-test","Model":"Kobo Clara","SerialNumber":"N123456789"} +} + +body:json { + [ + { + "ContentId": "{{bookUUID}}", + "PercentRead": 65.4, + "LastModified": "2026-01-31T12:00:00Z", + "Bookmarks": [ + { + "BookmarkId": "bookmark-123", + "ContentId": "{{bookUUID}}", + "BookmarkText": "This is an important note", + "BookmarkType": "bookmark", + "BookmarkTitle": "Chapter 5 Note" + } + ], + "Highlights": [ + { + "BookmarkId": "highlight-456", + "ContentId": "{{bookUUID}}", + "BookmarkText": "highlighted passage text", + "BookmarkType": "annotation", + "BookmarkTitle": "Chapter 3 Highlight" + } + ] + } + ] +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Sync from Bookhoard to Kobo + + Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint. + + **Method:** POST + + **Endpoint:** /api/sync/kobo/sync-from-server + + **Authentication:** Bearer token with Kobo device identification + + **Headers:** + - `x-kobo-device` (string): JSON string containing Kobo device info + - `DeviceId`: Kobo device ID + - `Model`: Kobo device model + - `SerialNumber`: Kobo device serial number + + **Request Body:** Array of sync data objects + - `ContentId` (string): Book UUID + - `PercentRead` (number): Reading progress percentage (0-100) + - `LastModified` (string): ISO 8601 timestamp + - `Bookmarks` (array, optional): Array of bookmark objects + - `BookmarkId`: Unique bookmark ID + - `ContentId`: Book UUID + - `BookmarkText`: Bookmark text/note + - `BookmarkType`: Type (bookmark, annotation, etc.) + - `BookmarkTitle`: Bookmark title + - `Highlights` (array, optional): Array of highlight objects (same structure as bookmarks) + + **Response:** + - Sync result confirmation + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error + + **Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights. } diff --git a/bruno/kobo/bookmark-sync.bru b/bruno/kobo/bookmark-sync.bru index 4f51cac..e48cf19 100644 --- a/bruno/kobo/bookmark-sync.bru +++ b/bruno/kobo/bookmark-sync.bru @@ -1,12 +1,22 @@ meta { - name: "Kobo Bookmark Sync - Enhanced with ContentId Mapping" + name: Kobo Bookmark Sync type: http seq: 3 } post { url: {{base_url}}/api/v1/kobo/bookmark - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +body:json { + { "BookmarkSync": [ { "BookmarkId": "bookmark_2", @@ -16,15 +26,50 @@ post { "DateCreated": "2026-01-31T12:00:00Z" } ] - }) - auth: { - type: bearer - bearer: {{device_token}} } } -assert { - response.status == 200 - response.body.Status == "Success" - response.body.BookmarksSynced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Status is Success', body.Status === "Success"); + tests('BookmarksSynced >= 0', body.BookmarksSynced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Kobo Bookmark Sync + + Synchronizes bookmarks from a Kobo device to the Bookhoard server. + + **Method:** POST + + **Endpoint:** /api/v1/kobo/bookmark + + **Authentication:** Bearer token (device token) + + **Request Body:** + - `BookmarkSync` (array): Array of bookmark objects + - `BookmarkId` (string): Unique bookmark ID + - `ContentId` (string): Book/content ID + - `BookmarkText` (string): Bookmark text or note + - `BookmarkType` (string): Type (bookmark, highlight, note) + - `DateCreated` (string): ISO 8601 timestamp + + **Response:** + - `Status` (string): Sync status (Success, Partial) + - `BookmarksSynced` (number): Number of bookmarks synced + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/kobo/initialization.bru b/bruno/kobo/initialization.bru index cfe1487..f68c7f9 100644 --- a/bruno/kobo/initialization.bru +++ b/bruno/kobo/initialization.bru @@ -1,5 +1,5 @@ meta { - name: "Kobo Initialization - Enhanced with ContentId Mapping" + name: Kobo Initialization type: http seq: 1 } @@ -7,15 +7,49 @@ meta { get { url: {{base_url}}/api/v1/kobo/initialization body: none - auth: { - type: bearer - bearer: {{device_token}} - } + auth: inherit } -assert { - response.status == 200 - response.body.ContentId exists() - response.body.Categories exists() - response.body.BookhoardUUID exists() +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Has ContentId', body.ContentId !== undefined); + tests('Has Categories', body.Categories !== undefined); + tests('Has BookhoardUUID', body.BookhoardUUID !== undefined); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Kobo Initialization + + Initializes Kobo device sync, returning device resources and account information. + + **Method:** GET + + **Endpoint:** /api/v1/kobo/initialization + + **Authentication:** Bearer token (device token) + + **Response:** + - `ContentId` (string): Device content ID + - `Categories` (array): Available categories/collections + - `BookhoardUUID` (string): Bookhoard instance UUID + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/kobo/markup-sync.bru b/bruno/kobo/markup-sync.bru index 63c5e2d..34ebc2f 100644 --- a/bruno/kobo/markup-sync.bru +++ b/bruno/kobo/markup-sync.bru @@ -1,12 +1,22 @@ meta { - name: "Kobo Markup Sync - Enhanced with ContentId Mapping" + name: Kobo Markup Sync type: http seq: 2 } post { url: {{base_url}}/api/v1/kobo/markup - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +body:json { + { "ReadingSync": [ { "ContentId": "kobo_abc123def456", @@ -26,16 +36,60 @@ post { } ], "Metadata": true - }) - auth: { - type: bearer - bearer: {{device_token}} } } -assert { - response.status == 200 - response.body.Status == "Success" || response.body.Status == "Partial" - response.body.MarkupsSynced >= 0 - response.body.BookmarksSynced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + const validStatus = body.Status === "Success" || body.Status === "Partial"; + tests('Status is Success or Partial', validStatus); + tests('MarkupsSynced >= 0', body.MarkupsSynced >= 0); + tests('BookmarksSynced >= 0', body.BookmarksSynced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Kobo Markup Sync + + Synchronizes reading progress and markup (highlights, bookmarks) from a Kobo device. + + **Method:** POST + + **Endpoint:** /api/v1/kobo/markup + + **Authentication:** Bearer token (device token) + + **Request Body:** + - `ReadingSync` (array, optional): Reading progress data + - `ContentId` (string): Book/content ID + - `PercentRead` (number): Percentage read (0-100) + - `RemainingTimeMin` (number): Remaining time in minutes + - `ReadingEvent` (string): Event type (BookRead, etc.) + - `LastModified` (string): ISO 8601 timestamp + - `BookmarkSync` (array, optional): Bookmark/highlight data + - `BookmarkId` (string): Unique bookmark ID + - `ContentId` (string): Book/content ID + - `BookmarkText` (string): Highlighted/bookmarked text + - `BookmarkType` (string): Type (annotation, bookmark) + - `DateCreated` (string): ISO 8601 timestamp + - `Metadata` (boolean): Whether to include metadata + + **Response:** + - `Status` (string): Sync status (Success, Partial) + - `MarkupsSynced` (number): Number of markups synced + - `BookmarksSynced` (number): Number of bookmarks synced + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/koreader/Get Book Metadata.bru b/bruno/koreader/Get Book Metadata.bru index 9f6b6cf..a693cea 100644 --- a/bruno/koreader/Get Book Metadata.bru +++ b/bruno/koreader/Get Book Metadata.bru @@ -6,15 +6,56 @@ meta { get { url: {{baseUrl}}/api/sync/koreader/metadata/{{book_uuid}} - headers: { - Authorization: Bearer {{device_token}}, - Content-Type: application/json - } + body: none + auth: inherit } -assert { - res.status == 200 - res.body.uuid != null - res.body.title != null - res.body.progress != null +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Status is 200'] = true; + tests['Has UUID'] = body.uuid !== null; + tests['Has title'] = body.title !== null; + tests['Has progress'] = body.progress !== null; + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Get Book Metadata + + Retrieves metadata for a specific book from the KOReader sync endpoint. + + **Method:** GET + + **Endpoint:** /api/sync/koreader/metadata/{book_uuid} + + **Authentication:** Bearer token (device token) + + **Path Parameters:** + - `book_uuid` (string): Book UUID + + **Response:** + - `uuid` (string): Book UUID + - `title` (string): Book title + - `progress` (object): Reading progress data + - `metadata` (object): Additional book metadata + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Book not found + - 500: Internal server error } diff --git a/bruno/koreader/Get Library.bru b/bruno/koreader/Get Library.bru index 6d37926..e6d47a8 100644 --- a/bruno/koreader/Get Library.bru +++ b/bruno/koreader/Get Library.bru @@ -6,14 +6,50 @@ meta { get { url: {{baseUrl}}/api/sync/koreader/library - headers: { - Authorization: Bearer {{device_token}}, - Content-Type: application/json - } + body: none + auth: inherit } -assert { - res.status == 200 - res.body.library_sync != null - res.body.total_books >= 0 +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Status is 200'] = res.getStatus() === 200; + tests['Has library_sync'] = body.library_sync != null; + tests('Total books >= 0', body.total_books >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Get Library + + Retrieves the user's library for KOReader sync operations. + + **Method:** GET + + **Endpoint:** /api/sync/koreader/library + + **Authentication:** Bearer token (device token) + + **Response:** + - `library_sync` (object): Library sync data + - `total_books` (number): Total number of books + - `books` (array): Array of book objects + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru b/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru index 0535b72..2238301 100644 --- a/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru +++ b/bruno/koreader/Sync Annotations (Per-Book SHA-256).bru @@ -1,12 +1,22 @@ meta { - name: "KOReader Sync Annotations - Per-Annotation SHA-256" + name: KOReader Sync Annotations - Per-Book SHA-256 type: http seq: 4 } post { url: {{base_url}}/api/v1/koreader/sync/bookmarks - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{koreader_device_token}} + Content-Type: application/json +} + +body:json { + { "book_uuid": "{{book_uuid}}", "highlights": [ { @@ -26,14 +36,51 @@ post { "book_sha256": "{{another_book_sha256}}" } ] - }) - auth: { - type: bearer - bearer: {{koreader_device_token}} } } -assert { - response.status == 200 - response.body.highlights_synced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Highlights synced >= 0', body.highlights_synced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Sync Annotations - Per-Book SHA-256 + + Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync. + + **Method:** POST + + **Endpoint:** /api/v1/koreader/sync/bookmarks + + **Authentication:** Bearer token (KOReader device token) + + **Request Body:** + - `book_uuid` (string): Primary book UUID + - `highlights` (array): Array of highlight objects + - `text` (string): Highlighted text + - `pos0`, `pos1` (string): EPUB CFI positions + - `color` (string): Highlight color (hex) + - `page` (number): Page number + - `book_sha256` (string): SHA-256 hash for this specific book + + **Response:** + - `highlights_synced` (number): Number of highlights synced + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error + + **Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request. } diff --git a/bruno/koreader/Sync Bookmarks (SHA-256).bru b/bruno/koreader/Sync Bookmarks (SHA-256).bru index f6f7982..174c34c 100644 --- a/bruno/koreader/Sync Bookmarks (SHA-256).bru +++ b/bruno/koreader/Sync Bookmarks (SHA-256).bru @@ -1,12 +1,22 @@ meta { - name: "KOReader Sync Bookmarks - Enhanced with SHA-256" + name: KOReader Sync Bookmarks - SHA-256 type: http seq: 3 } post { url: {{base_url}}/api/v1/koreader/sync/bookmarks - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{koreader_device_token}} + Content-Type: application/json +} + +body:json { + { "book_sha256": "{{book_sha256}}", "bookmarks": [ { @@ -33,15 +43,48 @@ post { "page": 50 } ] - }) - auth: { - type: bearer - bearer: {{koreader_device_token}} } } -assert { - response.status == 200 - response.body.sync_status == "completed" - response.body.total_synced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Sync completed', body.sync_status === "completed"); + tests('Total synced >= 0', body.total_synced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Sync Bookmarks - SHA-256 + + Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification. + + **Method:** POST + + **Endpoint:** /api/v1/koreader/sync/bookmarks + + **Authentication:** Bearer token (KOReader device token) + + **Request Body:** + - `book_sha256` (string): SHA-256 hash of book file + - `bookmarks` (array): Array of bookmarks + - `notes` (array): Array of notes + - `highlights` (array): Array of highlights + + **Response:** + - `sync_status` (string): Sync status + - `total_synced` (number): Total items synced + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/koreader/Sync Bookmarks.bru b/bruno/koreader/Sync Bookmarks.bru index 4c439a6..7feb6a4 100644 --- a/bruno/koreader/Sync Bookmarks.bru +++ b/bruno/koreader/Sync Bookmarks.bru @@ -6,7 +6,17 @@ meta { post { url: {{baseUrl}}/api/sync/koreader/bookmarks - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +body:json { + { "book_uuid": "{{book_uuid}}", "bookmarks": [ { @@ -46,15 +56,57 @@ post { "percentage": 0.45 } ] - }) - headers: { - Authorization: Bearer {{device_token}}, - Content-Type: application/json } } -assert { - res.status == 200 - res.body.sync_status == "completed" - res.body.total_synced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Sync completed'] = body.sync_status === "completed"; + tests('Items synced >= 0', body.total_synced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Sync Bookmarks + + Synchronizes bookmarks, notes, and highlights from a KOReader device. + + **Method:** POST + + **Endpoint:** /api/sync/koreader/bookmarks + + **Authentication:** Bearer token (device token) + + **Request Body:** + - `book_uuid` (string): Book UUID + - `bookmarks` (array): Array of bookmark objects + - `notes` (array): Array of note objects + - `highlights` (array): Array of highlight objects + + Each object includes: + - `chapter` (number): Chapter number + - `datetime` (string): ISO 8601 timestamp + - `text` (string): Highlighted/bookmarked text + - `pos0`, `pos1` (string): EPUB CFI positions + - `page` (number): Page number + - `type` (string): Type (highlight, bookmark, note) + - `percentage` (number): Position in book (0-1) + + **Response:** + - `sync_status` (string): Sync status (completed, partial) + - `total_synced` (number): Number of items synced + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/koreader/Sync Progress (SHA-256 Only).bru b/bruno/koreader/Sync Progress (SHA-256 Only).bru index 39c4467..0e36f7f 100644 --- a/bruno/koreader/Sync Progress (SHA-256 Only).bru +++ b/bruno/koreader/Sync Progress (SHA-256 Only).bru @@ -1,12 +1,22 @@ meta { - name: "KOReader Sync Progress - Priority Matching (SHA-256 only)" + name: KOReader Sync Progress - SHA-256 Only type: http seq: 2 } post { url: {{base_url}}/api/v1/koreader/sync/progress - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{koreader_device_token}} + Content-Type: application/json +} + +body:json { + { "sync_mode": "immediate", "books": [ { @@ -17,13 +27,50 @@ post { "total_pages": 200 } ] - }) - auth: { - type: bearer - bearer: {{koreader_device_token}} } } -assert { - response.status == 200 || response.status == 202 +script:post-response { + function onResponse(res) { + tests('Status is 200 or 202', res.getStatus() === 200 || res.getStatus() === 202); + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Sync Progress - SHA-256 Only + + Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available). + + **Method:** POST + + **Endpoint:** /api/v1/koreader/sync/progress + + **Authentication:** Bearer token (KOReader device token) + + **Request Body:** + - `sync_mode` (string): Sync mode (immediate, deferred) + - `books` (array): Array of book progress objects + - `sha256` (string): SHA-256 hash of book file + - `file_path` (string): Path to book file + - `percentage` (number): Progress percentage + - `page` (number): Current page + - `total_pages` (number): Total pages + + **Response:** + - `sync_status` (string): Sync status + - `books_synced` (number): Number of books synced + + **Status Codes:** + - 200: Success + - 202: Accepted + - 401: Unauthorized + - 500: Internal server error + + **Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification. } diff --git a/bruno/koreader/Sync Progress (SHA-256).bru b/bruno/koreader/Sync Progress (SHA-256).bru index ec06a75..d847d31 100644 --- a/bruno/koreader/Sync Progress (SHA-256).bru +++ b/bruno/koreader/Sync Progress (SHA-256).bru @@ -1,12 +1,22 @@ meta { - name: "KOReader Sync Progress - Enhanced with SHA-256" + name: KOReader Sync Progress - SHA-256 type: http seq: 1 } post { url: {{base_url}}/api/v1/koreader/sync/progress - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{koreader_device_token}} + Content-Type: application/json +} + +body:json { + { "sync_mode": "immediate", "books": [ { @@ -23,15 +33,59 @@ post { "authors": ["J.R.R. Tolkien"] } ] - }) - auth: { - type: bearer - bearer: {{koreader_device_token}} } } -assert { - response.status == 200 || response.status == 202 - response.body.sync_status exists() - response.body.books_synced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200 || res.getStatus() === 202) { + const body = res.getBody(); + tests['Status accepted'] = res.getStatus() === 200 || res.getStatus() === 202; + tests('Has sync_status', body.sync_status !== undefined); + tests('Books synced >= 0', body.books_synced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Sync Progress - SHA-256 + + Synchronizes reading progress from a KOReader device using SHA-256 book hash for identification. + + **Method:** POST + + **Endpoint:** /api/v1/koreader/sync/progress + + **Authentication:** Bearer token (KOReader device token) + + **Request Body:** + - `sync_mode` (string): Sync mode (immediate, deferred) + - `books` (array): Array of book progress objects + - `uuid` (string): Book UUID + - `sha256` (string): SHA-256 hash of book file for identification + - `file_path` (string): Path to book file on device + - `percentage` (number): Progress percentage (0-1) + - `chapter` (number): Current chapter + - `page` (number): Current page + - `total_pages` (number): Total pages + - `epubcfi` (string): EPUB location + - `last_read` (string): ISO 8601 timestamp + - `title` (string): Book title + - `authors` (array): List of authors + + **Response:** + - `sync_status` (string): Sync status + - `books_synced` (number): Number of books synced + + **Status Codes:** + - 200: Success + - 202: Accepted - processing + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/koreader/Sync Progress.bru b/bruno/koreader/Sync Progress.bru index f8b0738..81f7992 100644 --- a/bruno/koreader/Sync Progress.bru +++ b/bruno/koreader/Sync Progress.bru @@ -6,7 +6,17 @@ meta { post { url: {{baseUrl}}/api/sync/koreader/progress - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{device_token}} + Content-Type: application/json +} + +body:json { + { "library_id": null, "books": [ { @@ -28,15 +38,61 @@ post { "koreader_version": "2024.01", "device_model": "kindle-paperwhite-5" } - }) - headers: { - Authorization: Bearer {{device_token}}, - Content-Type: application/json } } -assert { - res.status == 202 - res.body.sync_status == "accepted" - res.body.books_synced >= 0 +script:post-response { + function onResponse(res) { + if (res.getStatus() === 202) { + const body = res.getBody(); + tests['Status is 202'] = res.getStatus() === 202; + tests['Sync status accepted'] = body.sync_status === "accepted"; + tests('Books synced >= 0', body.books_synced >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## KOReader Sync Progress + + Synchronizes reading progress from a KOReader device to the Bookhoard server. + + **Method:** POST + + **Endpoint:** /api/sync/koreader/progress + + **Authentication:** Bearer token (device token) + + **Request Body:** + - `library_id` (string, optional): Library UUID + - `books` (array): Array of book progress objects + - `uuid` (string): Book UUID + - `title` (string): Book title + - `authors` (array): List of authors + - `progress` (number): Progress value + - `percentage` (number): Percentage complete (0-1) + - `last_read` (string): ISO 8601 timestamp + - `chapter` (number): Current chapter + - `epubcfi` (string): EPUB Canonical Fragment Identifier + - `page` (number): Current page + - `total_pages` (number): Total pages + - `sync_mode` (string): Sync mode (immediate, deferred) + - `device_info` (object): Device information + - `koreader_version` (string): KOReader version + - `device_model` (string): Device model identifier + + **Response:** + - `sync_status` (string): Sync status (accepted, processing) + - `books_synced` (number): Number of books synced + + **Status Codes:** + - 202: Accepted - sync queued + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/media-items/EPUB Download.bru b/bruno/media-items/EPUB Download.bru index e87417e..c97924f 100644 --- a/bruno/media-items/EPUB Download.bru +++ b/bruno/media-items/EPUB Download.bru @@ -1,15 +1,42 @@ meta { - name: EPUB Download + name: Download EPUB Book type: http seq: 1 } -GET {{baseURL}}/api/books/{{bookUUID}}/download - -{ - "meta": { - "name": "Download EPUB Book", - "description": "Download an EPUB book file from Bookhoard server (no auth required - for Kobo device download)", - "documentation": "Kobo devices can download books directly from Bookhoard using this endpoint. The endpoint returns the book file with appropriate Content-Type headers." - } +get { + url: {{baseURL}}/api/books/{{bookUUID}}/download + body: none + auth: none +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Download EPUB Book + + Download an EPUB book file from Bookhoard server. No authentication required for Kobo device downloads. + + **Method:** GET + + **Endpoint:** /api/books/{bookUUID}/download + + **Authentication:** None (for Kobo device downloads) + + **Path Parameters:** + - `bookUUID` (string): Book UUID + + **Response:** Binary EPUB file data + + **Response Headers:** + - `Content-Type`: application/epub+zip + + **Status Codes:** + - 200: Success - EPUB file returned + - 404: Book not found + + **Note:** This endpoint is designed for Kobo devices to download books directly from Bookhoard. The endpoint returns the book file with appropriate Content-Type headers. } diff --git a/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru b/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru index 4dc67fe..bf24083 100644 --- a/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru +++ b/bruno/opds/Download Book KEPUB (On-the-fly Conversion).bru @@ -1,35 +1,71 @@ -{ - "meta": { - "name": "Download Book KEPUB (On-the-fly Conversion)", - "type": "http", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Test format-specific hash header", - "const kepubHash = resp.headers.get('X-Bookhoard-KEPUB-SHA256');", - "if (kepubHash) {", - " tests['KEPUB hash present'] = true;", - " tests['Hash is 64 chars'] = kepubHash.length === 64;", - "} else {", - " tests['KEPUB hash present'] = false;", - "}", - "", - "// Verify Bookhoard UUID header", - "const bookhoardUUID = resp.headers.get('X-Bookhoard-UUID');", - "tests['Bookhoard UUID present'] = bookhoardUUID !== null;", - "", - "// Verify content type", - "const contentType = resp.headers.get('Content-Type');", - "tests['Content-Type is KEPUB'] = contentType && contentType.includes('kepub');" - ] - } - } - ] - }, - "req": { - "url": "{{baseUrl}}/opds/devices/{{deviceId}}/download/{{mediaItemId}}?format=kepub", - "method": "GET" +meta { + name: Download Book KEPUB (On-the-fly Conversion) + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/opds/devices/{{deviceId}}/download/{{mediaItemId}}?format=kepub + body: none + auth: inherit +} + +script:post-response { + function onResponse(res) { + const headers = res.getHeaders(); + + const kepubHash = headers.get('X-Bookhoard-KEPUB-SHA256'); + if (kepubHash) { + tests['KEPUB hash present'] = true; + tests['Hash is 64 chars'] = kepubHash.length === 64; + } else { + tests['KEPUB hash present'] = false; + } + + const bookhoardUUID = headers.get('X-Bookhoard-UUID'); + tests['Bookhoard UUID present'] = bookhoardUUID !== null; + + const contentType = headers.get('Content-Type'); + tests['Content-Type is KEPUB'] = contentType && contentType.includes('kepub'); } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Download Book KEPUB (On-the-fly Conversion) + + Downloads a book in Kobo EPUB (KEPUB) format with on-the-fly conversion if needed. + + **Method:** GET + + **Endpoint:** /opds/devices/{deviceId}/download/{mediaItemId}?format=kepub + + **Authentication:** Bearer token + + **Path Parameters:** + - `deviceId` (string): Device UUID + - `mediaItemId` (string): Media item UUID + + **Query Parameters:** + - `format` (string): Must be "kepub" + + **Response Headers:** + - `X-Bookhoard-KEPUB-SHA256` (string): SHA-256 hash of the KEPUB file (64 chars) + - `X-Bookhoard-UUID` (string): Bookhoard UUID for the media item + - `Content-Type` (string): Will be "application/kepub+json" or similar + + **Response Body:** Binary KEPUB file data + + **Status Codes:** + - 200: Success - KEPUB file returned + - 401: Unauthorized + - 404: Media item or device not found + - 500: Internal server error or conversion failure + + **Note:** The KEPUB format adds special Kobo-specific markup to enhance reading features on Kobo devices. The file is converted on-the-fly if the source is not already KEPUB. } diff --git a/bruno/queue/Clear All Queue Items.bru b/bruno/queue/Clear All Queue Items.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno/queue/Clear Device Queue.bru b/bruno/queue/Clear Device Queue.bru index df1f91d..88026e8 100644 --- a/bruno/queue/Clear Device Queue.bru +++ b/bruno/queue/Clear Device Queue.bru @@ -7,35 +7,31 @@ meta { delete { url: {{base_url}}/api/queue/devices/{{device_id}}/clear body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## Clear Device Queue - + Clears all queue items for a specific device. - + **Method:** DELETE - + **Endpoint:** /api/queue/devices/:device_id/clear - + **Authentication:** Bearer token - + **Path Parameters:** - `device_id` (string): Device UUID - + **Response:** - Success message with count of cleared items - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Device not found - + **Example Response:** ```json { diff --git a/bruno/queue/Delete Queue Item.bru b/bruno/queue/Delete Queue Item.bru index 5af7ae7..c8144fb 100644 --- a/bruno/queue/Delete Queue Item.bru +++ b/bruno/queue/Delete Queue Item.bru @@ -7,35 +7,31 @@ meta { delete { url: {{base_url}}/api/queue/items/{{item_id}} body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## Delete Queue Item - + Deletes a queue item from the sync queue. - + **Method:** DELETE - + **Endpoint:** /api/queue/items/:item_id - + **Authentication:** Bearer token - + **Path Parameters:** - `item_id` (string): Queue item UUID - + **Response:** - Success message confirming deletion - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Queue item not found - + **Example Response:** ```json { diff --git a/bruno/queue/Filter by Status - Pending.bru b/bruno/queue/Filter by Status - Pending.bru new file mode 100644 index 0000000..5e51d95 --- /dev/null +++ b/bruno/queue/Filter by Status - Pending.bru @@ -0,0 +1,71 @@ +meta { + name: Filter by Status - Pending + type: http + seq: 3 +} + +docs { + Filter queue items by status - show only pending items. + + **Endpoint**: GET /queue/items?status=pending + **Auth**: Required (Bearer token) + + ## Query Parameters + + | Parameter | Type | Required | Description | + |-----------|------|-----------|-------------| + | status | string | Yes | Status filter: pending, processing, completed, failed | + + ## Response Fields + + | Field | Type | Description | + |-------|------|-------------| + | items | array | List of queue items with pending status | + | total | int | Total matching items | + + ## Example Request + + ``` + GET /queue/items?status=pending + ``` + + ## Example Response + + ```json + { + "items": [...], + "total": 15 + } + ``` + + ## Error Responses + + | Code | Description | + |------|-------------| + | 401 | Unauthorized | + | 500 | Internal server error | + + ## Notes + + - Only returns items with the specified status +} + +get { + url: {{base_url}}/queue/items?status=pending + body: none + auth: inherit +} + + token: {{jwt_token}} +} + +tests { + test("status must be 200", function() { + expect(res.status).to.eql(200); + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/queue/Get Device Queue Items.bru b/bruno/queue/Get Device Queue Items.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno/queue/Get Device Queue Stats.bru b/bruno/queue/Get Device Queue Stats.bru index 1b561fe..c9da7f9 100644 --- a/bruno/queue/Get Device Queue Stats.bru +++ b/bruno/queue/Get Device Queue Stats.bru @@ -7,35 +7,31 @@ meta { get { url: {{base_url}}/api/queue/devices/{{device_id}}/stats body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## Get Device Queue Stats - + Retrieves statistics for a specific device's sync queue. - + **Method:** GET - + **Endpoint:** /api/queue/devices/:device_id/stats - + **Authentication:** Bearer token - + **Path Parameters:** - `device_id` (string): Device UUID - + **Response:** - Queue statistics including pending, completed, and failed counts - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Device not found - + **Example Response:** ```json { diff --git a/bruno/queue/Get Queue Statistics.bru b/bruno/queue/Get Queue Statistics.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno/queue/List All Queue Items (Admin).bru b/bruno/queue/List All Queue Items (Admin).bru index c5f6ad3..2b1f1d4 100644 --- a/bruno/queue/List All Queue Items (Admin).bru +++ b/bruno/queue/List All Queue Items (Admin).bru @@ -7,32 +7,28 @@ meta { get { url: {{base_url}}/api/queue/items body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## List All Queue Items (Admin) - + Retrieves all queue items across all devices (admin only). - + **Method:** GET - + **Endpoint:** /api/queue/items - + **Authentication:** Bearer token (admin role required) - + **Response:** - Array of queue items with device and status information - + **Status Codes:** - 200: Success - 401: Unauthorized - 403: Forbidden - admin role required - + **Example Response:** ```json [ diff --git a/bruno/queue/List All Queue Items.bru b/bruno/queue/List All Queue Items.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno/queue/List Device Queue Items.bru b/bruno/queue/List Device Queue Items.bru index e7012bf..b2f7cfa 100644 --- a/bruno/queue/List Device Queue Items.bru +++ b/bruno/queue/List Device Queue Items.bru @@ -7,35 +7,31 @@ meta { get { url: {{base_url}}/api/queue/devices/{{device_id}}/items body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## List Device Queue Items - + Retrieves all queue items for a specific device. - + **Method:** GET - + **Endpoint:** /api/queue/devices/:device_id/items - + **Authentication:** Bearer token - + **Path Parameters:** - `device_id` (string): Device UUID - + **Response:** - Array of queue items for the specified device - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Device not found - + **Example Response:** ```json [ diff --git a/bruno/queue/List Queue Items (Pagination).bru b/bruno/queue/List Queue Items (Pagination).bru new file mode 100644 index 0000000..18f7473 --- /dev/null +++ b/bruno/queue/List Queue Items (Pagination).bru @@ -0,0 +1,65 @@ +meta { + name: List Queue Items (With Pagination) + type: http + seq: 2 +} + +docs { + List items in the sync queue with pagination. + + **Endpoint**: GET /queue/items + **Auth**: Required (Bearer token) + + ## Query Parameters + + | Parameter | Type | Required | Description | + |-----------|------|-----------|-------------| + | limit | int | No | Items per page | + | offset | int | No | Pagination offset | + + ## Response Fields + + | Field | Type | Description | + |-------|------|-------------| + | items | array | List of queue items | + | total | int | Total number of items | + | page | int | Current page number | + | per_page | int | Items per page | + + ## Example Request + + ``` + GET /queue/items?limit=50&offset=0 + ``` + + ## Error Responses + + | Code | Description | + |------|-------------| + | 401 | Unauthorized | + | 500 | Internal server error | + + ## Notes + + - Supports pagination for large queue lists +} + +get { + url: {{base_url}}/queue/items?limit=50&offset=0 + body: none + auth: inherit +} + + token: {{jwt_token}} +} + +tests { + test("status must be 200", function() { + expect(res.status).to.eql(200); + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/queue/Process Queue Item.bru b/bruno/queue/Process Queue Item.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno/queue/Retry Queue Item.bru b/bruno/queue/Retry Queue Item.bru index a6831bc..6535840 100644 --- a/bruno/queue/Retry Queue Item.bru +++ b/bruno/queue/Retry Queue Item.bru @@ -7,36 +7,32 @@ meta { post { url: {{base_url}}/api/queue/items/{{item_id}}/retry body: none - auth: bearer -} - -headers: { - Authorization: Bearer {{token}} + auth: inherit } docs { ## Retry Queue Item - + Retries a failed queue item. - + **Method:** POST - + **Endpoint:** /api/queue/items/:item_id/retry - + **Authentication:** Bearer token - + **Path Parameters:** - `item_id` (string): Queue item UUID - + **Response:** - Success message indicating retry initiated - + **Status Codes:** - 200: Success - 401: Unauthorized - 404: Queue item not found - 400: Invalid item status - + **Example Response:** ```json { diff --git a/bruno/sidecar/Download Device Sidecar File.bru b/bruno/sidecar/Download Device Sidecar File.bru index 1cac717..c8db7eb 100644 --- a/bruno/sidecar/Download Device Sidecar File.bru +++ b/bruno/sidecar/Download Device Sidecar File.bru @@ -1,19 +1,61 @@ meta { - name: "Download Device Sidecar File" + name: Download Device Sidecar File type: http seq: 2 } get { url: {{base_url}}/api/devices/{{device_id}}/sidecar/download - auth: { - type: bearer - bearer: {{user_token}} - } + body: none + auth: inherit } -assert { - response.status == 200 - response.headers["Content-Type"] contains "application/json" - response.headers["Content-Disposition"] contains ".bookhoard.json" +headers { + Authorization: Bearer {{user_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const headers = res.getHeaders(); + const contentType = headers.get("Content-Type"); + const contentDisposition = headers.get("Content-Disposition"); + tests('Content-Type is JSON', contentType && contentType.includes("application/json")); + tests('Has .bookhoard.json filename', contentDisposition && contentDisposition.includes(".bookhoard.json")); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Download Device Sidecar File + + Downloads the sidecar configuration file for a device in JSON format. + + **Method:** GET + + **Endpoint:** /api/devices/{device_id}/sidecar/download + + **Authentication:** Bearer token + + **Path Parameters:** + - `device_id` (string): Device UUID + + **Response Headers:** + - `Content-Type`: application/json + - `Content-Disposition`: attachment; filename="device.bookhoard.json" + + **Response Body:** JSON sidecar configuration file + + **Status Codes:** + - 200: Success - file returned + - 401: Unauthorized + - 404: Device not found + - 500: Internal server error } diff --git a/bruno/sidecar/Get Device Sidecar Config.bru b/bruno/sidecar/Get Device Sidecar Config.bru index 4d9a8f5..5adaa15 100644 --- a/bruno/sidecar/Get Device Sidecar Config.bru +++ b/bruno/sidecar/Get Device Sidecar Config.bru @@ -1,21 +1,61 @@ meta { - name: "Get Device Sidecar Config" + name: Get Device Sidecar Config type: http seq: 1 } get { url: {{base_url}}/api/devices/{{device_id}}/sidecar - auth: { - type: bearer - bearer: {{user_token}} - } + body: none + auth: inherit } -assert { - response.status == 200 - response.body.version == "1.0" - response.body.bookhoard exists() - response.body.books exists() - response.body.collections exists() +headers { + Authorization: Bearer {{user_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Version is 1.0', body.version === "1.0"); + tests('Has bookhoard config', body.bookhoard !== undefined); + tests('Has books array', body.books !== undefined); + tests('Has collections array', body.collections !== undefined); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Device Sidecar Config + + Retrieves sidecar configuration for a specific device. + + **Method:** GET + + **Endpoint:** /api/devices/{device_id}/sidecar + + **Authentication:** Bearer token + + **Path Parameters:** + - `device_id` (string): Device UUID + + **Response:** + - `version` (string): Sidecar version (e.g., "1.0") + - `bookhoard` (object): Bookhoard configuration + - `books` (array): Array of book configurations + - `collections` (array): Array of collection configurations + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Device not found + - 500: Internal server error } diff --git a/bruno/sidecar/Get System Configuration.bru b/bruno/sidecar/Get System Configuration.bru index f73f057..0b1b51a 100644 --- a/bruno/sidecar/Get System Configuration.bru +++ b/bruno/sidecar/Get System Configuration.bru @@ -1,20 +1,57 @@ meta { - name: "Get System Configuration" + name: Get System Configuration type: http seq: 3 } get { url: {{base_url}}/api/system/config - auth: { - type: bearer - bearer: {{admin_token}} - } + body: none + auth: inherit } -assert { - response.status == 200 - response.body.base_url exists() - response.body.opds_base_url exists() - response.body.api_base_url exists() +headers { + Authorization: Bearer {{admin_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Has base_url', body.base_url !== undefined); + tests('Has opds_base_url', body.opds_base_url !== undefined); + tests('Has api_base_url', body.api_base_url !== undefined); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get System Configuration + + Retrieves system-wide configuration settings. + + **Method:** GET + + **Endpoint:** /api/system/config + + **Authentication:** Bearer token (admin only) + + **Response:** + - `base_url` (string): Base URL + - `opds_base_url` (string): OPDS endpoint URL + - `api_base_url` (string): API endpoint URL + - Additional configuration fields + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 403: Forbidden (admin only) + - 500: Internal server error } diff --git a/bruno/sidecar/Update System Configuration.bru b/bruno/sidecar/Update System Configuration.bru index 354d955..7703c4d 100644 --- a/bruno/sidecar/Update System Configuration.bru +++ b/bruno/sidecar/Update System Configuration.bru @@ -1,23 +1,67 @@ meta { - name: "Update System Configuration" + name: Update System Configuration type: http seq: 4 } put { url: {{base_url}}/api/system/config - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{admin_token}} + Content-Type: application/json +} + +body:json { + { "base_url": "https://bookhoard.example.com", "opds_base_url": "https://bookhoard.example.com/opds", "api_base_url": "https://bookhoard.example.com/api" - }) - auth: { - type: bearer - bearer: {{admin_token}} } } -assert { - response.status == 200 - response.body.status == "success" +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Status is success', body.status === "success"); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Update System Configuration + + Updates system-wide configuration settings for the Bookhoard instance. + + **Method:** PUT + + **Endpoint:** /api/system/config + + **Authentication:** Bearer token (admin only) + + **Request Body:** + - `base_url` (string): Base URL for the instance + - `opds_base_url` (string): OPDS endpoint base URL + - `api_base_url` (string): API endpoint base URL + + **Response:** + - `status` (string): Update status + - `config` (object): Updated configuration + + **Status Codes:** + - 200: Success + - 400: Invalid configuration + - 401: Unauthorized + - 403: Forbidden (admin only) + - 500: Internal server error } diff --git a/bruno/sync-kobo/Auto Link Books.bru b/bruno/sync-kobo/Auto Link Books.bru index 904c5e0..5e320e1 100644 --- a/bruno/sync-kobo/Auto Link Books.bru +++ b/bruno/sync-kobo/Auto Link Books.bru @@ -1,24 +1,58 @@ -{ - "meta": { - "name": "Auto-Link Unlinked Books", - "type": "http" - }, - "req": { - "url": "{{baseUrl}}/sync/auto-link-books", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{authToken}}" - } - ], - "body": { - "confidence_threshold": 0.8, - "limit": 50 - } +meta { + name: Auto-Link Unlinked Books + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/sync/auto-link-books + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "confidence_threshold": 0.8, + "limit": 50 } } + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Auto-Link Unlinked Books + + Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold. + + **Method:** POST + + **Endpoint:** /sync/auto-link-books + + **Authentication:** Bearer token + + **Request Body:** + - `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8) + - `limit` (number, optional): Maximum number of books to auto-link (default: 50) + + **Response:** + - `results` (array): Results for each auto-link attempt + - `total` (number): Total number of books processed + - `success` (number): Number of successful links + - `failed` (number): Number of failed links + + **Status Codes:** + - 200: Success + - 400: Invalid request data + - 401: Unauthorized + - 500: Internal server error + + **Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy. +} diff --git a/bruno/sync-kobo/Bulk Link Books.bru b/bruno/sync-kobo/Bulk Link Books.bru index cedc310..54a9cfe 100644 --- a/bruno/sync-kobo/Bulk Link Books.bru +++ b/bruno/sync-kobo/Bulk Link Books.bru @@ -1,34 +1,70 @@ -{ - "meta": { - "name": "Bulk Link Unlinked Books", - "type": "http" - }, - "req": { - "url": "{{baseUrl}}/sync/bulk-link-books", - "method": "POST", - "header": [ +meta { + name: Bulk Link Unlinked Books + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/sync/bulk-link-books + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Authorization: Bearer {{authToken}} +} + +body:json { + { + "links": [ { - "key": "Content-Type", - "value": "application/json" + "unlinked_book_id": "{{unlinkedBookId1}}", + "media_item_id": "{{mediaItemId1}}", + "confidence_score": 1.0 }, { - "key": "Authorization", - "value": "Bearer {{authToken}}" + "unlinked_book_id": "{{unlinkedBookId2}}", + "media_item_id": "{{mediaItemId2}}", + "confidence_score": 0.9 } - ], - "body": { - "links": [ - { - "unlinked_book_id": "{{unlinkedBookId1}}", - "media_item_id": "{{mediaItemId1}}", - "confidence_score": 1.0 - }, - { - "unlinked_book_id": "{{unlinkedBookId2}}", - "media_item_id": "{{mediaItemId2}}", - "confidence_score": 0.9 - } - ] - } + ] } } + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Bulk Link Unlinked Books + + Links multiple unlinked books to media items in a single request. + + **Method:** POST + + **Endpoint:** /sync/bulk-link-books + + **Authentication:** Bearer token + + **Request Body:** + - `links` (array): Array of link objects + - `unlinked_book_id` (string): Unlinked book UUID + - `media_item_id` (string): Media item UUID to link to + - `confidence_score` (number): Match confidence (0-1) + + **Response:** + - `results` (array): Results for each link attempt + - `total` (number): Total number of links processed + - `success` (number): Number of successful links + - `failed` (number): Number of failed links + + **Status Codes:** + - 200: Success + - 400: Invalid request data + - 401: Unauthorized + - 500: Internal server error + + **Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint. +} diff --git a/bruno/sync-kobo/Get Unlinked Book Suggestions.bru b/bruno/sync-kobo/Get Unlinked Book Suggestions.bru index 96f3494..1782981 100644 --- a/bruno/sync-kobo/Get Unlinked Book Suggestions.bru +++ b/bruno/sync-kobo/Get Unlinked Book Suggestions.bru @@ -1,16 +1,50 @@ -{ - "meta": { - "name": "Get Unlinked Book Suggestions", - "type": "http" - }, - "req": { - "url": "{{baseUrl}}/sync/unlinked-books/{{unlinkedBookId}}/suggestions", - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{authToken}}" - } - ] - } +meta { + name: Get Unlinked Book Suggestions + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/sync/unlinked-books/{{unlinkedBookId}}/suggestions + body: none + auth: inherit +} + +headers { + Authorization: Bearer {{authToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Unlinked Book Suggestions + + Retrieves suggested media items from the library that match an unlinked book, enabling manual linking. + + **Method:** GET + + **Endpoint:** /sync/unlinked-books/{unlinkedBookId}/suggestions + + **Authentication:** Bearer token + + **Path Parameters:** + - `unlinkedBookId` (string): Unlinked book UUID + + **Response:** + - Array of suggested media items with: + - `id` (string): Media item UUID + - `title` (string): Media item title + - `author` (string): Media item author + - `confidence_score` (number): Match confidence (0-1) + - `match_reasons` (array): Reasons for the suggestion + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Unlinked book not found + + **Note:** Suggestions are generated based on title similarity, author matching, and other metadata comparisons. } diff --git a/bruno/sync-kobo/get-unlinked-books.bru b/bruno/sync-kobo/get-unlinked-books.bru index 72d7fc6..2aa3a59 100644 --- a/bruno/sync-kobo/get-unlinked-books.bru +++ b/bruno/sync-kobo/get-unlinked-books.bru @@ -1,5 +1,5 @@ meta { - name: "Get Unlinked Books - User View" + name: Get Unlinked Books - User View type: http seq: 4 } @@ -7,15 +7,54 @@ meta { get { url: {{base_url}}/api/sync/unlinked-books body: none - auth: { - type: bearer - bearer: {{user_token}} - } + auth: inherit } -assert { - response.status == 200 - response.body.unlinked exists() - response.body.total exists() - response.body.total >= 0 +headers { + Authorization: Bearer {{user_token}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests('Has unlinked array', body.unlinked !== undefined); + tests('Has total count', body.total !== undefined); + tests('Total >= 0', body.total >= 0); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Unlinked Books - User View + + Retrieves all unlinked books for the authenticated user that need manual linking. + + **Method:** GET + + **Endpoint:** /api/sync/unlinked-books + + **Authentication:** Bearer token + + **Response:** + - `unlinked` (array): Array of unlinked book objects + - `id` (string): Unlinked book UUID + - `title` (string): Book title + - `author` (string): Book author + - `device_id` (string): Source device ID + - `device_name` (string): Source device name + - `detected_at` (string): Detection timestamp + - `total` (number): Total count of unlinked books + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 500: Internal server error } diff --git a/bruno/sync-kobo/link-book.bru b/bruno/sync-kobo/link-book.bru index 1343ef5..425c265 100644 --- a/bruno/sync-kobo/link-book.bru +++ b/bruno/sync-kobo/link-book.bru @@ -1,26 +1,72 @@ meta { - name: "Link Unlinked Book - Manual Resolution" + name: Link Unlinked Book - Manual Resolution type: http seq: 5 } post { url: {{base_url}}/api/sync/link-book - body: json({ + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{user_token}} + Content-Type: application/json +} + +body:json { + { "unlinked_book_id": "{{unlinked_book_id}}", "media_item_id": "{{media_item_id}}", "confidence_score": 1.0 - }) - auth: { - type: bearer - bearer: {{user_token}} } } -assert { - response.status == 200 - response.body.status == "linked" - response.body.unlinked_book_id exists() - response.body.media_item_id exists() - response.body.message exists() +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Status is linked'] = body.status === "linked"; + tests('Has unlinked_book_id', body.unlinked_book_id !== undefined); + tests('Has media_item_id', body.media_item_id !== undefined); + tests('Has message', body.message !== undefined); + } + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Link Unlinked Book - Manual Resolution + + Manually links an unlinked book to a media item in the library. + + **Method:** POST + + **Endpoint:** /api/sync/link-book + + **Authentication:** Bearer token + + **Request Body:** + - `unlinked_book_id` (string): Unlinked book UUID + - `media_item_id` (string): Media item UUID to link to + - `confidence_score` (number): Match confidence (0-1, 1.0 for manual) + + **Response:** + - `status` (string): Link status (linked) + - `unlinked_book_id` (string): Unlinked book UUID + - `media_item_id` (string): Media item UUID + - `message` (string): Success message + + **Status Codes:** + - 200: Success - book linked + - 400: Invalid request + - 401: Unauthorized + - 404: Book or media item not found + - 500: Internal server error } diff --git a/bruno/universal-progress/Get Progress History.bru b/bruno/universal-progress/Get Progress History.bru index f932b84..c20f1f0 100644 --- a/bruno/universal-progress/Get Progress History.bru +++ b/bruno/universal-progress/Get Progress History.bru @@ -1,21 +1,68 @@ meta { - name: "Get Progress History" - type: "http" + name: Get Progress History + type: http seq: 3 } get { - url: "{{baseUrl}}/api/progress/{{mediaItemId}}/history" - body: null - headers: { - Authorization: "Bearer {{jwt}}" - Content-Type: "application/json" - } + url: {{baseUrl}}/api/progress/{{mediaItemId}}/history + body: none + auth: inherit } -tests { - assertions { - assert response.status == 200 - assert hasKey(response.body, "sessions") +headers { + Authorization: Bearer {{jwt}} + Content-Type: application/json +} + +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Has sessions key'] = body.sessions !== undefined; + } } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Progress History + + Retrieves historical reading progress sessions for a specific media item. + + **Method:** GET + + **Endpoint:** /api/progress/{mediaItemId}/history + + **Authentication:** Bearer token + + **Path Parameters:** + - `mediaItemId` (string): Media item UUID + + **Query Parameters:** + - `limit` (number, optional): Maximum number of sessions to return + - `offset` (number, optional): Offset for pagination + + **Response:** + - `sessions` (array): Array of reading sessions + - `session_id` (string): Session UUID + - `start_time` (string): Session start timestamp + - `end_time` (string): Session end timestamp + - `start_percentage` (number): Progress at start + - `end_percentage` (number): Progress at end + - `device_id` (string): Device used + - `duration_seconds` (number): Session duration + - `total_sessions` (number): Total number of sessions + - `total_reading_time` (number): Total reading time in seconds + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Media item not found + - 500: Internal server error } diff --git a/bruno/universal-progress/Get Universal Progress.bru b/bruno/universal-progress/Get Universal Progress.bru index 9e8a4f8..77614b4 100644 --- a/bruno/universal-progress/Get Universal Progress.bru +++ b/bruno/universal-progress/Get Universal Progress.bru @@ -1,14 +1,55 @@ meta { - name: "Get Universal Progress" - type: "http" + name: Get Universal Progress + type: http seq: 1 } get { - url: "{{baseUrl}}/api/progress/{{mediaItemId}}" - body: null - headers: { - Authorization: "Bearer {{jwt}}" - Content-Type: "application/json" - } + url: {{baseUrl}}/api/progress/{{mediaItemId}} + body: none + auth: inherit +} + +headers { + Authorization: Bearer {{jwt}} + Content-Type: application/json +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Universal Progress + + Retrieves universal reading progress for a specific media item across all devices. + + **Method:** GET + + **Endpoint:** /api/progress/{mediaItemId} + + **Authentication:** Bearer token + + **Path Parameters:** + - `mediaItemId` (string): Media item UUID + + **Response:** + - `media_item_id` (string): Media item UUID + - `progress` (object): Universal progress data + - `percentage` (number): Overall progress percentage + - `epubcfi` (string): EPUB location + - `page` (number): Current page + - `chapter` (number): Current chapter + - `devices` (array): Per-device progress data + - `device_id` (string): Device UUID + - `device_name` (string): Device name + - `progress` (object): Device-specific progress + - `last_updated` (string): Last update timestamp + + **Status Codes:** + - 200: Success + - 401: Unauthorized + - 404: Media item not found + - 500: Internal server error } diff --git a/bruno/universal-progress/Update Universal Progress.bru b/bruno/universal-progress/Update Universal Progress.bru index b179a03..229049e 100644 --- a/bruno/universal-progress/Update Universal Progress.bru +++ b/bruno/universal-progress/Update Universal Progress.bru @@ -1,12 +1,22 @@ meta { - name: "Update Universal Progress" - type: "http" + name: Update Universal Progress + type: http seq: 2 } post { - url: "{{baseUrl}}/api/progress/{{mediaItemId}}" - body: { + url: {{baseUrl}}/api/progress/{{mediaItemId}} + body: json + auth: inherit +} + +headers { + Authorization: Bearer {{jwt}} + Content-Type: application/json +} + +body:json { + { "source": "web", "location": { "percentage": 0.45, @@ -18,16 +28,59 @@ post { "user_agent": "integration-test" } } - headers: { - Authorization: "Bearer {{jwt}}" - Content-Type: "application/json" - } } -tests { - assertions { - assert response.status == 200 - assert response.body.sync_status == "success" - assert response.body.progress_updated == true +script:post-response { + function onResponse(res) { + if (res.getStatus() === 200) { + const body = res.getBody(); + tests['Sync status success'] = body.sync_status === "success"; + tests['Progress updated'] = body.progress_updated === true; + } } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Update Universal Progress + + Updates universal reading progress for a media item from a specific device or source. + + **Method:** POST + + **Endpoint:** /api/progress/{mediaItemId} + + **Authentication:** Bearer token + + **Path Parameters:** + - `mediaItemId` (string): Media item UUID + + **Request Body:** + - `source` (string): Source identifier (web, kobo, koreader, device_id) + - `location` (object): Progress location data + - `percentage` (number): Progress percentage (0-1) + - `page` (number, optional): Current page + - `total_pages` (number, optional): Total pages + - `epubcfi` (string, optional): EPUB location + - `chapter` (number, optional): Current chapter + - `device_metadata` (object): Device metadata + - `device_type` (string): Device type + - `user_agent` (string, optional): User agent string + + **Response:** + - `sync_status` (string): Sync status (success, partial) + - `progress_updated` (boolean): Whether progress was updated + - `conflicts_detected` (array, optional): Any conflicts detected + + **Status Codes:** + - 200: Success + - 400: Invalid request data + - 401: Unauthorized + - 404: Media item not found + - 500: Internal server error } diff --git a/bruno/user/auth/Logout User.bru b/bruno/user/auth/Logout User.bru index d330f0c..7419b30 100644 --- a/bruno/user/auth/Logout User.bru +++ b/bruno/user/auth/Logout User.bru @@ -1,85 +1,57 @@ -{ - "meta": { - "name": "Logout User", - "type": "http", - "seq": 1, - "auth": "Inherit" - }, - "request": { - "method": "POST", - "header": [ - { - "name": "Content-Type", - "value": "application/json" - } - ], - "body": { - "type": "json", - "json": { - "refresh_token": "{{refresh_token}}" - } - }, - "url": { - "raw": "{{base_url}}/api/auth/logout", - "host": ["{{base_url}}"], - "path": ["api", "auth", "logout"] - }, - "description": "Logs out the user by revoking their refresh token. If no refresh token is provided, the request succeeds but no token is revoked." - }, - "response": [ - { - "name": "Success Response", - "originalRequest": { - "method": "POST", - "header": [], - "body": { - "type": "json", - "json": { - "refresh_token": "valid-refresh-token-uuid" - } - }, - "url": { - "raw": "{{base_url}}/api/auth/logout", - "host": ["{{base_url}}"], - "path": ["api", "auth", "logout"] - } - }, - "status": 200, - "code": 200, - "header": [ - { - "name": "content-type", - "value": "application/json" - } - ], - "body": "{\n \"message\": \"logged out successfully\"\n}", - "description": "Successfully logged out and refresh token revoked." - }, - { - "name": "Logout Without Refresh Token", - "originalRequest": { - "method": "POST", - "header": [], - "body": { - "type": "json", - "json": {} - }, - "url": { - "raw": "{{base_url}}/api/auth/logout", - "host": ["{{base_url}}"], - "path": ["api", "auth", "logout"] - } - }, - "status": 200, - "code": 200, - "header": [ - { - "name": "content-type", - "value": "application/json" - } - ], - "body": "{\n \"message\": \"logged out successfully\"\n}", - "description": "Logout succeeds even without a refresh token." - } - ] +meta { + name: Logout User + type: http + seq: 1 +} + +post { + url: {{base_url}}/api/auth/logout + body: json + auth: inherit +} + +headers { + Content-Type: application/json +} + +body:json { + { + "refresh_token": "{{refresh_token}}" + } +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Logout User + + Logs out the user by revoking their refresh token. If no refresh token is provided, the request succeeds but no token is revoked. + + **Method:** POST + + **Endpoint:** /api/auth/logout + + **Authentication:** Bearer token (optional) + + **Request Body:** + - `refresh_token` (string, optional): Refresh token to revoke + + **Response:** + - `message` (string): Success message + + **Status Codes:** + - 200: Success - user logged out (token revoked if provided) + - 401: Unauthorized + + **Example Response:** + ```json + { + "message": "logged out successfully" + } + ``` + + **Note:** The access token will expire naturally after 1 hour. The refresh token is immediately revoked on logout, preventing future token refreshes. } diff --git a/bruno/user/auth/Refresh Token.bru b/bruno/user/auth/Refresh Token.bru index 138fca5..5dcf0b7 100644 --- a/bruno/user/auth/Refresh Token.bru +++ b/bruno/user/auth/Refresh Token.bru @@ -1,87 +1,68 @@ -{ - "meta": { - "name": "Refresh Access Token", - "type": "http", - "seq": 1, - "auth": "Inherit" - }, - "request": { - "method": "POST", - "header": [ - { - "name": "Content-Type", - "value": "application/json" - } - ], - "body": { - "type": "json", - "json": { - "refresh_token": "{{refresh_token}}" - } - }, - "url": { - "raw": "{{base_url}}/api/auth/refresh", - "host": ["{{base_url}}"], - "path": ["api", "auth", "refresh"] - }, - "description": "Refreshes an access token using a valid refresh token. Returns a new access token with 1-hour expiration." - }, - "response": [ - { - "name": "Success Response", - "originalRequest": { - "method": "POST", - "header": [], - "body": { - "type": "json", - "json": { - "refresh_token": "valid-refresh-token-uuid" - } - }, - "url": { - "raw": "{{base_url}}/api/auth/refresh", - "host": ["{{base_url}}"], - "path": ["api", "auth", "refresh"] - } - }, - "status": 200, - "code": 200, - "header": [ - { - "name": "content-type", - "value": "application/json" - } - ], - "body": "{\n \"access_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"token_type\": \"Bearer\",\n \"expires_in\": 3600\n}", - "description": "Returns a new access token that expires in 1 hour (3600 seconds)." - }, - { - "name": "Invalid Refresh Token", - "originalRequest": { - "method": "POST", - "header": [], - "body": { - "type": "json", - "json": { - "refresh_token": "invalid-token" - } - }, - "url": { - "raw": "{{base_url}}/api/auth/refresh", - "host": ["{{base_url}}"], - "path": ["api", "auth", "refresh"] - } - }, - "status": 401, - "code": 401, - "header": [ - { - "name": "content-type", - "value": "application/json" - } - ], - "body": "{\n \"error\": \"invalid or expired refresh token\"\n}", - "description": "Returned when the refresh token is invalid, expired, or has been revoked." - } - ] +meta { + name: Refresh Access Token + type: http + seq: 1 +} + +post { + url: {{base_url}}/api/auth/refresh + body: json + auth: inherit +} + +headers { + Content-Type: application/json +} + +body:json { + { + "refresh_token": "{{refresh_token}}" + } +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Refresh Access Token + + Refreshes an access token using a valid refresh token. Returns a new access token with 1-hour expiration. + + **Method:** POST + + **Endpoint:** /api/auth/refresh + + **Authentication:** Not required (refresh token is in request body) + + **Request Body:** + - `refresh_token` (string): Valid refresh token UUID + + **Response:** + - `access_token` (string): New JWT access token (1 hour expiration) + - `token_type` (string): Token type (usually "Bearer") + - `expires_in` (number): Token lifetime in seconds (3600) + + **Status Codes:** + - 200: Success - new access token generated + - 401: Unauthorized - invalid or expired refresh token + + **Example Response (Success):** + ```json + { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 3600 + } + ``` + + **Example Response (Invalid Token):** + ```json + { + "error": "invalid or expired refresh token" + } + ``` + + **Note:** Access tokens expire after 1 hour. Use the refresh token to obtain a new access token without requiring the user to log in again. } diff --git a/bruno/websocket_connect.bru b/bruno/websocket_connect.bru index 8fbe31d..136604e 100644 --- a/bruno/websocket_connect.bru +++ b/bruno/websocket_connect.bru @@ -1,5 +1,5 @@ meta { - name: "WebSocket - Connect and Receive Updates" + name: WebSocket - Connect and Receive Updates type: websocket seq: 1 } @@ -9,22 +9,63 @@ vars { } websocket { - url: "{{ baseUrl }}/ws/sync?token={{ token }}" + url: {{baseUrl}}/ws/sync?token={{token}} body: { type: "ping", timestamp: "2026-01-30T20:00:00Z" } - auth: { - type: bearer - token: "{{ token }}" - } - headers: { - Authorization: "Bearer {{ token }}" - } + auth: inherit } -assert { - res.status == 101 - res.headers.contains("Connection", "upgrade") - res.headers.contains("Upgrade", "websocket") +headers { + Authorization: Bearer {{token}} +} + +script:post-response { + function onResponse(res) { + tests('Status is 101 (Switching Protocols)', res.getStatus() === 101); + const headers = res.getHeaders(); + const connection = headers.get("Connection"); + const upgrade = headers.get("Upgrade"); + tests('Connection header has upgrade', connection && connection.toLowerCase().includes("upgrade")); + tests('Upgrade header is websocket', upgrade && upgrade.toLowerCase().includes("websocket")); + } + onResponse(res); +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## WebSocket - Connect and Receive Updates + + Establishes a WebSocket connection for real-time sync updates. + + **Type:** WebSocket + + **Endpoint:** /ws/sync?token={token} + + **Authentication:** Bearer token (via query parameter) + + **Query Parameters:** + - `token` (string): JWT access token + + **Request Body:** + - `type` (string): Message type (ping, subscribe, unsubscribe) + - `timestamp` (string): ISO 8601 timestamp + + **Expected Response:** + - Status: 101 Switching Protocols + - Headers: + - `Connection`: upgrade + - `Upgrade`: websocket + + **Status Codes:** + - 101: Switching Protocols - WebSocket established + - 401: Unauthorized + - 500: Internal server error + + **Note:** WebSocket connection allows real-time push notifications for sync updates, progress changes, and device activity. }