From 983980d07aa62cc2a71fb8d774b6b52f66cf6749 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 24 Oct 2026 19:20:13 -0400 Subject: [PATCH] bruno+docs: status asserts on generated mirrors, OpenAPI spec, safe negative scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups from the smoke run, all additive: 1. Status asserts: bruno_gen.go parses each doc's documented success status (## Response (2NN), 'Success is **201**', '**204 No Content**') and emits a runtime tests script (expect(res.status).to.eql(N)) on 148 of 191 mirrors. GETs default to 200 (uniform across this API); mutating verbs stay assert-less unless documented — no guessed asserts. Verified live: asserts execute and fail loudly (deliberate 999 probe), pass on real responses. 2. OpenAPI: the same -update run now also writes docs/developer/openapi.yaml (141 paths, tags from docs areas, security schemes per auth label, request examples literal, no invented schemas — the docs site remains the shape reference). Verify mode fails when the file is missing. 3. Scenarios: 7 safe negative scenarios rebuilt (auth registration matrix 409/403/400, invalid-uuid 400, unknown 404 x2, invalid range 400) — all no-mutation, repeatable, run via bru: 7/7 tests pass. These replace a fraction of the deleted broken suite; mutations- heavy scenarios stay out until a design for setup chains exists. created_at RFC3339 deliberately NOT done — it is the one item that would break things (Android parses current formats; plan reserves it as a compat decision).bru run of mutating mirrors also proven to touch live data (a 'To Read' collection was created and deleted in testing); only id-less GET mirrors and negative scenarios are safe for blanket runs. make test green; generation idempotent (byte-identical re-runs); no stray smoke rows in users. --- bruno/admin/hash_conflicts_api.yml | 8 + bruno/admin/list_users.yml | 8 + bruno/admin/update_user_max_devices.yml | 8 + bruno/analytics/get_analytics.yml | 8 + bruno/analytics/get_device_usage.yml | 8 + bruno/analytics/get_popular_books.yml | 8 + bruno/authentication/login.yml | 6 + bruno/authentication/logout.yml | 8 + bruno/authentication/password_rules.yml | 8 + bruno/authentication/refresh_token.yml | 6 + bruno/authentication/register.yml | 6 + ...register-admin-with-admins-present-403.yml | 40 + .../register-duplicate-email-409.yml | 39 + .../scenarios/register-weak-password-400.yml | 38 + bruno/book-matching/auto_link_books.yml | 8 + bruno/book-matching/bulk_link_books.yml | 8 + .../create_device_file_alias.yml | 8 + .../delete_device_file_alias.yml | 8 + bruno/book-matching/get_book_matches.yml | 8 + .../book-matching/get_device_file_aliases.yml | 8 + .../get_unlinked_book_suggestions.yml | 8 + bruno/book-matching/get_unlinked_books.yml | 8 + bruno/book-matching/link_book.yml | 8 + bruno/book-matching/search_books.yml | 8 + .../update_device_file_alias.yml | 8 + bruno/collections/add_auto_assign_rule.yml | 8 + bruno/collections/bulk_assign.yml | 8 + bruno/collections/create_shelf_mapping.yml | 8 + bruno/collections/delete_collection.yml | 8 + bruno/collections/delete_shelf_mapping.yml | 8 + bruno/collections/get_collection.yml | 8 + bruno/collections/get_collection_books.yml | 8 + bruno/collections/list_collections.yml | 8 + bruno/collections/remove_auto_assign_rule.yml | 8 + bruno/collections/test_rule.yml | 8 + bruno/conflicts/bulk_dismiss_conflicts.yml | 8 + bruno/conflicts/bulk_resolve_conflicts.yml | 8 + bruno/conflicts/delete_conflict.yml | 8 + bruno/conflicts/dismiss_all_resolved.yml | 8 + bruno/conflicts/get_conflict.yml | 8 + bruno/conflicts/list_conflicts.yml | 8 + bruno/conflicts/resolve_conflict.yml | 8 + bruno/dashboard/get_preferences.yml | 8 + bruno/dashboard/get_sections.yml | 8 + bruno/devices/add_to_shelf.yml | 8 + bruno/devices/approve_device_registration.yml | 8 + bruno/devices/clear_shelf.yml | 8 + bruno/devices/delete_device.yml | 8 + bruno/devices/download_sidecar_config.yml | 8 + bruno/devices/get_device.yml | 8 + bruno/devices/get_device_mappings.yml | 8 + bruno/devices/get_devices.yml | 8 + bruno/devices/get_shelf.yml | 8 + bruno/devices/get_sidecar_config.yml | 8 + bruno/devices/list_devices.yml | 8 + bruno/devices/list_pending_registrations.yml | 8 + bruno/devices/register_device.yml | 8 + bruno/devices/reject_device_registration.yml | 8 + bruno/devices/remove_from_shelf.yml | 8 + bruno/devices/update_device.yml | 8 + bruno/highlights/create_highlight.yml | 8 + bruno/highlights/delete_highlight.yml | 8 + bruno/highlights/get_highlights.yml | 8 + bruno/highlights/update_highlight.yml | 8 + bruno/jobs/create_job.yml | 8 + bruno/jobs/get_job_status.yml | 8 + bruno/kobo/analytics_gettests.yml | 8 + bruno/kobo/bookmark_sync.yml | 8 + bruno/kobo/initialization.yml | 8 + bruno/kobo/markup_sync.yml | 8 + bruno/kobo/sync_from_server.yml | 8 + bruno/koreader/get_library.yml | 8 + bruno/koreader/get_metadata.yml | 8 + bruno/koreader/get_rating.yml | 8 + bruno/koreader/get_review.yml | 8 + bruno/koreader/resolve_book.yml | 8 + bruno/koreader/sync_bookmarks.yml | 8 + bruno/koreader/sync_progress.yml | 8 + bruno/libraries/add_library_folder.yml | 8 + bruno/libraries/browse-folders.yml | 8 + bruno/libraries/create_library.yml | 8 + bruno/libraries/delete_library.yml | 8 + bruno/libraries/delete_library_folder.yml | 8 + bruno/libraries/get_library.yml | 8 + bruno/libraries/get_library_folders.yml | 8 + bruno/libraries/get_library_media_items.yml | 8 + bruno/libraries/get_library_stats.yml | 8 + bruno/libraries/get_library_types.yml | 8 + .../libraries/get_processing_issue_stats.yml | 8 + bruno/libraries/get_scan_settings.yml | 8 + bruno/libraries/get_visible_libraries.yml | 8 + bruno/libraries/list_libraries.yml | 8 + bruno/libraries/list_processing_issues.yml | 8 + bruno/libraries/set_library_visibility.yml | 8 + bruno/libraries/update_library.yml | 8 + bruno/media-items/bulk_delete_media_items.yml | 8 + bruno/media-items/bulk_update_media_items.yml | 8 + bruno/media-items/create_media_item.yml | 8 + bruno/media-items/delete_media_bookmark.yml | 8 + bruno/media-items/delete_media_item.yml | 8 + bruno/media-items/deleted_annotations.yml | 8 + .../media-items/download_media_item_file.yml | 8 + bruno/media-items/filter_count.yml | 8 + bruno/media-items/get_audio_chapters.yml | 8 + bruno/media-items/get_audio_track.yml | 8 + bruno/media-items/get_facets.yml | 8 + bruno/media-items/get_media_bookmarks.yml | 8 + bruno/media-items/get_media_highlight.yml | 8 + bruno/media-items/get_media_item.yml | 8 + bruno/media-items/get_media_note.yml | 8 + bruno/media-items/get_media_review.yml | 8 + bruno/media-items/list_media_items.yml | 8 + .../get-media-item-invalid-uuid-400.yml | 30 + .../scenarios/get-media-item-unknown-404.yml | 29 + bruno/media-items/search_media_items.yml | 8 + bruno/media-items/unarchive_media_item.yml | 8 + bruno/media-items/update_media_item.yml | 8 + bruno/notes/create_note.yml | 8 + bruno/notes/delete_note.yml | 8 + bruno/notes/get_notes.yml | 8 + bruno/notes/update_note.yml | 8 + bruno/opds/acquisition.yml | 8 + bruno/opds/feeds.yml | 8 + bruno/opds/publication.yml | 8 + bruno/progress/delete_media_progress.yml | 8 + bruno/progress/get_media_progress.yml | 8 + bruno/progress/get_progress_history.yml | 8 + bruno/progress/update_media_progress.yml | 8 + bruno/queue/clear_device_queue.yml | 8 + bruno/queue/delete_queue_item.yml | 8 + bruno/queue/get_device_queue_stats.yml | 8 + bruno/queue/list_all_queue_items.yml | 8 + bruno/queue/list_device_queue_items.yml | 8 + bruno/queue/retry_queue_item.yml | 8 + bruno/ratings/create_rating.yml | 8 + bruno/ratings/delete_rating.yml | 8 + bruno/ratings/get_ratings.yml | 8 + bruno/ratings/update_rating.yml | 8 + bruno/saved-filters/get_saved_filter.yml | 8 + bruno/saved-filters/list_saved_filters.yml | 8 + bruno/scanner/get_active_scans.yml | 8 + bruno/scanner/get_scan_status.yml | 8 + bruno/scanner/get_scan_summary.yml | 8 + bruno/scanner/get_watch_mode_status.yml | 8 + bruno/scanner/scan_library.yml | 8 + .../scenarios/scan-status-unknown-job-404.yml | 29 + bruno/scanner/start_scanner.yml | 8 + bruno/scanner/start_watch_mode.yml | 8 + bruno/scanner/stop_scanner.yml | 8 + bruno/scanner/stop_watch_mode.yml | 8 + bruno/series/get_series_books.yml | 8 + bruno/series/list_series.yml | 8 + bruno/stats/get_book_stats.yml | 8 + bruno/stats/get_library_usage.yml | 8 + bruno/stats/get_summary.yml | 8 + bruno/stats/get_this_week.yml | 8 + .../scenarios/summary-invalid-range-400.yml | 29 + bruno/system/get_config.yml | 8 + bruno/system/get_settings.yml | 8 + bruno/users/change_password.yml | 8 + bruno/users/delete_user.yml | 8 + bruno/users/get_profile.yml | 8 + bruno/users/update_profile.yml | 8 + docs/developer/openapi.yaml | 2421 +++++++++++++++++ internal/docs/bruno_gen.go | 145 +- internal/docs/drift_test.go | 6 + 166 files changed, 4040 insertions(+), 8 deletions(-) create mode 100644 bruno/authentication/scenarios/register-admin-with-admins-present-403.yml create mode 100644 bruno/authentication/scenarios/register-duplicate-email-409.yml create mode 100644 bruno/authentication/scenarios/register-weak-password-400.yml create mode 100644 bruno/media-items/scenarios/get-media-item-invalid-uuid-400.yml create mode 100644 bruno/media-items/scenarios/get-media-item-unknown-404.yml create mode 100644 bruno/scanner/scenarios/scan-status-unknown-job-404.yml create mode 100644 bruno/stats/scenarios/summary-invalid-range-400.yml create mode 100644 docs/developer/openapi.yaml diff --git a/bruno/admin/hash_conflicts_api.yml b/bruno/admin/hash_conflicts_api.yml index e7ef72bf..bb4a168c 100644 --- a/bruno/admin/hash_conflicts_api.yml +++ b/bruno/admin/hash_conflicts_api.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Hash Conflicts (API) diff --git a/bruno/admin/list_users.yml b/bruno/admin/list_users.yml index da29b6ab..0f34ff78 100644 --- a/bruno/admin/list_users.yml +++ b/bruno/admin/list_users.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Users diff --git a/bruno/admin/update_user_max_devices.yml b/bruno/admin/update_user_max_devices.yml index f002a737..17cfb013 100644 --- a/bruno/admin/update_user_max_devices.yml +++ b/bruno/admin/update_user_max_devices.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update User Max Devices diff --git a/bruno/analytics/get_analytics.yml b/bruno/analytics/get_analytics.yml index c41e3710..ca781c87 100644 --- a/bruno/analytics/get_analytics.yml +++ b/bruno/analytics/get_analytics.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Reading Statistics diff --git a/bruno/analytics/get_device_usage.yml b/bruno/analytics/get_device_usage.yml index cab39efd..cb1dc896 100644 --- a/bruno/analytics/get_device_usage.yml +++ b/bruno/analytics/get_device_usage.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device Usage diff --git a/bruno/analytics/get_popular_books.yml b/bruno/analytics/get_popular_books.yml index 414a4f1a..1d4d40cf 100644 --- a/bruno/analytics/get_popular_books.yml +++ b/bruno/analytics/get_popular_books.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Popular Books diff --git a/bruno/authentication/login.yml b/bruno/authentication/login.yml index 5c3e9f48..3ec2c567 100644 --- a/bruno/authentication/login.yml +++ b/bruno/authentication/login.yml @@ -43,6 +43,12 @@ runtime: } onResponse(); + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Login User diff --git a/bruno/authentication/logout.yml b/bruno/authentication/logout.yml index 14a174bf..6e15c31d 100644 --- a/bruno/authentication/logout.yml +++ b/bruno/authentication/logout.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Logout User diff --git a/bruno/authentication/password_rules.yml b/bruno/authentication/password_rules.yml index 218c1793..6096bd04 100644 --- a/bruno/authentication/password_rules.yml +++ b/bruno/authentication/password_rules.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Password Rules diff --git a/bruno/authentication/refresh_token.yml b/bruno/authentication/refresh_token.yml index a12f1fd1..3de13a0d 100644 --- a/bruno/authentication/refresh_token.yml +++ b/bruno/authentication/refresh_token.yml @@ -42,6 +42,12 @@ runtime: } onResponse(); + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Refresh Token diff --git a/bruno/authentication/register.yml b/bruno/authentication/register.yml index f25a45a2..b0d207f9 100644 --- a/bruno/authentication/register.yml +++ b/bruno/authentication/register.yml @@ -46,6 +46,12 @@ runtime: } onResponse(); + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Register User diff --git a/bruno/authentication/scenarios/register-admin-with-admins-present-403.yml b/bruno/authentication/scenarios/register-admin-with-admins-present-403.yml new file mode 100644 index 00000000..da4b7264 --- /dev/null +++ b/bruno/authentication/scenarios/register-admin-with-admins-present-403.yml @@ -0,0 +1,40 @@ +info: + name: Register - Admin Role Unauthenticated (403) + type: http + seq: 2 + +http: + method: POST + url: '{{base_url}}/api/auth/register' + auth: inherit + body: + type: json + data: |- + { + "email": "smoke-admin-attempts@tests.bookhoard.internal", + "username": "smokeadminattempts", + "password": "Sm0ke@Pass123!", + "role": "admin" + } +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 403", function() { + expect(res.status).to.eql(403); + }); + +docs: + content: |- + # Register - Admin Role Unauthenticated (403) + + With admins already present, an unauthenticated register attempt + carrying role=admin must be rejected with 403 and create nothing. + Repeats safely. + type: text/markdown diff --git a/bruno/authentication/scenarios/register-duplicate-email-409.yml b/bruno/authentication/scenarios/register-duplicate-email-409.yml new file mode 100644 index 00000000..0752482d --- /dev/null +++ b/bruno/authentication/scenarios/register-duplicate-email-409.yml @@ -0,0 +1,39 @@ +info: + name: Register - Duplicate Email (409) + type: http + seq: 1 + +http: + method: POST + url: '{{base_url}}/api/auth/register' + auth: inherit + body: + type: json + data: |- + { + "email": "testuser@tests.bookhoard.internal", + "username": "testuser", + "password": "Wh4t3v3r@Pass!" + } +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 409", function() { + expect(res.status).to.eql(409); + }); + +docs: + content: |- + # Register - Duplicate Email (409) + + Registering with an email that already exists must fail with 409 and + create nothing. Negative scenario — hand-authored (the generated set + covers only happy paths). Repeats safely. + type: text/markdown diff --git a/bruno/authentication/scenarios/register-weak-password-400.yml b/bruno/authentication/scenarios/register-weak-password-400.yml new file mode 100644 index 00000000..a8c3f07c --- /dev/null +++ b/bruno/authentication/scenarios/register-weak-password-400.yml @@ -0,0 +1,38 @@ +info: + name: Register - Weak Password (400) + type: http + seq: 3 + +http: + method: POST + url: '{{base_url}}/api/auth/register' + auth: inherit + body: + type: json + data: |- + { + "email": "smoke-weak-attempts@tests.bookhoard.internal", + "username": "smokeweakattempts", + "password": "weak" + } +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 400", function() { + expect(res.status).to.eql(400); + }); + +docs: + content: |- + # Register - Weak Password (400) + + Passwords below the configured complexity rules (see + GET /api/auth/password-rules) must be rejected with 400. Repeats safely. + type: text/markdown diff --git a/bruno/book-matching/auto_link_books.yml b/bruno/book-matching/auto_link_books.yml index a77cf05f..ee065b18 100644 --- a/bruno/book-matching/auto_link_books.yml +++ b/bruno/book-matching/auto_link_books.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Auto Link Books diff --git a/bruno/book-matching/bulk_link_books.yml b/bruno/book-matching/bulk_link_books.yml index ea38f6d1..0754bcf9 100644 --- a/bruno/book-matching/bulk_link_books.yml +++ b/bruno/book-matching/bulk_link_books.yml @@ -30,6 +30,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Bulk Link Books diff --git a/bruno/book-matching/create_device_file_alias.yml b/bruno/book-matching/create_device_file_alias.yml index 6220c69d..285efc5d 100644 --- a/bruno/book-matching/create_device_file_alias.yml +++ b/bruno/book-matching/create_device_file_alias.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Device File Alias diff --git a/bruno/book-matching/delete_device_file_alias.yml b/bruno/book-matching/delete_device_file_alias.yml index 3925fa33..eb0d9144 100644 --- a/bruno/book-matching/delete_device_file_alias.yml +++ b/bruno/book-matching/delete_device_file_alias.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Device File Alias diff --git a/bruno/book-matching/get_book_matches.yml b/bruno/book-matching/get_book_matches.yml index 1b9ab395..1cb89e48 100644 --- a/bruno/book-matching/get_book_matches.yml +++ b/bruno/book-matching/get_book_matches.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Book Matches diff --git a/bruno/book-matching/get_device_file_aliases.yml b/bruno/book-matching/get_device_file_aliases.yml index d6557957..b6bfe6ba 100644 --- a/bruno/book-matching/get_device_file_aliases.yml +++ b/bruno/book-matching/get_device_file_aliases.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device File Aliases diff --git a/bruno/book-matching/get_unlinked_book_suggestions.yml b/bruno/book-matching/get_unlinked_book_suggestions.yml index 7fa78199..d07dc6c8 100644 --- a/bruno/book-matching/get_unlinked_book_suggestions.yml +++ b/bruno/book-matching/get_unlinked_book_suggestions.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Unlinked Book Suggestions diff --git a/bruno/book-matching/get_unlinked_books.yml b/bruno/book-matching/get_unlinked_books.yml index 0d498703..522ec6f4 100644 --- a/bruno/book-matching/get_unlinked_books.yml +++ b/bruno/book-matching/get_unlinked_books.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Unlinked Books diff --git a/bruno/book-matching/link_book.yml b/bruno/book-matching/link_book.yml index 1ec74bfe..a24f299c 100644 --- a/bruno/book-matching/link_book.yml +++ b/bruno/book-matching/link_book.yml @@ -25,6 +25,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Link Book diff --git a/bruno/book-matching/search_books.yml b/bruno/book-matching/search_books.yml index 5a8510e2..87232c1a 100644 --- a/bruno/book-matching/search_books.yml +++ b/bruno/book-matching/search_books.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Search Books for Matching diff --git a/bruno/book-matching/update_device_file_alias.yml b/bruno/book-matching/update_device_file_alias.yml index a23de86a..a3e06726 100644 --- a/bruno/book-matching/update_device_file_alias.yml +++ b/bruno/book-matching/update_device_file_alias.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Device File Alias diff --git a/bruno/collections/add_auto_assign_rule.yml b/bruno/collections/add_auto_assign_rule.yml index fab3b407..456c025d 100644 --- a/bruno/collections/add_auto_assign_rule.yml +++ b/bruno/collections/add_auto_assign_rule.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Add Auto-Assign Rule diff --git a/bruno/collections/bulk_assign.yml b/bruno/collections/bulk_assign.yml index e400eb1c..ca98f446 100644 --- a/bruno/collections/bulk_assign.yml +++ b/bruno/collections/bulk_assign.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Bulk Assign Books diff --git a/bruno/collections/create_shelf_mapping.yml b/bruno/collections/create_shelf_mapping.yml index 7600f27f..e93fa5f3 100644 --- a/bruno/collections/create_shelf_mapping.yml +++ b/bruno/collections/create_shelf_mapping.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Shelf Mapping diff --git a/bruno/collections/delete_collection.yml b/bruno/collections/delete_collection.yml index 200b22b4..d5ff6ca5 100644 --- a/bruno/collections/delete_collection.yml +++ b/bruno/collections/delete_collection.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Collection diff --git a/bruno/collections/delete_shelf_mapping.yml b/bruno/collections/delete_shelf_mapping.yml index f60b3081..3f79c0ef 100644 --- a/bruno/collections/delete_shelf_mapping.yml +++ b/bruno/collections/delete_shelf_mapping.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Shelf Mapping diff --git a/bruno/collections/get_collection.yml b/bruno/collections/get_collection.yml index 39c6b0cc..48027b6d 100644 --- a/bruno/collections/get_collection.yml +++ b/bruno/collections/get_collection.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Collection diff --git a/bruno/collections/get_collection_books.yml b/bruno/collections/get_collection_books.yml index 77390b82..a295c9ef 100644 --- a/bruno/collections/get_collection_books.yml +++ b/bruno/collections/get_collection_books.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Book Collections diff --git a/bruno/collections/list_collections.yml b/bruno/collections/list_collections.yml index 6ec922fb..02c780c2 100644 --- a/bruno/collections/list_collections.yml +++ b/bruno/collections/list_collections.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Collections diff --git a/bruno/collections/remove_auto_assign_rule.yml b/bruno/collections/remove_auto_assign_rule.yml index 1612d43d..cdf0a866 100644 --- a/bruno/collections/remove_auto_assign_rule.yml +++ b/bruno/collections/remove_auto_assign_rule.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Remove Auto-Assign Rule diff --git a/bruno/collections/test_rule.yml b/bruno/collections/test_rule.yml index 268f3bf7..f3f0caf2 100644 --- a/bruno/collections/test_rule.yml +++ b/bruno/collections/test_rule.yml @@ -30,6 +30,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Test Rule diff --git a/bruno/conflicts/bulk_dismiss_conflicts.yml b/bruno/conflicts/bulk_dismiss_conflicts.yml index 79ede06c..0048ea78 100644 --- a/bruno/conflicts/bulk_dismiss_conflicts.yml +++ b/bruno/conflicts/bulk_dismiss_conflicts.yml @@ -22,6 +22,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Bulk Dismiss Conflicts diff --git a/bruno/conflicts/bulk_resolve_conflicts.yml b/bruno/conflicts/bulk_resolve_conflicts.yml index 9b9d2ac1..9e39723b 100644 --- a/bruno/conflicts/bulk_resolve_conflicts.yml +++ b/bruno/conflicts/bulk_resolve_conflicts.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Bulk Resolve Conflicts diff --git a/bruno/conflicts/delete_conflict.yml b/bruno/conflicts/delete_conflict.yml index 884a4450..6d7855cf 100644 --- a/bruno/conflicts/delete_conflict.yml +++ b/bruno/conflicts/delete_conflict.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Conflict diff --git a/bruno/conflicts/dismiss_all_resolved.yml b/bruno/conflicts/dismiss_all_resolved.yml index 29398f3f..e71219fc 100644 --- a/bruno/conflicts/dismiss_all_resolved.yml +++ b/bruno/conflicts/dismiss_all_resolved.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Dismiss All Resolved diff --git a/bruno/conflicts/get_conflict.yml b/bruno/conflicts/get_conflict.yml index e4e167b7..e5b7cf14 100644 --- a/bruno/conflicts/get_conflict.yml +++ b/bruno/conflicts/get_conflict.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Conflict Details diff --git a/bruno/conflicts/list_conflicts.yml b/bruno/conflicts/list_conflicts.yml index cf28f699..d58bcb62 100644 --- a/bruno/conflicts/list_conflicts.yml +++ b/bruno/conflicts/list_conflicts.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Conflicts diff --git a/bruno/conflicts/resolve_conflict.yml b/bruno/conflicts/resolve_conflict.yml index b1965ab1..85543913 100644 --- a/bruno/conflicts/resolve_conflict.yml +++ b/bruno/conflicts/resolve_conflict.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Resolve Conflict diff --git a/bruno/dashboard/get_preferences.yml b/bruno/dashboard/get_preferences.yml index 6a1446a1..7ee97366 100644 --- a/bruno/dashboard/get_preferences.yml +++ b/bruno/dashboard/get_preferences.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Dashboard Preferences diff --git a/bruno/dashboard/get_sections.yml b/bruno/dashboard/get_sections.yml index 4fff91c3..cf5e52eb 100644 --- a/bruno/dashboard/get_sections.yml +++ b/bruno/dashboard/get_sections.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Dashboard Sections diff --git a/bruno/devices/add_to_shelf.yml b/bruno/devices/add_to_shelf.yml index ecffa48b..cb34cd8c 100644 --- a/bruno/devices/add_to_shelf.yml +++ b/bruno/devices/add_to_shelf.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Add to Shelf diff --git a/bruno/devices/approve_device_registration.yml b/bruno/devices/approve_device_registration.yml index 9b140df2..d0133ce9 100644 --- a/bruno/devices/approve_device_registration.yml +++ b/bruno/devices/approve_device_registration.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Approve Device Registration diff --git a/bruno/devices/clear_shelf.yml b/bruno/devices/clear_shelf.yml index a70abaec..f82b0981 100644 --- a/bruno/devices/clear_shelf.yml +++ b/bruno/devices/clear_shelf.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Clear Shelf diff --git a/bruno/devices/delete_device.yml b/bruno/devices/delete_device.yml index 0756c39f..285a7522 100644 --- a/bruno/devices/delete_device.yml +++ b/bruno/devices/delete_device.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Device diff --git a/bruno/devices/download_sidecar_config.yml b/bruno/devices/download_sidecar_config.yml index 9d3c978b..9bf6292c 100644 --- a/bruno/devices/download_sidecar_config.yml +++ b/bruno/devices/download_sidecar_config.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Download Sidecar Config diff --git a/bruno/devices/get_device.yml b/bruno/devices/get_device.yml index 4462acd3..5848703f 100644 --- a/bruno/devices/get_device.yml +++ b/bruno/devices/get_device.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device diff --git a/bruno/devices/get_device_mappings.yml b/bruno/devices/get_device_mappings.yml index 5fe67f78..ae4e5dc3 100644 --- a/bruno/devices/get_device_mappings.yml +++ b/bruno/devices/get_device_mappings.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device Collection Mappings diff --git a/bruno/devices/get_devices.yml b/bruno/devices/get_devices.yml index 9dfe9b71..49375408 100644 --- a/bruno/devices/get_devices.yml +++ b/bruno/devices/get_devices.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device / Check Registration Status diff --git a/bruno/devices/get_shelf.yml b/bruno/devices/get_shelf.yml index 7ea4bbc2..a35e6d33 100644 --- a/bruno/devices/get_shelf.yml +++ b/bruno/devices/get_shelf.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Shelf diff --git a/bruno/devices/get_sidecar_config.yml b/bruno/devices/get_sidecar_config.yml index 1175901a..8c549859 100644 --- a/bruno/devices/get_sidecar_config.yml +++ b/bruno/devices/get_sidecar_config.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device Sidecar Config diff --git a/bruno/devices/list_devices.yml b/bruno/devices/list_devices.yml index 88b08142..0da76069 100644 --- a/bruno/devices/list_devices.yml +++ b/bruno/devices/list_devices.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List User Devices diff --git a/bruno/devices/list_pending_registrations.yml b/bruno/devices/list_pending_registrations.yml index f69d1dc1..f7e00f11 100644 --- a/bruno/devices/list_pending_registrations.yml +++ b/bruno/devices/list_pending_registrations.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Pending Device Registrations:<|end|><|start|><|start|>assistant to=edit<|channel|>analysis <|constrain|>xml<|message|> diff --git a/bruno/devices/register_device.yml b/bruno/devices/register_device.yml index 2be1fe33..100d2883 100644 --- a/bruno/devices/register_device.yml +++ b/bruno/devices/register_device.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Register Device diff --git a/bruno/devices/reject_device_registration.yml b/bruno/devices/reject_device_registration.yml index 2654dab7..5683c95b 100644 --- a/bruno/devices/reject_device_registration.yml +++ b/bruno/devices/reject_device_registration.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Reject Device Registration diff --git a/bruno/devices/remove_from_shelf.yml b/bruno/devices/remove_from_shelf.yml index 1fa292b8..d926dd50 100644 --- a/bruno/devices/remove_from_shelf.yml +++ b/bruno/devices/remove_from_shelf.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Remove from Shelf diff --git a/bruno/devices/update_device.yml b/bruno/devices/update_device.yml index 18a0f5d3..3ca5513f 100644 --- a/bruno/devices/update_device.yml +++ b/bruno/devices/update_device.yml @@ -22,6 +22,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Device diff --git a/bruno/highlights/create_highlight.yml b/bruno/highlights/create_highlight.yml index e0258b29..1e168da5 100644 --- a/bruno/highlights/create_highlight.yml +++ b/bruno/highlights/create_highlight.yml @@ -24,6 +24,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Highlight diff --git a/bruno/highlights/delete_highlight.yml b/bruno/highlights/delete_highlight.yml index e26c2832..986f4c3a 100644 --- a/bruno/highlights/delete_highlight.yml +++ b/bruno/highlights/delete_highlight.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Highlight diff --git a/bruno/highlights/get_highlights.yml b/bruno/highlights/get_highlights.yml index 9b716d00..eaed1818 100644 --- a/bruno/highlights/get_highlights.yml +++ b/bruno/highlights/get_highlights.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Highlights diff --git a/bruno/highlights/update_highlight.yml b/bruno/highlights/update_highlight.yml index 6d5958d8..b542fe94 100644 --- a/bruno/highlights/update_highlight.yml +++ b/bruno/highlights/update_highlight.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Highlight diff --git a/bruno/jobs/create_job.yml b/bruno/jobs/create_job.yml index 13266476..4fc80403 100644 --- a/bruno/jobs/create_job.yml +++ b/bruno/jobs/create_job.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 202", function() { + expect(res.status).to.eql(202); + }); + docs: content: |- # Create Job diff --git a/bruno/jobs/get_job_status.yml b/bruno/jobs/get_job_status.yml index 840452f4..bc6eadfa 100644 --- a/bruno/jobs/get_job_status.yml +++ b/bruno/jobs/get_job_status.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Job Status diff --git a/bruno/kobo/analytics_gettests.yml b/bruno/kobo/analytics_gettests.yml index b60319b3..b3b7004c 100644 --- a/bruno/kobo/analytics_gettests.yml +++ b/bruno/kobo/analytics_gettests.yml @@ -22,6 +22,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Analytics GetTests diff --git a/bruno/kobo/bookmark_sync.yml b/bruno/kobo/bookmark_sync.yml index 8ddf655d..ba576581 100644 --- a/bruno/kobo/bookmark_sync.yml +++ b/bruno/kobo/bookmark_sync.yml @@ -30,6 +30,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Bookmark Sync diff --git a/bruno/kobo/initialization.yml b/bruno/kobo/initialization.yml index 6d02a9a7..bfb3967d 100644 --- a/bruno/kobo/initialization.yml +++ b/bruno/kobo/initialization.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Kobo Initialization diff --git a/bruno/kobo/markup_sync.yml b/bruno/kobo/markup_sync.yml index 944366ba..bf59e7de 100644 --- a/bruno/kobo/markup_sync.yml +++ b/bruno/kobo/markup_sync.yml @@ -34,6 +34,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Markup Sync diff --git a/bruno/kobo/sync_from_server.yml b/bruno/kobo/sync_from_server.yml index 77bc59ae..9838db7c 100644 --- a/bruno/kobo/sync_from_server.yml +++ b/bruno/kobo/sync_from_server.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Sync From Server diff --git a/bruno/koreader/get_library.yml b/bruno/koreader/get_library.yml index 36a0a4a9..6550e2d0 100644 --- a/bruno/koreader/get_library.yml +++ b/bruno/koreader/get_library.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library diff --git a/bruno/koreader/get_metadata.yml b/bruno/koreader/get_metadata.yml index 9b73e810..d6b572d3 100644 --- a/bruno/koreader/get_metadata.yml +++ b/bruno/koreader/get_metadata.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Metadata diff --git a/bruno/koreader/get_rating.yml b/bruno/koreader/get_rating.yml index 9b98b8e3..5c88598f 100644 --- a/bruno/koreader/get_rating.yml +++ b/bruno/koreader/get_rating.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get KOReader Rating diff --git a/bruno/koreader/get_review.yml b/bruno/koreader/get_review.yml index cf3f08fb..55dbe336 100644 --- a/bruno/koreader/get_review.yml +++ b/bruno/koreader/get_review.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get KOReader Review diff --git a/bruno/koreader/resolve_book.yml b/bruno/koreader/resolve_book.yml index 18b4f165..32c8ef54 100644 --- a/bruno/koreader/resolve_book.yml +++ b/bruno/koreader/resolve_book.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Resolve Book:<|end|><|start|> assistant to=edit<|channel|>analysis <|constrain|>xml<|message|> diff --git a/bruno/koreader/sync_bookmarks.yml b/bruno/koreader/sync_bookmarks.yml index ab03d2ba..5b09a97b 100644 --- a/bruno/koreader/sync_bookmarks.yml +++ b/bruno/koreader/sync_bookmarks.yml @@ -45,6 +45,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Sync Bookmarks diff --git a/bruno/koreader/sync_progress.yml b/bruno/koreader/sync_progress.yml index 4bc1445c..06f3d33f 100644 --- a/bruno/koreader/sync_progress.yml +++ b/bruno/koreader/sync_progress.yml @@ -39,6 +39,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 202", function() { + expect(res.status).to.eql(202); + }); + docs: content: |- # Sync Progress diff --git a/bruno/libraries/add_library_folder.yml b/bruno/libraries/add_library_folder.yml index 162031b5..76f88a45 100644 --- a/bruno/libraries/add_library_folder.yml +++ b/bruno/libraries/add_library_folder.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Add Library Folder diff --git a/bruno/libraries/browse-folders.yml b/bruno/libraries/browse-folders.yml index 374825bb..57a63021 100644 --- a/bruno/libraries/browse-folders.yml +++ b/bruno/libraries/browse-folders.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Browse Library Folders diff --git a/bruno/libraries/create_library.yml b/bruno/libraries/create_library.yml index 9bd0a4ba..90770e5b 100644 --- a/bruno/libraries/create_library.yml +++ b/bruno/libraries/create_library.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Library diff --git a/bruno/libraries/delete_library.yml b/bruno/libraries/delete_library.yml index 2ccff0fe..02b7bdf1 100644 --- a/bruno/libraries/delete_library.yml +++ b/bruno/libraries/delete_library.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Library diff --git a/bruno/libraries/delete_library_folder.yml b/bruno/libraries/delete_library_folder.yml index 51195e60..cf728d3d 100644 --- a/bruno/libraries/delete_library_folder.yml +++ b/bruno/libraries/delete_library_folder.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Library Folder diff --git a/bruno/libraries/get_library.yml b/bruno/libraries/get_library.yml index afc93b71..52101186 100644 --- a/bruno/libraries/get_library.yml +++ b/bruno/libraries/get_library.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library Details:<|end|><|start|> assistant to=edit<|channel|>analysis <|constrain|>xml<|message|> diff --git a/bruno/libraries/get_library_folders.yml b/bruno/libraries/get_library_folders.yml index 4aa41700..ed5485d1 100644 --- a/bruno/libraries/get_library_folders.yml +++ b/bruno/libraries/get_library_folders.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library Folders diff --git a/bruno/libraries/get_library_media_items.yml b/bruno/libraries/get_library_media_items.yml index 45c01628..9830d425 100644 --- a/bruno/libraries/get_library_media_items.yml +++ b/bruno/libraries/get_library_media_items.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library Media Items diff --git a/bruno/libraries/get_library_stats.yml b/bruno/libraries/get_library_stats.yml index 0815f80f..d6db4d81 100644 --- a/bruno/libraries/get_library_stats.yml +++ b/bruno/libraries/get_library_stats.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library Statistics diff --git a/bruno/libraries/get_library_types.yml b/bruno/libraries/get_library_types.yml index fe6f085d..5f740653 100644 --- a/bruno/libraries/get_library_types.yml +++ b/bruno/libraries/get_library_types.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library Types diff --git a/bruno/libraries/get_processing_issue_stats.yml b/bruno/libraries/get_processing_issue_stats.yml index 880ead73..3640993a 100644 --- a/bruno/libraries/get_processing_issue_stats.yml +++ b/bruno/libraries/get_processing_issue_stats.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Processing Issue Stats diff --git a/bruno/libraries/get_scan_settings.yml b/bruno/libraries/get_scan_settings.yml index 1eff8dc9..ac97184f 100644 --- a/bruno/libraries/get_scan_settings.yml +++ b/bruno/libraries/get_scan_settings.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Scan Settings diff --git a/bruno/libraries/get_visible_libraries.yml b/bruno/libraries/get_visible_libraries.yml index e5fd3a75..5cdd4612 100644 --- a/bruno/libraries/get_visible_libraries.yml +++ b/bruno/libraries/get_visible_libraries.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Visible Libraries diff --git a/bruno/libraries/list_libraries.yml b/bruno/libraries/list_libraries.yml index 3d6dc8eb..c17d0970 100644 --- a/bruno/libraries/list_libraries.yml +++ b/bruno/libraries/list_libraries.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Libraries (Admin) diff --git a/bruno/libraries/list_processing_issues.yml b/bruno/libraries/list_processing_issues.yml index 85319f05..a1afd6e4 100644 --- a/bruno/libraries/list_processing_issues.yml +++ b/bruno/libraries/list_processing_issues.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Processing Issues diff --git a/bruno/libraries/set_library_visibility.yml b/bruno/libraries/set_library_visibility.yml index f0cf04e1..24a2b1c5 100644 --- a/bruno/libraries/set_library_visibility.yml +++ b/bruno/libraries/set_library_visibility.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Set Library Visibility diff --git a/bruno/libraries/update_library.yml b/bruno/libraries/update_library.yml index bd93d13c..9aef0093 100644 --- a/bruno/libraries/update_library.yml +++ b/bruno/libraries/update_library.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Library diff --git a/bruno/media-items/bulk_delete_media_items.yml b/bruno/media-items/bulk_delete_media_items.yml index 79bea343..4aec497c 100644 --- a/bruno/media-items/bulk_delete_media_items.yml +++ b/bruno/media-items/bulk_delete_media_items.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Bulk Delete Media Items diff --git a/bruno/media-items/bulk_update_media_items.yml b/bruno/media-items/bulk_update_media_items.yml index a9c03a25..5dd13a98 100644 --- a/bruno/media-items/bulk_update_media_items.yml +++ b/bruno/media-items/bulk_update_media_items.yml @@ -35,6 +35,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Bulk Update Media Items diff --git a/bruno/media-items/create_media_item.yml b/bruno/media-items/create_media_item.yml index 2018e75a..e2b5612b 100644 --- a/bruno/media-items/create_media_item.yml +++ b/bruno/media-items/create_media_item.yml @@ -31,6 +31,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Media Item diff --git a/bruno/media-items/delete_media_bookmark.yml b/bruno/media-items/delete_media_bookmark.yml index b9913c5c..64ebaf89 100644 --- a/bruno/media-items/delete_media_bookmark.yml +++ b/bruno/media-items/delete_media_bookmark.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Media Bookmark diff --git a/bruno/media-items/delete_media_item.yml b/bruno/media-items/delete_media_item.yml index dcf52bb5..864373cc 100644 --- a/bruno/media-items/delete_media_item.yml +++ b/bruno/media-items/delete_media_item.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Media Item diff --git a/bruno/media-items/deleted_annotations.yml b/bruno/media-items/deleted_annotations.yml index e307aab5..c4fac057 100644 --- a/bruno/media-items/deleted_annotations.yml +++ b/bruno/media-items/deleted_annotations.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Deleted Annotations History diff --git a/bruno/media-items/download_media_item_file.yml b/bruno/media-items/download_media_item_file.yml index c80a7a2d..bb90fbd6 100644 --- a/bruno/media-items/download_media_item_file.yml +++ b/bruno/media-items/download_media_item_file.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Download Media Item File diff --git a/bruno/media-items/filter_count.yml b/bruno/media-items/filter_count.yml index 2893535b..7367aa07 100644 --- a/bruno/media-items/filter_count.yml +++ b/bruno/media-items/filter_count.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Filter Count diff --git a/bruno/media-items/get_audio_chapters.yml b/bruno/media-items/get_audio_chapters.yml index 38e50e14..65006be6 100644 --- a/bruno/media-items/get_audio_chapters.yml +++ b/bruno/media-items/get_audio_chapters.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Audio Chapters diff --git a/bruno/media-items/get_audio_track.yml b/bruno/media-items/get_audio_track.yml index d6f98a3c..b09ae748 100644 --- a/bruno/media-items/get_audio_track.yml +++ b/bruno/media-items/get_audio_track.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Audio Track diff --git a/bruno/media-items/get_facets.yml b/bruno/media-items/get_facets.yml index 8053e7fe..b9b3dcc6 100644 --- a/bruno/media-items/get_facets.yml +++ b/bruno/media-items/get_facets.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Item Facets diff --git a/bruno/media-items/get_media_bookmarks.yml b/bruno/media-items/get_media_bookmarks.yml index afc37d76..11798502 100644 --- a/bruno/media-items/get_media_bookmarks.yml +++ b/bruno/media-items/get_media_bookmarks.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Bookmarks diff --git a/bruno/media-items/get_media_highlight.yml b/bruno/media-items/get_media_highlight.yml index 97623215..8a251e12 100644 --- a/bruno/media-items/get_media_highlight.yml +++ b/bruno/media-items/get_media_highlight.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Highlight diff --git a/bruno/media-items/get_media_item.yml b/bruno/media-items/get_media_item.yml index 47f2de13..ee05ec16 100644 --- a/bruno/media-items/get_media_item.yml +++ b/bruno/media-items/get_media_item.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Item diff --git a/bruno/media-items/get_media_note.yml b/bruno/media-items/get_media_note.yml index dc750d4c..f5bf08f1 100644 --- a/bruno/media-items/get_media_note.yml +++ b/bruno/media-items/get_media_note.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Note diff --git a/bruno/media-items/get_media_review.yml b/bruno/media-items/get_media_review.yml index fd163c1b..7121ade7 100644 --- a/bruno/media-items/get_media_review.yml +++ b/bruno/media-items/get_media_review.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Review diff --git a/bruno/media-items/list_media_items.yml b/bruno/media-items/list_media_items.yml index 99fed17e..e952c99a 100644 --- a/bruno/media-items/list_media_items.yml +++ b/bruno/media-items/list_media_items.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Media Items diff --git a/bruno/media-items/scenarios/get-media-item-invalid-uuid-400.yml b/bruno/media-items/scenarios/get-media-item-invalid-uuid-400.yml new file mode 100644 index 00000000..107f66ef --- /dev/null +++ b/bruno/media-items/scenarios/get-media-item-invalid-uuid-400.yml @@ -0,0 +1,30 @@ +info: + name: Get Media Item - Invalid UUID (400) + type: http + seq: 1 + +http: + method: GET + url: '{{base_url}}/api/media-items/not-a-uuid' + auth: inherit +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 400", function() { + expect(res.status).to.eql(400); + }); + +docs: + content: |- + # Get Media Item - Invalid UUID (400) + + Malformed media item ids must be rejected with 400, never passed to + the database layer. Repeats safely. + type: text/markdown diff --git a/bruno/media-items/scenarios/get-media-item-unknown-404.yml b/bruno/media-items/scenarios/get-media-item-unknown-404.yml new file mode 100644 index 00000000..37103469 --- /dev/null +++ b/bruno/media-items/scenarios/get-media-item-unknown-404.yml @@ -0,0 +1,29 @@ +info: + name: Get Media Item - Unknown (404) + type: http + seq: 2 + +http: + method: GET + url: '{{base_url}}/api/media-items/00000000-0000-0000-0000-000000000000' + auth: inherit +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 404", function() { + expect(res.status).to.eql(404); + }); + +docs: + content: |- + # Get Media Item - Unknown (404) + + A well-formed but nonexistent media item id must 404. Repeats safely. + type: text/markdown diff --git a/bruno/media-items/search_media_items.yml b/bruno/media-items/search_media_items.yml index 83cc4cb9..0d38ae89 100644 --- a/bruno/media-items/search_media_items.yml +++ b/bruno/media-items/search_media_items.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Search Media Items (Unified):<|end|><|start|>edit to=assistant<|channel|>analysis<|message|>Edited docs/developer/api/media-items/create_media_item.md (1 replacement)<|end|><|start|>edit to=assistant<|channel|>analysis<|message|>Edited docs/developer/api/media-items/update_media_item.md (1 replacement)<|end|><|start|>edit to=assistant<|channel|>analysis<|channel|>Edited docs/developer/api/media-items/delete_media_item.md (1 replacement)<|end|><|start|>edit to=assistant<|channel|>analysis<|message|>Edited docs/developer/api/media-items/search_media_items.md (1 replacement) diff --git a/bruno/media-items/unarchive_media_item.yml b/bruno/media-items/unarchive_media_item.yml index f3224bfc..54821062 100644 --- a/bruno/media-items/unarchive_media_item.yml +++ b/bruno/media-items/unarchive_media_item.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Unarchive Media Item diff --git a/bruno/media-items/update_media_item.yml b/bruno/media-items/update_media_item.yml index 460f6df3..fbbd4a49 100644 --- a/bruno/media-items/update_media_item.yml +++ b/bruno/media-items/update_media_item.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Media Item diff --git a/bruno/notes/create_note.yml b/bruno/notes/create_note.yml index 468c3394..71352ec6 100644 --- a/bruno/notes/create_note.yml +++ b/bruno/notes/create_note.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Note diff --git a/bruno/notes/delete_note.yml b/bruno/notes/delete_note.yml index b692bae3..10eb54f4 100644 --- a/bruno/notes/delete_note.yml +++ b/bruno/notes/delete_note.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Note diff --git a/bruno/notes/get_notes.yml b/bruno/notes/get_notes.yml index 35309837..0dcf3d7a 100644 --- a/bruno/notes/get_notes.yml +++ b/bruno/notes/get_notes.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Notes diff --git a/bruno/notes/update_note.yml b/bruno/notes/update_note.yml index 4f56f0b7..bc4bc10f 100644 --- a/bruno/notes/update_note.yml +++ b/bruno/notes/update_note.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Note diff --git a/bruno/opds/acquisition.yml b/bruno/opds/acquisition.yml index a5e54a58..1a710de0 100644 --- a/bruno/opds/acquisition.yml +++ b/bruno/opds/acquisition.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # OPDS Acquisition diff --git a/bruno/opds/feeds.yml b/bruno/opds/feeds.yml index daf93076..a5eaead3 100644 --- a/bruno/opds/feeds.yml +++ b/bruno/opds/feeds.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # OPDS Feeds diff --git a/bruno/opds/publication.yml b/bruno/opds/publication.yml index e25ddce5..2d9314fb 100644 --- a/bruno/opds/publication.yml +++ b/bruno/opds/publication.yml @@ -16,6 +16,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # OPDS Publication diff --git a/bruno/progress/delete_media_progress.yml b/bruno/progress/delete_media_progress.yml index 3ea7df89..7b8668f2 100644 --- a/bruno/progress/delete_media_progress.yml +++ b/bruno/progress/delete_media_progress.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Delete Media Item Progress diff --git a/bruno/progress/get_media_progress.yml b/bruno/progress/get_media_progress.yml index 573ab2bb..1335e565 100644 --- a/bruno/progress/get_media_progress.yml +++ b/bruno/progress/get_media_progress.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Media Item Progress diff --git a/bruno/progress/get_progress_history.yml b/bruno/progress/get_progress_history.yml index 410c1aa6..d796d590 100644 --- a/bruno/progress/get_progress_history.yml +++ b/bruno/progress/get_progress_history.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Progress History diff --git a/bruno/progress/update_media_progress.yml b/bruno/progress/update_media_progress.yml index 79e02032..24bb6700 100644 --- a/bruno/progress/update_media_progress.yml +++ b/bruno/progress/update_media_progress.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Media Item Progress diff --git a/bruno/queue/clear_device_queue.yml b/bruno/queue/clear_device_queue.yml index a1a9f5ba..cc38d51d 100644 --- a/bruno/queue/clear_device_queue.yml +++ b/bruno/queue/clear_device_queue.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Clear Device Queue diff --git a/bruno/queue/delete_queue_item.yml b/bruno/queue/delete_queue_item.yml index 21952f4d..e77b0a02 100644 --- a/bruno/queue/delete_queue_item.yml +++ b/bruno/queue/delete_queue_item.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 204", function() { + expect(res.status).to.eql(204); + }); + docs: content: |- # Delete Queue Item diff --git a/bruno/queue/get_device_queue_stats.yml b/bruno/queue/get_device_queue_stats.yml index 3c6d6214..1a02a4cb 100644 --- a/bruno/queue/get_device_queue_stats.yml +++ b/bruno/queue/get_device_queue_stats.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Device Queue Statistics diff --git a/bruno/queue/list_all_queue_items.yml b/bruno/queue/list_all_queue_items.yml index 49a8b611..54beb214 100644 --- a/bruno/queue/list_all_queue_items.yml +++ b/bruno/queue/list_all_queue_items.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List All Queue Items (Admin) diff --git a/bruno/queue/list_device_queue_items.yml b/bruno/queue/list_device_queue_items.yml index 24b914d0..1fa7a646 100644 --- a/bruno/queue/list_device_queue_items.yml +++ b/bruno/queue/list_device_queue_items.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Device Queue Items diff --git a/bruno/queue/retry_queue_item.yml b/bruno/queue/retry_queue_item.yml index bfbb8bac..eaa5ae82 100644 --- a/bruno/queue/retry_queue_item.yml +++ b/bruno/queue/retry_queue_item.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Retry Queue Item diff --git a/bruno/ratings/create_rating.yml b/bruno/ratings/create_rating.yml index d9f75948..aa7f14dd 100644 --- a/bruno/ratings/create_rating.yml +++ b/bruno/ratings/create_rating.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 201", function() { + expect(res.status).to.eql(201); + }); + docs: content: |- # Create Rating diff --git a/bruno/ratings/delete_rating.yml b/bruno/ratings/delete_rating.yml index c12ca604..5279f73a 100644 --- a/bruno/ratings/delete_rating.yml +++ b/bruno/ratings/delete_rating.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Delete Rating diff --git a/bruno/ratings/get_ratings.yml b/bruno/ratings/get_ratings.yml index 77587ddb..b7cf86c5 100644 --- a/bruno/ratings/get_ratings.yml +++ b/bruno/ratings/get_ratings.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Rating diff --git a/bruno/ratings/update_rating.yml b/bruno/ratings/update_rating.yml index 7bce46c0..58829571 100644 --- a/bruno/ratings/update_rating.yml +++ b/bruno/ratings/update_rating.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Rating diff --git a/bruno/saved-filters/get_saved_filter.yml b/bruno/saved-filters/get_saved_filter.yml index dfdcef41..d429a3ef 100644 --- a/bruno/saved-filters/get_saved_filter.yml +++ b/bruno/saved-filters/get_saved_filter.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Saved Filter diff --git a/bruno/saved-filters/list_saved_filters.yml b/bruno/saved-filters/list_saved_filters.yml index 4e6fb935..427cfd1a 100644 --- a/bruno/saved-filters/list_saved_filters.yml +++ b/bruno/saved-filters/list_saved_filters.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Saved Filters diff --git a/bruno/scanner/get_active_scans.yml b/bruno/scanner/get_active_scans.yml index 3f5fb354..551d7af6 100644 --- a/bruno/scanner/get_active_scans.yml +++ b/bruno/scanner/get_active_scans.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Active Scans diff --git a/bruno/scanner/get_scan_status.yml b/bruno/scanner/get_scan_status.yml index 72e50817..25ad8f8a 100644 --- a/bruno/scanner/get_scan_status.yml +++ b/bruno/scanner/get_scan_status.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Scan Status diff --git a/bruno/scanner/get_scan_summary.yml b/bruno/scanner/get_scan_summary.yml index 72478913..2dfd6192 100644 --- a/bruno/scanner/get_scan_summary.yml +++ b/bruno/scanner/get_scan_summary.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Scan Summary diff --git a/bruno/scanner/get_watch_mode_status.yml b/bruno/scanner/get_watch_mode_status.yml index fbfa0e63..35b8ca7f 100644 --- a/bruno/scanner/get_watch_mode_status.yml +++ b/bruno/scanner/get_watch_mode_status.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Watch Mode Status diff --git a/bruno/scanner/scan_library.yml b/bruno/scanner/scan_library.yml index 076ef455..58e76fa8 100644 --- a/bruno/scanner/scan_library.yml +++ b/bruno/scanner/scan_library.yml @@ -20,6 +20,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 202", function() { + expect(res.status).to.eql(202); + }); + docs: content: |- # Scan Library diff --git a/bruno/scanner/scenarios/scan-status-unknown-job-404.yml b/bruno/scanner/scenarios/scan-status-unknown-job-404.yml new file mode 100644 index 00000000..57cac5dc --- /dev/null +++ b/bruno/scanner/scenarios/scan-status-unknown-job-404.yml @@ -0,0 +1,29 @@ +info: + name: Scan Status - Unknown Job (404) + type: http + seq: 1 + +http: + method: GET + url: '{{base_url}}/api/scanner/status/00000000-0000-0000-0000-000000000000' + auth: inherit +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 404", function() { + expect(res.status).to.eql(404); + }); + +docs: + content: |- + # Scan Status - Unknown Job (404) + + Unknown job ids must 404. Repeats safely. + type: text/markdown diff --git a/bruno/scanner/start_scanner.yml b/bruno/scanner/start_scanner.yml index c1182da5..7e33a62b 100644 --- a/bruno/scanner/start_scanner.yml +++ b/bruno/scanner/start_scanner.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Start Scanner diff --git a/bruno/scanner/start_watch_mode.yml b/bruno/scanner/start_watch_mode.yml index 7ccd8a23..5674203a 100644 --- a/bruno/scanner/start_watch_mode.yml +++ b/bruno/scanner/start_watch_mode.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Start Watch Mode diff --git a/bruno/scanner/stop_scanner.yml b/bruno/scanner/stop_scanner.yml index 17fc71c7..4171c11d 100644 --- a/bruno/scanner/stop_scanner.yml +++ b/bruno/scanner/stop_scanner.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Stop Scanner diff --git a/bruno/scanner/stop_watch_mode.yml b/bruno/scanner/stop_watch_mode.yml index f3a743a6..bdb17cf8 100644 --- a/bruno/scanner/stop_watch_mode.yml +++ b/bruno/scanner/stop_watch_mode.yml @@ -19,6 +19,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Stop Watch Mode diff --git a/bruno/series/get_series_books.yml b/bruno/series/get_series_books.yml index b5ac2202..c5deaefc 100644 --- a/bruno/series/get_series_books.yml +++ b/bruno/series/get_series_books.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Series Books diff --git a/bruno/series/list_series.yml b/bruno/series/list_series.yml index db36fdfa..e34b4229 100644 --- a/bruno/series/list_series.yml +++ b/bruno/series/list_series.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # List Series diff --git a/bruno/stats/get_book_stats.yml b/bruno/stats/get_book_stats.yml index 66a308e0..344c192f 100644 --- a/bruno/stats/get_book_stats.yml +++ b/bruno/stats/get_book_stats.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Book Stats diff --git a/bruno/stats/get_library_usage.yml b/bruno/stats/get_library_usage.yml index 1740fd65..54f237e8 100644 --- a/bruno/stats/get_library_usage.yml +++ b/bruno/stats/get_library_usage.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Library Usage Statistics diff --git a/bruno/stats/get_summary.yml b/bruno/stats/get_summary.yml index 4d2e2b1c..aab1f7a4 100644 --- a/bruno/stats/get_summary.yml +++ b/bruno/stats/get_summary.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Reading Summary diff --git a/bruno/stats/get_this_week.yml b/bruno/stats/get_this_week.yml index 0a84daa8..a6ff844d 100644 --- a/bruno/stats/get_this_week.yml +++ b/bruno/stats/get_this_week.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get This Week's Stats diff --git a/bruno/stats/scenarios/summary-invalid-range-400.yml b/bruno/stats/scenarios/summary-invalid-range-400.yml new file mode 100644 index 00000000..3be01add --- /dev/null +++ b/bruno/stats/scenarios/summary-invalid-range-400.yml @@ -0,0 +1,29 @@ +info: + name: Stats Summary - Invalid Range (400) + type: http + seq: 1 + +http: + method: GET + url: '{{base_url}}/api/stats/summary?range=bogus' + auth: inherit +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +runtime: + scripts: + - type: tests + code: |- + test("status is 400", function() { + expect(res.status).to.eql(400); + }); + +docs: + content: |- + # Stats Summary - Invalid Range (400) + + range accepts 7|30|90|year; anything else must 400. Repeats safely. + type: text/markdown diff --git a/bruno/system/get_config.yml b/bruno/system/get_config.yml index 2684ee0c..de145e57 100644 --- a/bruno/system/get_config.yml +++ b/bruno/system/get_config.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get System Configuration diff --git a/bruno/system/get_settings.yml b/bruno/system/get_settings.yml index ff8968e7..0439a32c 100644 --- a/bruno/system/get_settings.yml +++ b/bruno/system/get_settings.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get System Settings diff --git a/bruno/users/change_password.yml b/bruno/users/change_password.yml index e9d24da1..2fb45ea7 100644 --- a/bruno/users/change_password.yml +++ b/bruno/users/change_password.yml @@ -21,6 +21,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Change Password diff --git a/bruno/users/delete_user.yml b/bruno/users/delete_user.yml index c5e0adb0..ec04b58f 100644 --- a/bruno/users/delete_user.yml +++ b/bruno/users/delete_user.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Delete User diff --git a/bruno/users/get_profile.yml b/bruno/users/get_profile.yml index ee63c780..3e2627c4 100644 --- a/bruno/users/get_profile.yml +++ b/bruno/users/get_profile.yml @@ -13,6 +13,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Get Current User Profile diff --git a/bruno/users/update_profile.yml b/bruno/users/update_profile.yml index e5436c88..08e6e182 100644 --- a/bruno/users/update_profile.yml +++ b/bruno/users/update_profile.yml @@ -23,6 +23,14 @@ settings: followRedirects: true maxRedirects: 5 +runtime: + scripts: + - type: tests + code: |- + test("status is 200", function() { + expect(res.status).to.eql(200); + }); + docs: content: |- # Update Profile diff --git a/docs/developer/openapi.yaml b/docs/developer/openapi.yaml new file mode 100644 index 00000000..46518964 --- /dev/null +++ b/docs/developer/openapi.yaml @@ -0,0 +1,2421 @@ +openapi: 3.0.3 +info: + title: Bookhoard API + version: '2026.10.24' + description: | + Generated from the canonical API docs (docs/developer/api/**) by + internal/docs (go test ./internal/docs -run TestBrunoMirrorsDocs -update). + The docs site is the shape reference; this file lists the surface. +servers: + - url: / +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT +paths: + "/api/admin/hash-conflicts": + get: + tags: [admin] + summary: "Hash Conflicts (API)" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/analytics/device-usage": + get: + tags: [analytics] + summary: "Get Device Usage" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/analytics/popular-books": + get: + tags: [analytics] + summary: "Get Popular Books" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/analytics/reading-stats": + get: + tags: [analytics] + summary: "Get Reading Statistics" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/auth/login": + post: + tags: [authentication] + summary: "Login User" + security: [] + requestBody: + content: + application/json: + example: |- + { + "login": "user@example.com", + "password": "SecureP@ss123!" + } + responses: + '200': + description: OK + "/api/auth/logout": + post: + tags: [authentication] + summary: "Logout User" + security: [] + responses: + '200': + description: OK + "/api/auth/password": + put: + tags: [authentication] + summary: "Change Password" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "current_password": "OldPassword123!", + "new_password": "NewSecureP@ss123!", + "confirm_password": "NewSecureP@ss123!" + } + responses: + '200': + description: OK + "/api/auth/password-rules": + get: + tags: [authentication] + summary: "Password Rules" + security: [] + responses: + '200': + description: OK + "/api/auth/profile": + delete: + tags: [authentication] + summary: "Delete User" + security: + - bearerAuth: [] + responses: + '200': + description: OK + get: + tags: [authentication] + summary: "Get Current User Profile" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [authentication] + summary: "Update Profile" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "username": "newusername", + "email": "newemail@example.com", + "first_name": "John", + "last_name": "Smith", + "theme": "dracula" + } + responses: + '200': + description: OK + "/api/auth/refresh": + post: + tags: [authentication] + summary: "Refresh Token" + security: [] + requestBody: + content: + application/json: + example: |- + { + "refresh_token": "d4f5g6h7..." + } + responses: + '200': + description: OK + "/api/auth/register": + post: + tags: [authentication] + summary: "Register User" + security: [] + requestBody: + content: + application/json: + example: |- + { + "email": "user@example.com", + "username": "john", + "password": "SecureP@ss123!", + "first_name": "John", + "last_name": "Doe" + } + responses: + '201': + description: Created + "/api/auth/users": + get: + tags: [authentication] + summary: "List Users" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/auth/users/{id}/max-devices": + put: + tags: [authentication] + summary: "Update User Max Devices" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "max_devices": 15 + } + responses: + '200': + description: OK + "/api/books/match": + get: + tags: [book-matching] + summary: "Get Book Matches" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/collections": + get: + tags: [collections] + summary: "List Collections" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [collections] + summary: "Create Collection" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "name": "To Read", + "description": "Books I want to read soon", + "color": "#00FF00", + "icon": "📖", + "collection_type": "static", + "auto_assign_rules": [], + "view_settings": { "kobo": { "view_mode": "grid" } } + } + responses: + '200': + description: Success + "/api/collections/bulk-add-books": + post: + tags: [collections] + summary: "Bulk Add Books" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "operations": [ + { + "collection_id": "550e8400-e29b-41d4-a716-446655440000", + "book_ids": [ + "660e8400-e29b-41d4-a716-446655440001" + ] + } + ] + } + responses: + '200': + description: Success + "/api/collections/preview": + post: + tags: [collections] + summary: "Preview Collection" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "rules": [], + "manual_book_ids": [], + "limit": 20 + } + responses: + '200': + description: Success + "/api/collections/test-rules": + post: + tags: [collections] + summary: "Test Rule" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "rules": [ + { + "field": "genre", + "operator": "equals", + "value": "Science Fiction" + }, + { + "field": "author", + "operator": "contains", + "value": "Asimov" + } + ] + } + responses: + '200': + description: OK + "/api/collections/{id}": + delete: + tags: [collections] + summary: "Delete Collection" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [collections] + summary: "Get Collection" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [collections] + summary: "Update Collection" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "name": "To Read (2026)", + "description": "Updated description", + "color": "#33CCFF", + "icon": "📚", + "auto_assign_rules": [], + "view_settings": { "hide_finished": true }, + "hide_finished": ["true"], + "show_up_next": ["true"] + } + responses: + '200': + description: Success + "/api/collections/{id}/books": + get: + tags: [collections] + summary: "Get Book Collections" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [collections] + summary: "Bulk Assign Books" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "book_ids": [ + "660e8400-e29b-41d4-a716-446655440000", + "770e8400-e29b-41d4-a716-446655440000", + "880e8400-e29b-41d4-a716-446655440000" + ] + } + responses: + '204': + description: No Content + "/api/collections/{id}/books/bulk-remove": + post: + tags: [collections] + summary: "Bulk Remove Books" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "book_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "660e8400-e29b-41d4-a716-446655440001" + ] + } + responses: + '200': + description: Success + "/api/collections/{id}/items/order": + put: + tags: [collections] + summary: "Reorder Collection Items" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "item_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "660e8400-e29b-41d4-a716-446655440001" + ] + } + responses: + '200': + description: Success + "/api/collections/{id}/rules": + post: + tags: [collections] + summary: "Add Auto-Assign Rule" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "field": "genre", + "operator": "equals", + "value": "Science Fiction", + "priority": 1, + "enabled": true + } + responses: + '201': + description: Created + "/api/collections/{id}/rules/{ruleId}": + delete: + tags: [collections] + summary: "Remove Auto-Assign Rule" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + put: + tags: [collections] + summary: "Update Collection Rule" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "enabled": false + } + responses: + '200': + description: Success + "/api/conflicts": + get: + tags: [conflicts] + summary: "List Conflicts" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/conflicts/bulk-dismiss": + post: + tags: [conflicts] + summary: "Bulk Dismiss Conflicts" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "conflict_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "660e8400-e29b-41d4-a716-446655440001" + ] + } + responses: + '200': + description: OK + "/api/conflicts/bulk-resolve": + post: + tags: [conflicts] + summary: "Bulk Resolve Conflicts" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "conflict_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "660e8400-e29b-41d4-a716-446655440001" + ], + "strategy": "highest_progress" + } + responses: + '200': + description: OK + "/api/conflicts/dismiss-all": + post: + tags: [conflicts] + summary: "Dismiss All Resolved" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/conflicts/{id}": + delete: + tags: [conflicts] + summary: "Delete Conflict" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [conflicts] + summary: "Get Conflict Details" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/conflicts/{id}/resolve": + post: + tags: [conflicts] + summary: "Resolve Conflict" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "winner": "kobo" + } + responses: + '200': + description: OK + "/api/dashboard/preferences": + get: + tags: [dashboard] + summary: "Get Dashboard Preferences" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [dashboard] + summary: "Update Dashboard Preferences" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "hidden_collections": ["not-started"], + "collection_order": ["recently-added", "continue-reading", "recently-read"], + "items_per_section": 20 + } + responses: + '200': + description: Success + "/api/dashboard/restore-system-collection": + post: + tags: [dashboard] + summary: "Restore System Collection" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "collection_name": "Continue Reading", + "reset_type": "full" + } + responses: + '200': + description: Success + "/api/dashboard/sections": + get: + tags: [dashboard] + summary: "Get Dashboard Sections" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices": + get: + tags: [devices] + summary: "List User Devices" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/approve/{registration_id}": + get: + tags: [devices] + summary: "Approve Device Registration" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/pending": + get: + tags: [devices] + summary: "List Pending Device Registrations:\u003c|end|\u003e\u003c|start|\u003e\u003c|start|\u003eassistant to=edit\u003c|channel|\u003eanalysis \u003c|constrain|\u003exml\u003c|message|\u003e\u003crfl:parameter name=\"path\"\u003e" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/register": + post: + tags: [devices] + summary: "Register Device" + security: [] + requestBody: + content: + application/json: + example: |- + { + "device_name": "My Kobo Clara", + "device_type": "kobo", + "device_identifier": "hardware-specific-id" + } + responses: + '201': + description: Created + "/api/devices/register/status": + post: + tags: [devices] + summary: "Get Device / Check Registration Status" + security: [] + requestBody: + content: + application/json: + example: |- + { + "registration_id": "registration-uuid" + } + responses: + '200': + description: OK + "/api/devices/reject/{registration_id}": + post: + tags: [devices] + summary: "Reject Device Registration" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/{deviceId}/sync/link-book": + post: + tags: [book-matching] + summary: "Link Book" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "device_file": { + "file_path": "/books/hobbit.epub", + "sha256": "abc123...", + "title": "The Hobbit" + }, + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "confidence_score": 1.0 + } + responses: + '200': + description: OK + post: + tags: [devices] + summary: "Link Book" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "device_file": { + "file_path": "/books/1984.epub", + "sha256": "abc123...", + "title": "1984" + }, + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "confidence_score": 0.95 + } + responses: + '200': + description: Success + "/api/devices/{deviceId}/sync/unlinked-books": + get: + tags: [book-matching] + summary: "Get Unlinked Books" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/{id}": + delete: + tags: [devices] + summary: "Delete Device" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [devices] + summary: "Get Device" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [devices] + summary: "Update Device" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "device_name": "My Kobo Clara", + "sync_enabled": true, + "auto_sync": true, + "sync_frequency_minutes": 60 + } + responses: + '200': + description: OK + "/api/devices/{id}/collections": + get: + tags: [devices] + summary: "Get Device Collection Mappings" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [collections] + summary: "Create Shelf Mapping" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "collection_id": "550e8400-e29b-41d4-a716-446655440000", + "device_shelf_name": "Sci-Fi", + "sync_direction": "bidirectional" + } + responses: + '201': + description: Created + "/api/devices/{id}/collections/{collectionId}": + delete: + tags: [collections] + summary: "Delete Shelf Mapping" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + put: + tags: [devices] + summary: "Update Device Collection Mapping" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "device_shelf_name": "Kobo", + "sync_direction": "bidirectional" + } + responses: + '200': + description: Success + "/api/devices/{id}/file-aliases": + get: + tags: [book-matching] + summary: "Get Device File Aliases" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [book-matching] + summary: "Create Device File Alias" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "media_item_id": "550e8400-e29b-41d4-a716-446655440001", + "file_name": "book.epub", + "file_hash": "abc123..." + } + responses: + '201': + description: Created + "/api/devices/{id}/file-aliases/{aliasId}": + delete: + tags: [book-matching] + summary: "Delete Device File Alias" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + put: + tags: [book-matching] + summary: "Update Device File Alias" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "file_name": "updated-book.epub", + "file_hash": "newhash123..." + } + responses: + '200': + description: OK + "/api/devices/{id}/regenerate-token": + put: + tags: [devices] + summary: "Regenerate Device Token" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/devices/{id}/shelves": + delete: + tags: [devices] + summary: "Remove from Shelf" + security: + - bearerAuth: [] + responses: + '200': + description: OK + get: + tags: [devices] + summary: "Get Shelf" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [devices] + summary: "Add to Shelf" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "media_item_ids": ["550e8400-e29b-41d4-a716-446655440001"], + "shelf_name": "Default", + "shelf_position": 0 + } + responses: + '200': + description: OK + "/api/devices/{id}/shelves/clear": + delete: + tags: [devices] + summary: "Clear Shelf" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/{id}/sidecar": + get: + tags: [devices] + summary: "Get Device Sidecar Config" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/devices/{id}/sidecar/download": + get: + tags: [devices] + summary: "Download Sidecar Config" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/jobs": + post: + tags: [jobs] + summary: "Create Job" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "type": "import", + "params": { "library_id": "990e8400-e29b-41d4-a716-446655440000" } + } + responses: + '202': + description: Accepted + "/api/jobs/{jobId}": + get: + tags: [jobs] + summary: "Get Job Status" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/libraries": + get: + tags: [libraries] + summary: "List Libraries (Admin)" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [libraries] + summary: "Create Library" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "name": "Comics Collection", + "description": "Digital comics", + "type": "comics" + } + responses: + '201': + description: Created + "/api/libraries/browse": + get: + tags: [libraries] + summary: "Browse Library Folders" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/libraries/scan-settings": + get: + tags: [libraries] + summary: "Get Scan Settings" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [libraries] + summary: "Update Scan Settings" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "scan_poll_interval_seconds": 3600, + "auto_scan_enabled": true + } + responses: + '200': + description: Success + "/api/libraries/types": + get: + tags: [libraries] + summary: "Get Library Types" + security: [] + responses: + '200': + description: OK + "/api/libraries/visibility": + get: + tags: [libraries] + summary: "Get Visible Libraries" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [libraries] + summary: "Set Library Visibility" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "library-uuid", + "is_visible": true + } + responses: + '200': + description: OK + "/api/libraries/{id}": + delete: + tags: [libraries] + summary: "Delete Library" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [libraries] + summary: "Get Library Details:\u003c|end|\u003e\u003c|start|\u003e assistant to=edit\u003c|channel|\u003eanalysis \u003c|constrain|\u003exml\u003c|message|\u003e\u003crfl:parameter name=\"path\"\u003e" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [libraries] + summary: "Update Library" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "name": "My Updated Library", + "description": "Updated description" + } + responses: + '200': + description: OK + "/api/libraries/{id}/folders": + delete: + tags: [libraries] + summary: "Delete Library Folder" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "folder_path": "/path/to/library/folder" + } + responses: + '204': + description: No Content + get: + tags: [libraries] + summary: "Get Library Folders" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [libraries] + summary: "Add Library Folder" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "folder_path": "/path/to/comics" + } + responses: + '201': + description: Created + "/api/libraries/{id}/issues/list": + get: + tags: [libraries] + summary: "List Processing Issues" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/libraries/{id}/issues/stats": + get: + tags: [libraries] + summary: "Get Processing Issue Stats" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/libraries/{id}/issues/{issueId}": + delete: + tags: [libraries] + summary: "Delete Processing Issue" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/libraries/{id}/issues/{issueId}/{mediaItemId}/resolve": + post: + tags: [libraries] + summary: "Resolve Processing Issue" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/libraries/{id}/media-items": + get: + tags: [libraries] + summary: "Get Library Media Items" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/libraries/{id}/scan": + post: + tags: [libraries] + summary: "Scan Library" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/libraries/{id}/stats": + get: + tags: [libraries] + summary: "Get Library Statistics" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items": + get: + tags: [media-items] + summary: "List Media Items" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [media-items] + summary: "Create Media Item" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "title": "Example Book Title", + "author": "Jane Doe", + "isbn": "978-0-123456-78-9", + "description": "A great book about technology", + "file_path": "/library/books/example.epub", + "file_size": 1048576, + "mime_type": "application/epub+zip", + "series": "Tech Series", + "series_number": 1, + "tags": ["science-fiction", "technology", "ACME CORP."], + "publisher": "O'Reilly Media", + "contributors": ["John Smith", "ACME CORP."] + } + responses: + '201': + description: Created + "/api/media-items/bulk-delete": + post: + tags: [media-items] + summary: "Bulk Delete Media Items" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "media_item_ids": [ + "550e8400-e29b-41d4-a716-446655440001", + "660e8400-e29b-41d4-a716-446655440002", + "770e8400-e29b-41d4-a716-446655440003" + ] + } + responses: + '200': + description: OK + "/api/media-items/bulk-update": + post: + tags: [media-items] + summary: "Bulk Update Media Items" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "media_item_updates": [ + { + "media_item_id": "550e8400-e29b-41d4-a716-446655440001", + "updates": { + "title": "Updated Title", + "genre": "Science Fiction", + "tags": ["science fiction", "space opera", "ACME CORP."] + } + }, + { + "media_item_id": "660e8400-e29b-41d4-a716-446655440002", + "updates": { + "author": "Updated Author Name", + "language": "en" + } + } + ] + } + responses: + '200': + description: OK + "/api/media-items/facets": + get: + tags: [media-items] + summary: "Get Media Item Facets" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/filter-count": + get: + tags: [media-items] + summary: "Filter Count" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/purge-archived": + post: + tags: [media-items] + summary: "Purge Archived Media Items" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/media-items/search": + get: + tags: [media-items] + summary: "Search Media Items (Unified):\u003c|end|\u003e\u003c|start|\u003eedit to=assistant\u003c|channel|\u003eanalysis\u003c|message|\u003eEdited docs/developer/api/media-items/create_media_item.md (1 replacement)\u003c|end|\u003e\u003c|start|\u003eedit to=assistant\u003c|channel|\u003eanalysis\u003c|message|\u003eEdited docs/developer/api/media-items/update_media_item.md (1 replacement)\u003c|end|\u003e\u003c|start|\u003eedit to=assistant\u003c|channel|\u003eanalysis\u003c|channel|\u003eEdited docs/developer/api/media-items/delete_media_item.md (1 replacement)\u003c|end|\u003e\u003c|start|\u003eedit to=assistant\u003c|channel|\u003eanalysis\u003c|message|\u003eEdited docs/developer/api/media-items/search_media_items.md (1 replacement)" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/{id}": + delete: + tags: [media-items] + summary: "Delete Media Item" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [media-items] + summary: "Get Media Item" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [media-items] + summary: "Update Media Item" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "title": "Updated Title", + "author": "Updated Author", + "description": "Updated description", + "series": "Series", + "series_number": 2 + } + responses: + '200': + description: OK + "/api/media-items/{id}/annotations/deleted": + get: + tags: [media-items] + summary: "Deleted Annotations History" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/{id}/annotations/{annotationId}": + delete: + tags: [media-items] + summary: "Purge Deleted Annotation" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/media-items/{id}/annotations/{annotationId}/restore": + post: + tags: [media-items] + summary: "Restore Deleted Annotation" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/media-items/{id}/audio-speed": + put: + tags: [media-items] + summary: "Set Audio Speed" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "speed": 1.5 + } + responses: + '200': + description: Success + "/api/media-items/{id}/audio/{trackIndex}": + get: + tags: [media-items] + summary: "Get Audio Track" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/{id}/bookmarks": + get: + tags: [media-items] + summary: "Get Media Bookmarks" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [media-items] + summary: "Create Media Bookmark" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "title": "Chapter 3", + "position": "kindle:pos:1:2", + "notes": "revisit this", + "cfi_position": "epubcfi(/6/4)", + "page_number": 42, + "chapter_number": 3, + "percentage": 35.5, + "chapter_reference": 3, + "origin": "android" + } + responses: + '200': + description: Success + "/api/media-items/{id}/bookmarks/{bookmarkId}": + delete: + tags: [media-items] + summary: "Delete Media Bookmark" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + put: + tags: [media-items] + summary: "Update Media Bookmark" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "title": "Chapter 3 (renamed)", + "notes": "updated note", + "position": "kindle:pos:1:2" + } + responses: + '200': + description: Success + "/api/media-items/{id}/chapters": + get: + tags: [media-items] + summary: "Get Audio Chapters" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/{id}/download": + get: + tags: [media-items] + summary: "Download Media Item File" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/media-items/{id}/highlights": + get: + tags: [media-items] + summary: "Get Highlights" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [media-items] + summary: "Create Highlight" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "selection_text": "Highlighted text...", + "start_position": "epubcfi(/6/4/2:15)", + "end_position": "epubcfi(/6/4/2:20)", + "color": "#ffff00", + "percentage_start": 0.45, + "percentage_end": 0.47 + } + responses: + '201': + description: Created + "/api/media-items/{id}/highlights/{highlightId}": + delete: + tags: [media-items] + summary: "Delete Highlight" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [media-items] + summary: "Get Media Highlight" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [media-items] + summary: "Update Highlight" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "selection_text": "Updated text", + "color": "#00ff00" + } + responses: + '200': + description: OK + "/api/media-items/{id}/merge": + post: + tags: [media-items] + summary: "Merge Archived Media Item" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "target_id": "550e8400-e29b-41d4-a716-446655440000" + } + responses: + '200': + description: Success + "/api/media-items/{id}/notes": + get: + tags: [media-items] + summary: "Get Notes" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [media-items] + summary: "Create Note" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "content": "This is a note", + "position": "epubcfi(/6/4/2:15)", + "origin": "web" + } + responses: + '201': + description: Created + "/api/media-items/{id}/notes/{noteId}": + delete: + tags: [media-items] + summary: "Delete Note" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + get: + tags: [media-items] + summary: "Get Media Note" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [media-items] + summary: "Update Note" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "content": "Updated note content", + "position": "epubcfi(/6/4/2:20)" + } + responses: + '200': + description: OK + "/api/media-items/{id}/progress": + delete: + tags: [media-items] + summary: "Delete Media Item Progress" + security: + - bearerAuth: [] + responses: + '200': + description: OK + get: + tags: [media-items] + summary: "Get Media Item Progress" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [media-items] + summary: "Update Media Item Progress" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "percentage": 0.0415, + "context_text": "was at war with one of these Powers it was generally at peace with the other. But what was strange w", + "epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)" + } + responses: + '200': + description: OK + "/api/media-items/{id}/rating": + delete: + tags: [media-items] + summary: "Delete Rating" + security: + - bearerAuth: [] + responses: + '200': + description: OK + get: + tags: [media-items] + summary: "Get Rating" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [media-items] + summary: "Create Rating" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "rating": 8 + } + responses: + '201': + description: Created + put: + tags: [media-items] + summary: "Update Rating" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "rating": 8, + "review": "Great book! Very enjoyable read." + } + responses: + '200': + description: OK + "/api/media-items/{id}/rescan": + post: + tags: [media-items] + summary: "Rescan Media Item" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/media-items/{id}/review": + delete: + tags: [media-items] + summary: "Delete Media Review" + security: + - bearerAuth: [] + responses: + '200': + description: Success + get: + tags: [media-items] + summary: "Get Media Review" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [media-items] + summary: "Upsert Media Review" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "review": "A slow start, but the second act earns it." + } + responses: + '200': + description: Success + "/api/media-items/{id}/unarchive": + post: + tags: [media-items] + summary: "Unarchive Media Item" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + "/api/progress/{id}/history": + get: + tags: [progress] + summary: "Get Progress History" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/queue/devices/{device_id}/clear": + delete: + tags: [queue] + summary: "Clear Device Queue" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/queue/devices/{device_id}/items": + get: + tags: [queue] + summary: "List Device Queue Items" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/queue/devices/{device_id}/stats": + get: + tags: [queue] + summary: "Get Device Queue Statistics" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/queue/items": + get: + tags: [queue] + summary: "List All Queue Items (Admin)" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/queue/items/{item_id}": + delete: + tags: [queue] + summary: "Delete Queue Item" + security: + - bearerAuth: [] + responses: + '204': + description: No Content + "/api/queue/items/{item_id}/retry": + post: + tags: [queue] + summary: "Retry Queue Item" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/saved-filters": + get: + tags: [saved-filters] + summary: "List Saved Filters" + security: + - bearerAuth: [] + responses: + '200': + description: OK + post: + tags: [saved-filters] + summary: "Create Saved Filter" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "name": "My Filter", + "resource_type": "media-items", + "filters": {} + } + responses: + '200': + description: Success + "/api/saved-filters/{id}": + delete: + tags: [saved-filters] + summary: "Delete Saved Filter" + security: + - bearerAuth: [] + responses: + '200': + description: Success + get: + tags: [saved-filters] + summary: "Get Saved Filter" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [saved-filters] + summary: "Update Saved Filter" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "name": "Renamed Filter", + "resource_type": "media-items", + "filters": {} + } + responses: + '200': + description: Success + "/api/scanner/active": + get: + tags: [scanner] + summary: "Get Active Scans" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/scanner/active-summary": + get: + tags: [scanner] + summary: "Get Scan Summary" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/scanner/cancel/{jobId}": + post: + tags: [scanner] + summary: "Cancel Scan" + security: + - bearerAuth: [] + responses: + '200': + description: Success + "/api/scanner/scan": + post: + tags: [scanner] + summary: "Scan Library" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "force": false + } + responses: + '202': + description: Accepted + "/api/scanner/start": + post: + tags: [scanner] + summary: "Start Scanner" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "folder_paths": ["/data/books"] + } + responses: + '200': + description: OK + "/api/scanner/status/{jobId}": + get: + tags: [scanner] + summary: "Get Scan Status" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/scanner/stop": + post: + tags: [scanner] + summary: "Stop Scanner" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/scanner/watch/start": + post: + tags: [scanner] + summary: "Start Watch Mode" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "550e8400-e29b-41d4-a716-446655440000" + } + responses: + '200': + description: OK + "/api/scanner/watch/status": + get: + tags: [scanner] + summary: "Get Watch Mode Status" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/scanner/watch/stop": + post: + tags: [scanner] + summary: "Stop Watch Mode" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "library_id": "550e8400-e29b-41d4-a716-446655440000" + } + responses: + '200': + description: OK + "/api/series": + get: + tags: [series] + summary: "List Series" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/series/books": + get: + tags: [series] + summary: "Get Series Books" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/stats/books/{id}": + get: + tags: [stats] + summary: "Get Book Stats" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/stats/library-usage": + get: + tags: [stats] + summary: "Get Library Usage Statistics" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/stats/summary": + get: + tags: [stats] + summary: "Get Reading Summary" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/stats/this-week": + get: + tags: [stats] + summary: "Get This Week's Stats" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/sync/auto-link-books": + post: + tags: [book-matching] + summary: "Auto Link Books" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "confidence_threshold": 0.8, + "limit": 100 + } + responses: + '200': + description: OK + "/api/sync/books/query": + post: + tags: [book-matching] + summary: "Search Books for Matching" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "identifiers": ["isbn:978-0345391802", "uuid:abc-123"], + "sha256": "a1b2c3d4e5f6abc123...", + "title": "The Hobbit", + "author": "J.R.R. Tolkien", + "file_size": 2456789 + } + responses: + '200': + description: OK + "/api/sync/bulk-link-books": + post: + tags: [book-matching] + summary: "Bulk Link Books" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "links": [ + { + "unlinked_book_id": "770e8400-e29b-41d4-a716-446655440002", + "media_item_id": "660e8400-e29b-41d4-a716-446655440001", + "confidence_score": 0.95 + }, + { + "unlinked_book_id": "880e8400-e29b-41d4-a716-446655440003", + "media_item_id": "990e8400-e29b-41d4-a716-446655440004", + "confidence_score": 0.87 + } + ] + } + responses: + '200': + description: OK + "/api/sync/kobo/{token}/bookmark": + post: + tags: [kobo] + summary: "Bookmark Sync" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "bookmarks": [ + { + "BookmarkID": "bookmark123", + "ContentID": "book-uuid", + "StartPosition": 1234, + "Text": "Chapter 5", + "DateCreated": "2026-02-08T10:00:00Z" + } + ] + } + responses: + '200': + description: OK + "/api/sync/kobo/{token}/markup": + post: + tags: [kobo] + summary: "Markup Sync" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "bookmarks": [ + { + "BookmarkID": "bookmark123", + "ContentID": "book-uuid", + "StartPosition": 1234, + "EndPosition": 1456, + "Text": "Highlighted text from book", + "Annotation": "My annotation", + "DateCreated": "2026-02-08T10:00:00Z", + "Chapter": "Chapter 1", + "Hidden": false + } + ] + } + responses: + '200': + description: OK + "/api/sync/kobo/{token}/sync-from-server": + post: + tags: [kobo] + summary: "Sync From Server" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "book_ids": ["content-id-1", "content-id-2"], + "full_sync": false + } + responses: + '200': + description: OK + "/api/sync/kobo/{token}/v1/analytics/gettests": + post: + tags: [kobo] + summary: "Analytics GetTests" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "data": {} + } + responses: + '200': + description: OK + "/api/sync/kobo/{token}/v1/initialization": + get: + tags: [kobo] + summary: "Kobo Initialization" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + "/api/sync/koreader/bookmarks": + post: + tags: [koreader] + summary: "Sync Bookmarks" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "book_sha256": "64-hex-char-sha256", + "bookmarks": [ + { + "chapter": 3, + "datetime": "2026-08-20T10:00:00Z", + "pos0": "/body/Doc[4]/Sec[2]", + "page": 25, + "text": "", + "type": "bookmark", + "percentage": 0.125 + } + ], + "highlights": [ + { + "datetime": "2026-08-20T10:05:00Z", + "pos0": "/body/Doc[4]/Sec[2]/text()[3]:0", + "pos1": "/body/Doc[4]/Sec[2]/text()[3]:42", + "text": "Text to remember", + "notes": "Why this matters", + "type": "highlight", + "color": "blue", + "dedup_key": "echo-key-from-server" + } + ] + } + responses: + '200': + description: OK + "/api/sync/koreader/library": + get: + tags: [koreader] + summary: "Get Library" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + "/api/sync/koreader/metadata/{uuid}": + get: + tags: [koreader] + summary: "Get Metadata" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + "/api/sync/koreader/progress": + post: + tags: [koreader] + summary: "Sync Progress" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "books": [ + { + "uuid": "774641f9-317b-4087-8e04-53bb4392ae56", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "title": "1984", + "authors": ["George Orwell"], + "percentage": 0.045, + "context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim", + "page": 14, + "total_pages": 311, + "epubcfi": "/body/DocFragment[1]/body/p[29]/text().598", + "device_info": { + "koreader_version": "v2026.07.1", + "device_model": "emulator" + } + } + ], + "sync_mode": "immediate" + } + responses: + '202': + description: Accepted + "/api/sync/koreader/rating": + get: + tags: [koreader] + summary: "Get KOReader Rating" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + post: + tags: [koreader] + summary: "Set KOReader Rating" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "rating": 8 + } + responses: + '200': + description: Success + "/api/sync/koreader/resolve": + get: + tags: [koreader] + summary: "Resolve Book:\u003c|end|\u003e\u003c|start|\u003e assistant to=edit\u003c|channel|\u003eanalysis \u003c|constrain|\u003exml\u003c|message|\u003e\u003crfl:parameter name=\"path\"\u003e" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + "/api/sync/koreader/review": + get: + tags: [koreader] + summary: "Get KOReader Review" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + post: + tags: [koreader] + summary: "Set KOReader Review" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "review": "A slow start, but the second act earns it." + } + responses: + '200': + description: Success + "/api/sync/koreader/stats": + post: + tags: [koreader] + summary: "Push KOReader Stats" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + requestBody: + content: + application/json: + example: |- + { + "total_seconds": 3600 + } + responses: + '200': + description: Success + "/api/sync/unlinked-books/{id}/suggestions": + get: + tags: [book-matching] + summary: "Get Unlinked Book Suggestions" + security: + - bearerAuth: [] + responses: + '200': + description: OK + "/api/system/config": + get: + tags: [system] + summary: "Get System Configuration" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [system] + summary: "Update System Configuration" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "base_url": "https://books.example.com", + "default_timezone": "UTC" + } + responses: + '200': + description: Success + "/api/system/settings": + get: + tags: [system] + summary: "Get System Settings" + security: + - bearerAuth: [] + responses: + '200': + description: OK + put: + tags: [system] + summary: "Update System Setting" + security: + - bearerAuth: [] + requestBody: + content: + application/json: + example: |- + { + "key": "scan_poll_interval_seconds", + "value": "60" + } + responses: + '200': + description: Success + "/opds/devices/{deviceId}/catalog": + get: + tags: [opds] + summary: "OPDS Feeds" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + "/opds/devices/{deviceId}/cover/{bookId}": + get: + tags: [opds] + summary: "OPDS Publication" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK + "/opds/devices/{deviceId}/download/{bookId}": + get: + tags: [opds] + summary: "OPDS Acquisition" + description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).' + responses: + '200': + description: OK diff --git a/internal/docs/bruno_gen.go b/internal/docs/bruno_gen.go index 58f55669..4a2750b5 100644 --- a/internal/docs/bruno_gen.go +++ b/internal/docs/bruno_gen.go @@ -8,6 +8,7 @@ import ( "regexp" "sort" "strings" + "time" ) // Bruno mirror generation. The docs tree is canonical; bruno/ holds the @@ -118,18 +119,44 @@ func brunoURL(doc APIDoc) string { return "{{base_url}}" + path } -func brunoDocTitle(doc APIDoc) string { +func brunoDocBody(doc APIDoc) string { raw, err := os.ReadFile(filepath.Join(docsRoot, brunoRel(doc))) - if err == nil { - for _, line := range strings.Split(string(raw), "\n") { - if strings.HasPrefix(line, "# ") { - return strings.TrimSpace(strings.TrimPrefix(line, "# ")) - } + if err != nil { + return "" + } + return string(raw) +} + +func brunoDocTitle(doc APIDoc) string { + for _, line := range strings.Split(brunoDocBody(doc), "\n") { + if strings.HasPrefix(line, "# ") { + return strings.TrimSpace(strings.TrimPrefix(line, "# ")) } } return doc.Frontmatter.Path } +var brunoStatusRe = regexp.MustCompile(`(?:## Response \((\d{3})|Success is \*\*(\d{3})|\*\*(\d{3}) No Content)`) + +// brunoSuccessStatus extracts the documented happy-path status code from +// the doc body ("## Response (201 Created)", "Success is **201**", +// "**204 No Content**"). ok=false when the doc declares none — no assert +// is emitted rather than guessing. +func brunoSuccessStatus(doc APIDoc) (int, bool) { + m := brunoStatusRe.FindStringSubmatch(brunoDocBody(doc)) + if m == nil { + return 0, false + } + for _, g := range m[1:] { + if g != "" { + n := 0 + fmt.Sscanf(g, "%d", &n) + return n, true + } + } + return 0, false +} + // yamlBlock emits a literal block scalar at the given indent. func yamlBlock(indent int, text string) string { pad := strings.Repeat(" ", indent) @@ -163,9 +190,25 @@ func brunoYAML(doc APIDoc, seq int) ([]byte, error) { b.WriteString(" headers:\n - key: Authorization\n value: Bearer {{device_token}}\n") } b.WriteString("settings:\n encodeUrl: true\n timeout: 0\n followRedirects: true\n maxRedirects: 5\n") + scripts := "" if brunoScriptDocs[doc.Name()] { - b.WriteString("\nruntime:\n scripts:\n - type: after-response\n code: " + - yamlBlock(8, brunoTokenScript)) + scripts += " - type: after-response\n code: " + yamlBlock(8, brunoTokenScript) + } + status, hasStatus := brunoSuccessStatus(doc) + // GET is uniformly 200 across this API; mutating verbs vary + // (201/202/204/200) and stay assert-less unless the doc declares one. + if !hasStatus && doc.Frontmatter.Method == "GET" { + status, hasStatus = 200, true + } + if hasStatus { + if scripts != "" { + scripts += "\n" + } + assert := fmt.Sprintf("test(\"status is %d\", function() {\n expect(res.status).to.eql(%d);\n});", status, status) + scripts += " - type: tests\n code: " + yamlBlock(8, assert) + } + if scripts != "" { + b.WriteString("\nruntime:\n scripts:\n" + scripts) } b.WriteString("\ndocs:\n content: " + yamlBlock(4, fmt.Sprintf( "# %s\n\n`%s %s` — auth: %s.\n\nCanonical reference: docs/developer/api/%s (docs site → Developer → API). "+ @@ -175,6 +218,92 @@ func brunoYAML(doc APIDoc, seq int) ([]byte, error) { return []byte(b.String()), nil } +// updateOpenAPISpec writes docs/developer/openapi.yaml from the same doc +// set: paths, tags (docs area), summaries, auth schemes, and request +// examples. Lean by design — examples are literal, schemas stay out +// (the docs site remains the shape reference). +func updateOpenAPISpec(docs []APIDoc) error { + type op struct { + method string + doc APIDoc + } + paths := map[string][]op{} + for _, d := range docs { + p := brunoParamRe.ReplaceAllStringFunc(d.Frontmatter.Path, func(p string) string { + return "{" + strings.TrimPrefix(p, ":") + "}" + }) + paths[p] = append(paths[p], op{strings.ToLower(d.Frontmatter.Method), d}) + } + keys := make([]string, 0, len(paths)) + for k := range paths { + keys = append(keys, k) + } + sort.Strings(keys) + + var b strings.Builder + b.WriteString("openapi: 3.0.3\n") + b.WriteString("info:\n title: Bookhoard API\n") + fmt.Fprintf(&b, " version: '%s'\n", time.Now().Format("2006.01.02")) + b.WriteString(" description: |\n Generated from the canonical API docs (docs/developer/api/**) by\n" + + " internal/docs (go test ./internal/docs -run TestBrunoMirrorsDocs -update).\n" + + " The docs site is the shape reference; this file lists the surface.\n") + b.WriteString("servers:\n - url: /\n") + b.WriteString("components:\n securitySchemes:\n bearerAuth:\n type: http\n scheme: bearer\n bearerFormat: JWT\n") + b.WriteString("paths:\n") + for _, p := range keys { + fmt.Fprintf(&b, " %s:\n", mustYAMLString(p)) + ops := paths[p] + sort.Slice(ops, func(i, j int) bool { return ops[i].method < ops[j].method }) + for _, o := range ops { + fmt.Fprintf(&b, " %s:\n", o.method) + fmt.Fprintf(&b, " tags: [%s]\n", o.doc.Frontmatter.Area) + fmt.Fprintf(&b, " summary: %s\n", mustYAMLString(brunoDocTitle(o.doc))) + switch o.doc.Frontmatter.Auth { + case "none": + b.WriteString(" security: []\n") + case "device-token": + b.WriteString(" description: 'Device token: Bearer header (KOReader) or ?token= query (Kobo/OPDS).'\n") + default: + b.WriteString(" security:\n - bearerAuth: []\n") + } + if len(o.doc.ExampleRequest) > 0 { + b.WriteString(" requestBody:\n content:\n application/json:\n") + b.WriteString(" example: " + yamlBlock(14, string(o.doc.ExampleRequest))) + } + status, ok := brunoSuccessStatus(o.doc) + if !ok && o.doc.Frontmatter.Method == "GET" { + status = 200 + } + if ok || o.doc.Frontmatter.Method == "GET" { + desc := "OK" + switch status { + case 201: + desc = "Created" + case 202: + desc = "Accepted" + case 204: + desc = "No Content" + } + fmt.Fprintf(&b, " responses:\n '%d':\n description: %s\n", status, desc) + } else { + b.WriteString(" responses:\n '200':\n description: Success\n") + } + } + } + full := filepath.Join(docsRoot, "..", "openapi.yaml") + return os.WriteFile(full, []byte(b.String()), 0o644) +} + +// mustYAMLString renders s as a safely-quoted YAML scalar (JSON string +// syntax is valid YAML flow scalar syntax). +func mustYAMLString(s string) string { + out, err := json.Marshal(s) + if err != nil { + return `""` + } + return string(out) +} + // Name is the doc filename without extension (login.md → login). func (d APIDoc) Name() string { return strings.TrimSuffix(filepath.Base(d.RelPath), ".md") diff --git a/internal/docs/drift_test.go b/internal/docs/drift_test.go index 5e80d21a..66d0dd3e 100644 --- a/internal/docs/drift_test.go +++ b/internal/docs/drift_test.go @@ -208,6 +208,9 @@ func TestBrunoMirrorsDocs(t *testing.T) { if err != nil { t.Fatalf("regenerate bruno mirrors: %v", err) } + if err := updateOpenAPISpec(docs); err != nil { + t.Fatalf("regenerate openapi spec: %v", err) + } t.Logf("bruno mirrors: %d written, %d deleted", len(written), len(deleted)) for _, rel := range written { t.Logf(" wrote %s", rel) @@ -215,6 +218,9 @@ func TestBrunoMirrorsDocs(t *testing.T) { for _, rel := range deleted { t.Logf(" deleted %s", rel) } + t.Logf("openapi spec written") + } else if _, err := os.Stat(filepath.Join(docsRoot, "..", "openapi.yaml")); err != nil { + t.Errorf("docs/developer/openapi.yaml missing — regenerate with: go test ./internal/docs -run TestBrunoMirrorsDocs -update") } // Collect bruno requests: "METHOD normalized-path" -> relpath.