mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
cucumber for days (#5766)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
[behave]
|
||||
# Enterprise and premium-licensed features live in features/enterprise/.
|
||||
# They are excluded from the default CI run because the test environment
|
||||
# does not have a commercial licence. To run them explicitly:
|
||||
#
|
||||
# python -m behave features/enterprise
|
||||
#
|
||||
exclude_re = features/enterprise
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
@jwt @auth @admin_settings
|
||||
Feature: Admin Settings API
|
||||
|
||||
Tests for the admin settings REST API endpoints, which expose application
|
||||
configuration values to authenticated admins.
|
||||
|
||||
All endpoints require ROLE_ADMIN. Non-admin / unauthenticated requests must
|
||||
receive 401 or 403.
|
||||
|
||||
Admin credentials: username=admin, password=stirling
|
||||
|
||||
# =========================================================================
|
||||
# GET ALL SETTINGS
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve all application settings
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/admin/settings" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response body should not be empty
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to settings returns 401
|
||||
When I send a GET request to "/api/v1/admin/settings" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# GET SETTINGS DELTA
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve the settings delta (changed values)
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/admin/settings/delta" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to settings delta returns 401
|
||||
When I send a GET request to "/api/v1/admin/settings/delta" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# GET SETTINGS BY SECTION
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve settings for the system section
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/admin/settings/section/system" with JWT authentication
|
||||
Then the response status code should be one of "200, 404"
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve settings for the security section
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/admin/settings/section/security" with JWT authentication
|
||||
Then the response status code should be one of "200, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to settings section returns 401
|
||||
When I send a GET request to "/api/v1/admin/settings/section/system" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# GET SINGLE SETTING BY KEY
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve a single setting by key
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/admin/settings/key/system.defaultLocale" with JWT authentication
|
||||
Then the response status code should be one of "200, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to settings key returns 401
|
||||
When I send a GET request to "/api/v1/admin/settings/key/system.defaultLocale" with no authentication
|
||||
Then the response status code should be 401
|
||||
@@ -0,0 +1,201 @@
|
||||
@analysis
|
||||
Feature: Analysis API Endpoints
|
||||
|
||||
Analysis endpoints inspect a PDF and return structured JSON (HTTP 200).
|
||||
No binary output is produced; all responses are application/json with
|
||||
a non-empty body.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/page-count
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@page-count @positive
|
||||
Scenario Outline: page-count returns JSON with pageCount for different page numbers
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains <pages> pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/page-count"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
Examples:
|
||||
| pages |
|
||||
| 1 |
|
||||
| 5 |
|
||||
| 20 |
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/basic-info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@basic-info @positive
|
||||
Scenario: basic-info returns JSON for a standard PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/basic-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@basic-info @positive
|
||||
Scenario: basic-info returns JSON for a single-page PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/analysis/basic-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@basic-info @positive
|
||||
Scenario: basic-info returns JSON for a PDF with text content
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/analysis/basic-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/document-properties
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@document-properties @positive
|
||||
Scenario: document-properties returns JSON for a plain PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/document-properties"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@document-properties @positive
|
||||
Scenario: document-properties returns JSON for a multi-page PDF with text
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/analysis/document-properties"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/page-dimensions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@page-dimensions @positive
|
||||
Scenario Outline: page-dimensions returns JSON for PDFs of different sizes
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains <pages> pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/page-dimensions"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
Examples:
|
||||
| pages |
|
||||
| 1 |
|
||||
| 3 |
|
||||
| 10 |
|
||||
|
||||
@page-dimensions @positive
|
||||
Scenario: page-dimensions returns JSON for a LETTER-sized PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/analysis/page-dimensions"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/form-fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@form-fields @positive
|
||||
Scenario: form-fields returns JSON for a PDF without any form fields
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/form-fields"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@form-fields @positive
|
||||
Scenario: form-fields returns JSON for a multi-page PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 10 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/form-fields"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/annotation-info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@annotation-info @positive
|
||||
Scenario: annotation-info returns JSON for a PDF with no annotations
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/annotation-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@annotation-info @positive
|
||||
Scenario: annotation-info returns JSON for a text-content PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/analysis/annotation-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/font-info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@font-info @positive
|
||||
Scenario: font-info returns JSON for a blank PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/font-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@font-info @positive
|
||||
Scenario: font-info returns JSON for a PDF containing text
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/analysis/font-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/v1/analysis/security-info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@security-info @positive
|
||||
Scenario: security-info returns JSON for an unencrypted PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/analysis/security-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@security-info @positive
|
||||
Scenario: security-info returns JSON for a multi-page PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/analysis/security-info"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
@@ -0,0 +1,82 @@
|
||||
@misc
|
||||
Feature: Attachments API Validation
|
||||
|
||||
@add-attachments @positive
|
||||
Scenario: Add a single attachment to PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And I also generate a PDF file as "attachments"
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-attachments"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@add-attachments @positive
|
||||
Scenario: Add multiple attachments to PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And I also generate a PDF file as "attachments"
|
||||
And I also generate a PDF file as "attachments"
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-attachments"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@list-attachments @positive
|
||||
Scenario: List attachments in PDF with embedded attachment
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has an attachment named "test_doc.txt"
|
||||
When I send the API request to the endpoint "/api/v1/misc/list-attachments"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@list-attachments @positive
|
||||
Scenario: List attachments in plain PDF returns empty list
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
When I send the API request to the endpoint "/api/v1/misc/list-attachments"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@extract-attachments @positive
|
||||
Scenario: Extract attachments from PDF with embedded attachment
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has an attachment named "report.txt"
|
||||
When I send the API request to the endpoint "/api/v1/misc/extract-attachments"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@rename-attachment @positive
|
||||
Scenario: Rename attachment in PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has an attachment named "original.txt"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| attachmentName | original.txt |
|
||||
| newName | renamed.txt |
|
||||
When I send the API request to the endpoint "/api/v1/misc/rename-attachment"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@delete-attachment @positive
|
||||
Scenario: Delete attachment from PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has an attachment named "to_delete.txt"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| attachmentName | to_delete.txt |
|
||||
When I send the API request to the endpoint "/api/v1/misc/delete-attachment"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
@@ -0,0 +1,41 @@
|
||||
@misc
|
||||
Feature: Auto-Split PDF API Validation
|
||||
|
||||
@auto-split @positive
|
||||
Scenario: Auto-split PDF with QR code marker on first page
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages with random text
|
||||
And the pdf has a Stirling-PDF QR code split marker on page 1
|
||||
When I send the API request to the endpoint "/api/v1/misc/auto-split-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@auto-split @positive
|
||||
Scenario: Auto-split PDF with QR code marker on middle page
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages with random text
|
||||
And the pdf has a Stirling-PDF QR code split marker on page 3
|
||||
When I send the API request to the endpoint "/api/v1/misc/auto-split-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@auto-split @positive
|
||||
Scenario: Auto-split PDF with duplex mode enabled
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages with random text
|
||||
And the pdf has a Stirling-PDF QR code split marker on page 1
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| duplexMode | true |
|
||||
When I send the API request to the endpoint "/api/v1/misc/auto-split-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@auto-split @positive
|
||||
Scenario: Auto-split single-page PDF with QR marker
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And the pdf has a Stirling-PDF QR code split marker on page 1
|
||||
When I send the API request to the endpoint "/api/v1/misc/auto-split-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
@@ -0,0 +1,63 @@
|
||||
@general
|
||||
Feature: Bookmarks and Chapter Splitting API Validation
|
||||
|
||||
@extract-bookmarks @positive
|
||||
Scenario: Extract bookmarks from PDF with bookmarks
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the pdf has bookmarks
|
||||
When I send the API request to the endpoint "/api/v1/general/extract-bookmarks"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@extract-bookmarks @positive
|
||||
Scenario: Extract bookmarks from plain PDF returns empty list
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/general/extract-bookmarks"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@split-pdf-by-chapters @positive
|
||||
Scenario: Split PDF by chapters with top-level bookmarks
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages with random text
|
||||
And the pdf has bookmarks
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| bookmarkLevel | 0 |
|
||||
| includeMetadata | false |
|
||||
| allowDuplicates | false |
|
||||
When I send the API request to the endpoint "/api/v1/general/split-pdf-by-chapters"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@split-pdf-by-chapters @positive
|
||||
Scenario: Split PDF by chapters with metadata included
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the pdf has bookmarks
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| bookmarkLevel | 0 |
|
||||
| includeMetadata | true |
|
||||
| allowDuplicates | false |
|
||||
When I send the API request to the endpoint "/api/v1/general/split-pdf-by-chapters"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@split-pdf-by-chapters @positive
|
||||
Scenario: Split PDF by chapters allowing duplicate pages
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the pdf has bookmarks
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| bookmarkLevel | 0 |
|
||||
| includeMetadata | false |
|
||||
| allowDuplicates | true |
|
||||
When I send the API request to the endpoint "/api/v1/general/split-pdf-by-chapters"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
@@ -0,0 +1,23 @@
|
||||
@convert @image
|
||||
Feature: Comic Archive Conversion API Validation
|
||||
|
||||
@cbz-to-pdf @positive
|
||||
Scenario: Convert CBZ comic archive to PDF
|
||||
Given I generate a CBZ comic archive file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/cbz/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@cbz-to-pdf @positive
|
||||
Scenario: Convert CBZ comic archive to PDF without ebook optimisation
|
||||
Given I generate a CBZ comic archive file as "fileInput"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| optimizeForEbook | false |
|
||||
When I send the API request to the endpoint "/api/v1/convert/cbz/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
@@ -0,0 +1,25 @@
|
||||
@convert
|
||||
Feature: EML to PDF Conversion API Validation
|
||||
|
||||
@eml-to-pdf @positive
|
||||
Scenario: Convert EML email to PDF
|
||||
Given I generate an EML email file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/eml/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@eml-to-pdf @positive
|
||||
Scenario: Convert EML with subject and body to PDF
|
||||
Given I generate an EML email file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/eml/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@eml-to-pdf @positive
|
||||
Scenario: Convert MSG (Outlook) file to PDF
|
||||
Given I use an example file at "exampleFiles/example.msg" as parameter "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/eml/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
@@ -0,0 +1,99 @@
|
||||
@convert
|
||||
Feature: Image Conversion API Validation
|
||||
|
||||
@img-to-pdf @positive
|
||||
Scenario: Convert single PNG image to PDF
|
||||
Given I generate a PNG image file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/img/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@img-to-pdf @positive
|
||||
Scenario: Convert PNG image to PDF with fillPage fit option
|
||||
Given I generate a PNG image file as "fileInput"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| fitOption | fillPage |
|
||||
| colorType | color |
|
||||
| autoRotate | false |
|
||||
When I send the API request to the endpoint "/api/v1/convert/img/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@img-to-pdf @positive
|
||||
Scenario: Convert multiple PNG images to PDF
|
||||
Given I generate a PNG image file as "fileInput"
|
||||
And I also generate a PNG image file as "fileInput"
|
||||
And I also generate a PNG image file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/img/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@svg-to-pdf @positive
|
||||
Scenario: Convert single SVG file to PDF
|
||||
Given I generate an SVG file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/svg/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@svg-to-pdf @positive
|
||||
Scenario: Convert multiple SVG files to PDF
|
||||
Given I generate an SVG file as "fileInput"
|
||||
And I also generate an SVG file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/convert/svg/pdf"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@add-image @positive
|
||||
Scenario: Overlay PNG image onto PDF at default position
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And I generate a PNG image file as "imageFile"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| x | 0 |
|
||||
| y | 0 |
|
||||
| everyPage | false |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-image"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@add-image @positive
|
||||
Scenario: Overlay PNG image onto every page of PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And I generate a PNG image file as "imageFile"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| x | 10 |
|
||||
| y | 10 |
|
||||
| everyPage | true |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-image"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@add-image @positive
|
||||
Scenario: Overlay SVG image onto PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And I generate an SVG file as "imageFile"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| x | 50 |
|
||||
| y | 50 |
|
||||
| everyPage | true |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-image"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
@@ -0,0 +1,97 @@
|
||||
@convert
|
||||
Feature: Convert API Validation (additional endpoints)
|
||||
|
||||
@ghostscript @positive
|
||||
Scenario Outline: Convert PDF to vector format
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| outputFormat | <format> |
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/vector"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension "<extension>"
|
||||
|
||||
@pdf-to-eps
|
||||
Examples:
|
||||
| format | extension |
|
||||
| eps | .eps |
|
||||
|
||||
Examples:
|
||||
| format | extension |
|
||||
| ps | .ps |
|
||||
| pcl | .pcl |
|
||||
| xps | .xps |
|
||||
|
||||
@image @positive @pdf-to-cbz
|
||||
Scenario: Convert PDF to CBZ with default DPI
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/cbz"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".cbz"
|
||||
|
||||
@image @positive @pdf-to-cbz
|
||||
Scenario: Convert PDF to CBZ with low DPI
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| dpi | 72 |
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/cbz"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".cbz"
|
||||
|
||||
@image @positive @pdf-to-cbz
|
||||
Scenario: Convert single-page PDF to CBZ
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| dpi | 72 |
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/cbz"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".cbz"
|
||||
|
||||
@calibre @positive @pdf-to-epub
|
||||
Scenario: Convert PDF to EPUB format
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| outputFormat | EPUB |
|
||||
| detectChapters | false |
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/epub"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".epub"
|
||||
|
||||
@calibre @positive @pdf-to-epub
|
||||
Scenario: Convert PDF to AZW3 format
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| outputFormat | AZW3 |
|
||||
| detectChapters | false |
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/epub"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".azw3"
|
||||
|
||||
@calibre @positive @pdf-to-epub
|
||||
Scenario: Convert PDF to EPUB with chapter detection
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| outputFormat | EPUB |
|
||||
| detectChapters | true |
|
||||
When I send the API request to the endpoint "/api/v1/convert/pdf/epub"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".epub"
|
||||
@@ -0,0 +1,138 @@
|
||||
@jwt @auth @audit
|
||||
Feature: Audit Dashboard API
|
||||
|
||||
Tests for the audit dashboard REST API endpoints, which provide
|
||||
audit log data, statistics, and export capabilities.
|
||||
|
||||
All endpoints:
|
||||
- Require ROLE_ADMIN (JWT authentication)
|
||||
- Are gated by @EnterpriseEndpoint (may return 403 on non-enterprise builds)
|
||||
|
||||
Responses are therefore expected to be one of: 200 (enterprise enabled)
|
||||
or 403 (enterprise feature not available in this build).
|
||||
|
||||
Admin credentials: username=admin, password=stirling
|
||||
|
||||
# =========================================================================
|
||||
# AUDIT DATA
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve audit log data
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/audit/data" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to audit data returns 401
|
||||
When I send a GET request to "/api/v1/audit/data" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# AUDIT STATS
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve audit statistics
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/audit/stats" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to audit stats returns 401
|
||||
When I send a GET request to "/api/v1/audit/stats" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# AUDIT TYPES
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve audit event types
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/audit/types" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to audit types returns 401
|
||||
When I send a GET request to "/api/v1/audit/types" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# AUDIT EXPORT (CSV)
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can export audit log as CSV
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/audit/export/csv" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to audit CSV export returns 401
|
||||
When I send a GET request to "/api/v1/audit/export/csv" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# AUDIT EXPORT (JSON)
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can export audit log as JSON
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/audit/export/json" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to audit JSON export returns 401
|
||||
When I send a GET request to "/api/v1/audit/export/json" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# AUDIT CLEANUP
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can trigger cleanup of old audit records
|
||||
Given I am logged in as admin
|
||||
When I send a DELETE request to "/api/v1/audit/cleanup/before" with JWT authentication and params "date=2020-01-01"
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to audit cleanup returns 401
|
||||
When I send a DELETE request to "/api/v1/audit/cleanup/before" with no authentication and params "date=2020-01-01"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# PROPRIETARY UI DATA – AUDIT EVENTS (AuditRestController)
|
||||
# Endpoint base: /api/v1/proprietary/ui-data
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve paginated audit events from UI data API
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/proprietary/ui-data/audit-events" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve audit chart data
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/proprietary/ui-data/audit-charts" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve list of audit event types from UI data API
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/proprietary/ui-data/audit-event-types" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@positive
|
||||
Scenario: Admin can retrieve list of audited users
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/proprietary/ui-data/audit-users" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to proprietary audit events returns 401
|
||||
When I send a GET request to "/api/v1/proprietary/ui-data/audit-events" with no authentication
|
||||
Then the response status code should be 401
|
||||
@@ -0,0 +1,45 @@
|
||||
@jwt @auth @signature
|
||||
Feature: Signatures API
|
||||
|
||||
Tests for the saved signatures REST API, which allows authenticated
|
||||
users to store and retrieve their signature images.
|
||||
|
||||
Endpoints:
|
||||
- GET /api/v1/proprietary/signatures (authenticated)
|
||||
- POST /api/v1/proprietary/signatures (authenticated, multipart)
|
||||
- DELETE /api/v1/proprietary/signatures/{id} (authenticated)
|
||||
|
||||
POST is omitted here because it requires a multipart image upload; the
|
||||
format of SavedSignatureRequest is tested via integration rather than BDD.
|
||||
|
||||
Admin credentials: username=admin, password=stirling
|
||||
|
||||
# =========================================================================
|
||||
# LIST SIGNATURES
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Authenticated user can retrieve their signatures list
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/proprietary/signatures" with JWT authentication
|
||||
Then the response status code should be one of "200, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to signatures list returns 401
|
||||
When I send a GET request to "/api/v1/proprietary/signatures" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# DELETE SIGNATURE
|
||||
# =========================================================================
|
||||
|
||||
@negative
|
||||
Scenario: Delete a non-existent signature returns 404 or 403
|
||||
Given I am logged in as admin
|
||||
When I send a DELETE request to "/api/v1/proprietary/signatures/nonexistent-sig-id-xyz" with JWT authentication
|
||||
Then the response status code should be one of "403, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to delete signature returns 401
|
||||
When I send a DELETE request to "/api/v1/proprietary/signatures/some-id" with no authentication
|
||||
Then the response status code should be 401
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Re-export all step definitions so that `behave features/enterprise/` works
|
||||
as a standalone run without needing to reference the parent steps directory.
|
||||
|
||||
When running the default suite (`behave` from testing/cucumber/), these
|
||||
enterprise features are excluded via behave.ini. When running enterprise
|
||||
tests explicitly (`python -m behave features/enterprise`), Behave loads
|
||||
steps only from this directory, so we import the parent implementations here.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Make the parent steps/ directory importable
|
||||
_parent_steps = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../steps"))
|
||||
if _parent_steps not in sys.path:
|
||||
sys.path.insert(0, _parent_steps)
|
||||
|
||||
from step_definitions import * # noqa: F401, F403
|
||||
from auth_step_definitions import * # noqa: F401, F403
|
||||
from enterprise_step_definitions import * # noqa: F401, F403
|
||||
@@ -0,0 +1,80 @@
|
||||
@jwt @auth @team
|
||||
Feature: Teams API
|
||||
|
||||
Tests for the teams REST API, which provides multi-user grouping
|
||||
functionality (a @PremiumEndpoint feature).
|
||||
|
||||
Endpoints:
|
||||
- POST /api/v1/teams/create (admin only, query param: name)
|
||||
- POST /api/v1/teams/rename (admin only, query params: teamId, name)
|
||||
- POST /api/v1/teams/delete (admin only, query param: teamId)
|
||||
- POST /api/v1/teams/addUser (admin only, query params: teamId, username)
|
||||
|
||||
Because this is a @PremiumEndpoint, responses may be 200 (premium enabled)
|
||||
or 403 (premium not available in this build).
|
||||
|
||||
There is no GET /teams endpoint, so full CRUD lifecycle cannot be verified
|
||||
via ID-based lookup. Tests are limited to exercising each endpoint and
|
||||
checking the response is not a security bypass.
|
||||
|
||||
Admin credentials: username=admin, password=stirling
|
||||
|
||||
# =========================================================================
|
||||
# CREATE TEAM
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can attempt to create a new team
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/teams/create" with JWT authentication and params "name=bdd_test_team"
|
||||
Then the response status code should be one of "200, 201, 403"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to create team returns 401
|
||||
When I send a POST request to "/api/v1/teams/create" with no authentication and params "name=evil_team"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# RENAME TEAM
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can attempt to rename a team
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/teams/rename" with JWT authentication and params "teamId=1&newName=bdd_renamed_team"
|
||||
Then the response status code should be one of "200, 400, 403, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to rename team returns 401
|
||||
When I send a POST request to "/api/v1/teams/rename" with no authentication and params "teamId=1&newName=evil_renamed"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# ADD USER TO TEAM
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can attempt to add a user to a team
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/teams/addUser" with JWT authentication and params "teamId=1&userId=1"
|
||||
Then the response status code should be one of "200, 400, 403, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to add user to team returns 401
|
||||
When I send a POST request to "/api/v1/teams/addUser" with no authentication and params "teamId=1&userId=1"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# DELETE TEAM
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can attempt to delete a team
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/teams/delete" with JWT authentication and params "teamId=999"
|
||||
Then the response status code should be one of "200, 400, 403, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to delete team returns 401
|
||||
When I send a POST request to "/api/v1/teams/delete" with no authentication and params "teamId=1"
|
||||
Then the response status code should be 401
|
||||
@@ -1,25 +1,124 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
_BASE_URL = "http://localhost:8080"
|
||||
|
||||
# Tags that indicate a scenario requires JWT Bearer auth to be functional.
|
||||
# These scenarios are skipped when the server has JWT disabled (V2=false).
|
||||
# @login and @register scenarios work in both modes.
|
||||
# The "jwt" tag itself is included so that feature-level @jwt tagging is sufficient
|
||||
# to mark an entire feature as JWT-dependent.
|
||||
_JWT_DEPENDENT_TAGS = frozenset({
|
||||
# jwt_auth.feature scenario tags
|
||||
"me", "refresh", "logout", "role", "token", "mfa", "apikey",
|
||||
# proprietary/enterprise feature tags (all scenarios in these features need JWT)
|
||||
"jwt", "user_mgmt", "admin_settings", "audit", "signature", "team",
|
||||
})
|
||||
|
||||
|
||||
def _check_jwt_available():
|
||||
"""Probe the server to determine whether JWT Bearer auth is functional.
|
||||
|
||||
Logs in as admin, then attempts to use the returned token on /me.
|
||||
Returns True only when the full JWT round-trip succeeds (V2 enabled).
|
||||
"""
|
||||
try:
|
||||
login = requests.post(
|
||||
f"{_BASE_URL}/api/v1/auth/login",
|
||||
json={"username": "admin", "password": "stirling"},
|
||||
timeout=10,
|
||||
)
|
||||
if login.status_code != 200:
|
||||
return False
|
||||
token = login.json().get("session", {}).get("access_token")
|
||||
if not token:
|
||||
return False
|
||||
me = requests.get(
|
||||
f"{_BASE_URL}/api/v1/auth/me",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
return me.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def before_all(context):
|
||||
context.endpoint = None
|
||||
context.request_data = None
|
||||
context.files = {}
|
||||
context.response = None
|
||||
context.jwt_available = _check_jwt_available()
|
||||
if not context.jwt_available:
|
||||
print(
|
||||
"\n[JWT] JWT Bearer authentication is not available in this environment "
|
||||
"(server likely running with V2=false). "
|
||||
"Scenarios tagged with JWT-dependent tags will be skipped."
|
||||
)
|
||||
|
||||
|
||||
def before_scenario(context, scenario):
|
||||
"""Reset all per-scenario state before each scenario runs."""
|
||||
# Skip scenarios that require JWT Bearer auth when it is not functional.
|
||||
scenario_tags = set(scenario.effective_tags)
|
||||
if _JWT_DEPENDENT_TAGS & scenario_tags and not context.jwt_available:
|
||||
scenario.skip(
|
||||
"JWT Bearer authentication not available in this environment (V2 disabled). "
|
||||
"Run against a server with V2=true to execute these scenarios."
|
||||
)
|
||||
return
|
||||
|
||||
context.files = {}
|
||||
context.multi_files = []
|
||||
context.json_parts = {}
|
||||
context.request_data = None
|
||||
# JWT auth state
|
||||
context.jwt_token = None
|
||||
context.original_jwt_token = None
|
||||
# OR-status helper used by auth step definitions
|
||||
context._status_ok = False
|
||||
# Stored value used by enterprise step definitions for dynamic URL composition
|
||||
context.stored_value = None
|
||||
|
||||
|
||||
def after_scenario(context, scenario):
|
||||
if hasattr(context, "files"):
|
||||
for file in context.files.values():
|
||||
try:
|
||||
file.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Close any multi-file handles
|
||||
for _key, file in getattr(context, "multi_files", []):
|
||||
try:
|
||||
file.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if os.path.exists("response_file"):
|
||||
os.remove("response_file")
|
||||
if hasattr(context, "file_name") and os.path.exists(context.file_name):
|
||||
# Guard against context.file_name being None (e.g. reset from a previous scenario)
|
||||
if hasattr(context, "file_name") and context.file_name and os.path.exists(context.file_name):
|
||||
os.remove(context.file_name)
|
||||
|
||||
# Remove any temporary files
|
||||
# Remove any temporary files generated during the scenario
|
||||
for temp_file in os.listdir("."):
|
||||
if temp_file.startswith("genericNonCustomisableName") or temp_file.startswith(
|
||||
"temp_image_"
|
||||
):
|
||||
os.remove(temp_file)
|
||||
try:
|
||||
os.remove(temp_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Reset all per-scenario state so stale handles don't bleed into the next scenario
|
||||
context.files = {}
|
||||
context.multi_files = []
|
||||
context.json_parts = {}
|
||||
context.request_data = None
|
||||
# JWT auth state
|
||||
context.jwt_token = None
|
||||
context.original_jwt_token = None
|
||||
context._status_ok = False
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
@filter
|
||||
Feature: Filter API Endpoints
|
||||
|
||||
Filter endpoints return 200 with the original PDF when the filter condition
|
||||
is satisfied, or 204 (No Content) when the condition is not satisfied.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter-page-count
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@filter-page-count @positive
|
||||
Scenario Outline: filter-page-count returns 200 when condition is met
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageCount | <pageCount> |
|
||||
| comparator | <comparator> |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-count"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
Examples:
|
||||
| pageCount | comparator |
|
||||
| 3 | Greater |
|
||||
| 5 | Equal |
|
||||
| 7 | Less |
|
||||
|
||||
@filter-page-count @negative
|
||||
Scenario Outline: filter-page-count returns 204 when condition is not met
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageCount | <pageCount> |
|
||||
| comparator | <comparator> |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-count"
|
||||
Then the response status code should be 204
|
||||
|
||||
Examples:
|
||||
| pageCount | comparator |
|
||||
| 5 | Greater |
|
||||
| 4 | Equal |
|
||||
| 5 | Less |
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter-file-size
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@filter-file-size @positive
|
||||
Scenario Outline: filter-file-size returns 200 when condition is met
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| fileSize | <fileSize> |
|
||||
| comparator | <comparator> |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-file-size"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
Examples:
|
||||
| fileSize | comparator |
|
||||
| 100 | Greater |
|
||||
| 9999999 | Less |
|
||||
|
||||
@filter-file-size @negative
|
||||
Scenario: filter-file-size returns 204 when file is not large enough
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| fileSize | 99999999 |
|
||||
| comparator | Greater |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-file-size"
|
||||
Then the response status code should be 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter-page-rotation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@filter-page-rotation @positive
|
||||
Scenario: filter-page-rotation returns 200 for Equal comparator on 0-degree pages
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| rotation | 0 |
|
||||
| comparator | Equal |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-rotation"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@filter-page-rotation @positive
|
||||
Scenario Outline: filter-page-rotation returns 200 for Greater comparator on 0-degree pages
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| rotation | <rotation> |
|
||||
| comparator | <comparator> |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-rotation"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
Examples:
|
||||
| rotation | comparator |
|
||||
| 90 | Less |
|
||||
| 180 | Less |
|
||||
| 270 | Less |
|
||||
|
||||
@filter-page-rotation @negative
|
||||
Scenario Outline: filter-page-rotation returns 204 when condition is not met
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| rotation | <rotation> |
|
||||
| comparator | <comparator> |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-rotation"
|
||||
Then the response status code should be 204
|
||||
|
||||
Examples:
|
||||
| rotation | comparator |
|
||||
| 0 | Greater |
|
||||
| 90 | Equal |
|
||||
| 180 | Equal |
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter-page-size
|
||||
# Blank pages are 72x72 points (smaller than any standard page size).
|
||||
# Pages with random text use LETTER (612x792 points).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@filter-page-size @positive
|
||||
Scenario Outline: filter-page-size returns 200 when blank PDF is smaller than standard size
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| standardPageSize | <standardPageSize> |
|
||||
| comparator | Less |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-size"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
Examples:
|
||||
| standardPageSize |
|
||||
| A0 |
|
||||
| A4 |
|
||||
| A6 |
|
||||
| LETTER |
|
||||
| LEGAL |
|
||||
|
||||
@filter-page-size @positive
|
||||
Scenario: filter-page-size returns 200 when text PDF equals LETTER size
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| standardPageSize | LETTER |
|
||||
| comparator | Equal |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-size"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@filter-page-size @negative
|
||||
Scenario Outline: filter-page-size returns 204 when blank PDF does not match standard size as Equal
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| standardPageSize | <standardPageSize> |
|
||||
| comparator | Equal |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-size"
|
||||
Then the response status code should be 204
|
||||
|
||||
Examples:
|
||||
| standardPageSize |
|
||||
| A4 |
|
||||
| LETTER |
|
||||
| LEGAL |
|
||||
|
||||
@filter-page-size @negative
|
||||
Scenario: filter-page-size returns 204 when blank PDF is not Greater than any standard size
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| standardPageSize | A6 |
|
||||
| comparator | Greater |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-page-size"
|
||||
Then the response status code should be 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter-contains-text
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@filter-contains-text @positive
|
||||
Scenario: filter-contains-text returns 200 when text is found in PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf pages all contain the text "FINDME"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| text | FINDME |
|
||||
| pageNumbers | all |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-contains-text"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@filter-contains-text @negative
|
||||
Scenario: filter-contains-text returns 204 when text is not found in blank PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| text | NOTPRESENT |
|
||||
| pageNumbers | all |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-contains-text"
|
||||
Then the response status code should be 204
|
||||
|
||||
@filter-contains-text @negative
|
||||
Scenario: filter-contains-text returns 204 when searched text differs from PDF content
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf pages all contain the text "HELLO"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| text | GOODBYE |
|
||||
| pageNumbers | all |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-contains-text"
|
||||
Then the response status code should be 204
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# filter-contains-image
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@filter-contains-image @positive
|
||||
Scenario: filter-contains-image returns 200 when PDF contains images
|
||||
Given the pdf contains 2 images of size 100x100 on 1 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | all |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-contains-image"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@filter-contains-image @negative
|
||||
Scenario: filter-contains-image returns 204 when PDF has no images
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | all |
|
||||
When I send the API request to the endpoint "/api/v1/filter/filter-contains-image"
|
||||
Then the response status code should be 204
|
||||
@@ -0,0 +1,29 @@
|
||||
@proprietary @forms
|
||||
Feature: Advanced Forms API Validation (JSON data parts)
|
||||
|
||||
@fill @positive
|
||||
Scenario: Fill PDF form with empty JSON data part
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has form fields
|
||||
And the request includes a JSON part "data" with content "{}"
|
||||
When I send the API request to the endpoint "/api/v1/form/fill"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@fill @positive
|
||||
Scenario: Fill and flatten PDF form with empty JSON data part
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has form fields
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| flatten | true |
|
||||
And the request includes a JSON part "data" with content "{}"
|
||||
When I send the API request to the endpoint "/api/v1/form/fill"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
@@ -0,0 +1,91 @@
|
||||
@proprietary @forms
|
||||
Feature: Forms API Validation
|
||||
|
||||
@fields @positive
|
||||
Scenario: Get form fields from plain PDF
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/fields"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@fields @positive
|
||||
Scenario: Get form fields from multi-page PDF
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 5 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/fields"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@fields-with-coordinates @positive
|
||||
Scenario: Get form fields with coordinates from PDF
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/fields-with-coordinates"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@fields-with-coordinates @positive
|
||||
Scenario: Get form fields with coordinates from multi-page PDF
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 4 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/fields-with-coordinates"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@fill @positive
|
||||
Scenario: Fill PDF form with default options
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf has form fields
|
||||
When I send the API request to the endpoint "/api/v1/form/fill"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@fill @positive
|
||||
Scenario: Fill and flatten PDF form
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 3 pages
|
||||
And the pdf has form fields
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| flatten | true |
|
||||
When I send the API request to the endpoint "/api/v1/form/fill"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@modify-fields @negative
|
||||
Scenario: Modify form fields with no updates payload returns 400
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/modify-fields"
|
||||
Then the response status code should be 400
|
||||
|
||||
@modify-fields @negative
|
||||
Scenario: Modify form fields in multi-page PDF with no updates payload returns 400
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 5 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/modify-fields"
|
||||
Then the response status code should be 400
|
||||
|
||||
@delete-fields @negative
|
||||
Scenario: Delete form fields with no names payload returns 400
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/delete-fields"
|
||||
Then the response status code should be 400
|
||||
|
||||
@delete-fields @negative
|
||||
Scenario: Delete form fields from multi-page PDF with no names payload returns 400
|
||||
Given I generate a PDF file as "file"
|
||||
And the pdf contains 4 pages
|
||||
When I send the API request to the endpoint "/api/v1/form/delete-fields"
|
||||
Then the response status code should be 400
|
||||
@@ -0,0 +1,251 @@
|
||||
@general-new
|
||||
Feature: General PDF Operations API Validation
|
||||
|
||||
|
||||
@rotate-pdf @positive
|
||||
Scenario Outline: rotate-pdf with valid rotation angles
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| angle | <angle> |
|
||||
When I send the API request to the endpoint "/api/v1/general/rotate-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 3 pages
|
||||
|
||||
Examples:
|
||||
| angle |
|
||||
| 90 |
|
||||
| 180 |
|
||||
| 270 |
|
||||
|
||||
|
||||
@rotate-pdf @negative @rotate-pdf-negative
|
||||
Scenario: rotate-pdf with invalid angle returns error
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| angle | 45 |
|
||||
When I send the API request to the endpoint "/api/v1/general/rotate-pdf"
|
||||
Then the response status code should be 400
|
||||
|
||||
|
||||
@remove-pages @positive
|
||||
Scenario: remove-pages removes the specified page from a multi-page PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | 3 |
|
||||
When I send the API request to the endpoint "/api/v1/general/remove-pages"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 4 pages
|
||||
|
||||
|
||||
@remove-pages @positive
|
||||
Scenario Outline: remove-pages with various page selections
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 6 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | <pageNumbers> |
|
||||
When I send the API request to the endpoint "/api/v1/general/remove-pages"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain <remaining> pages
|
||||
|
||||
Examples:
|
||||
| pageNumbers | remaining |
|
||||
| 1 | 5 |
|
||||
| 6 | 5 |
|
||||
| 2,4 | 4 |
|
||||
| 1,2,3 | 3 |
|
||||
|
||||
|
||||
@rearrange-pages @positive
|
||||
Scenario Outline: rearrange-pages with different custom modes
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| customMode | <customMode> |
|
||||
When I send the API request to the endpoint "/api/v1/general/rearrange-pages"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain <expectedPages> pages
|
||||
|
||||
Examples:
|
||||
| customMode | expectedPages |
|
||||
| REVERSE_ORDER | 4 |
|
||||
|
||||
@rearrange-duplicate
|
||||
Examples:
|
||||
| customMode | expectedPages |
|
||||
| DUPLICATE | 8 |
|
||||
|
||||
Examples:
|
||||
| customMode | expectedPages |
|
||||
| ODD_EVEN_SPLIT | 4 |
|
||||
|
||||
|
||||
@scale-pages @positive
|
||||
Scenario Outline: scale-pages to various standard page sizes
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageSize | <pageSize> |
|
||||
When I send the API request to the endpoint "/api/v1/general/scale-pages"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 3 pages
|
||||
|
||||
Examples:
|
||||
| pageSize |
|
||||
| A4 |
|
||||
| LETTER |
|
||||
| A3 |
|
||||
| LEGAL |
|
||||
|
||||
|
||||
@crop @positive
|
||||
Scenario: crop PDF pages to a specific region
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| x | 0 |
|
||||
| y | 0 |
|
||||
| width | 50 |
|
||||
| height | 50 |
|
||||
When I send the API request to the endpoint "/api/v1/general/crop"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 2 pages
|
||||
|
||||
|
||||
@crop @positive
|
||||
Scenario: crop single-page PDF preserves page count
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| x | 0 |
|
||||
| y | 0 |
|
||||
| width | 50 |
|
||||
| height | 50 |
|
||||
When I send the API request to the endpoint "/api/v1/general/crop"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response PDF should contain 1 pages
|
||||
|
||||
|
||||
@pdf-to-single-page @positive
|
||||
Scenario: pdf-to-single-page combines all pages into one long page
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
When I send the API request to the endpoint "/api/v1/general/pdf-to-single-page"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 1 pages
|
||||
|
||||
|
||||
@pdf-to-single-page @positive
|
||||
Scenario: pdf-to-single-page with a single-page input returns one page
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
When I send the API request to the endpoint "/api/v1/general/pdf-to-single-page"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response PDF should contain 1 pages
|
||||
|
||||
|
||||
@multi-page-layout @positive
|
||||
Scenario Outline: multi-page-layout combines input pages onto fewer sheets
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pagesPerSheet | <pagesPerSheet> |
|
||||
When I send the API request to the endpoint "/api/v1/general/multi-page-layout"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain <outputPages> pages
|
||||
|
||||
Examples:
|
||||
| pagesPerSheet | outputPages |
|
||||
| 2 | 2 |
|
||||
| 4 | 1 |
|
||||
|
||||
|
||||
@multi-page-layout @positive
|
||||
Scenario: multi-page-layout with 9 pages per sheet on 9 input pages
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 9 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pagesPerSheet | 9 |
|
||||
When I send the API request to the endpoint "/api/v1/general/multi-page-layout"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 1 pages
|
||||
|
||||
|
||||
@booklet-imposition @positive
|
||||
Scenario: booklet-imposition returns a valid PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pagesPerSheet | 2 |
|
||||
When I send the API request to the endpoint "/api/v1/general/booklet-imposition"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
|
||||
|
||||
@booklet-imposition @positive
|
||||
Scenario: booklet-imposition with 8-page input returns valid PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 8 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pagesPerSheet | 2 |
|
||||
When I send the API request to the endpoint "/api/v1/general/booklet-imposition"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
|
||||
|
||||
@remove-image-pdf @positive
|
||||
Scenario: remove-image-pdf strips images from a PDF containing images
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 images of size 100x100 on 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/general/remove-image-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
@remove-image-pdf @positive
|
||||
Scenario: remove-image-pdf on a plain text PDF returns a PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/general/remove-image-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
@@ -0,0 +1,42 @@
|
||||
@info
|
||||
Feature: Info API Validation
|
||||
|
||||
@status @positive
|
||||
Scenario: Get application status
|
||||
When I send a GET request to "/api/v1/info/status"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
And the JSON value of "status" should be "UP"
|
||||
|
||||
@uptime @positive
|
||||
Scenario: Get application uptime
|
||||
When I send a GET request to "/api/v1/info/uptime"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@requests @positive
|
||||
Scenario: Get total request count
|
||||
When I send a GET request to "/api/v1/info/requests"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@requests @positive
|
||||
Scenario: Get per-endpoint request counts
|
||||
When I send a GET request to "/api/v1/info/requests/all"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@load @positive
|
||||
Scenario: Get current system load
|
||||
When I send a GET request to "/api/v1/info/load"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@load @positive
|
||||
Scenario: Get per-endpoint load statistics
|
||||
When I send a GET request to "/api/v1/info/load/all"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 0
|
||||
@@ -0,0 +1,77 @@
|
||||
@jwt @auth @user_mgmt
|
||||
Feature: Invite Link API
|
||||
|
||||
Tests for the invite link REST API, which allows admins to manage
|
||||
registration invite links.
|
||||
|
||||
Endpoints (base: /api/v1/invite — from @InviteApi annotation):
|
||||
- POST /api/v1/invite/generate (admin only, requires MAIL_ENABLEINVITES)
|
||||
- GET /api/v1/invite/list (admin only)
|
||||
- GET /api/v1/invite/validate/{token} (public)
|
||||
- DELETE /api/v1/invite/revoke/{inviteId} (admin only)
|
||||
- POST /api/v1/invite/cleanup (admin only)
|
||||
|
||||
NOTE: The /generate endpoint requires MAIL_ENABLEINVITES=true AND an SMTP
|
||||
server. Since the CI environment has no SMTP, generate-dependent scenarios
|
||||
(generate, full lifecycle, revoke-by-id) are omitted. Auth guard tests for
|
||||
those endpoints are still covered.
|
||||
|
||||
Admin credentials: username=admin, password=stirling
|
||||
|
||||
# =========================================================================
|
||||
# GENERATE INVITE LINK – auth guard only (no SMTP in CI)
|
||||
# =========================================================================
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to generate invite link returns 401
|
||||
When I send a POST request to "/api/v1/invite/generate" with no authentication and params "role=ROLE_USER&expiryHours=24&sendEmail=false"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# LIST INVITE LINKS
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can list all active invite links
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/invite/list" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "invites" should be a list
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to list invite links returns 401
|
||||
When I send a GET request to "/api/v1/invite/list" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# VALIDATE INVITE TOKEN (public endpoint)
|
||||
# =========================================================================
|
||||
|
||||
@negative
|
||||
Scenario: Validating a non-existent invite token returns 404 or 400
|
||||
When I send a GET request to "/api/v1/invite/validate/completely-invalid-token-xyz-999" with no authentication
|
||||
Then the response status code should be one of "400, 404"
|
||||
|
||||
# =========================================================================
|
||||
# REVOKE INVITE LINK – auth guard only
|
||||
# =========================================================================
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to revoke invite link returns 401
|
||||
When I send a DELETE request to "/api/v1/invite/revoke/some-id-xyz" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# CLEANUP EXPIRED INVITE LINKS
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can trigger cleanup of expired invite links
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/invite/cleanup" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@negative
|
||||
Scenario: Unauthenticated request to cleanup invite links returns 401
|
||||
When I send a POST request to "/api/v1/invite/cleanup" with no authentication
|
||||
Then the response status code should be 401
|
||||
@@ -0,0 +1,379 @@
|
||||
@jwt @auth
|
||||
Feature: JWT Authentication End-to-End
|
||||
|
||||
Comprehensive end-to-end tests for JWT-based authentication covering login,
|
||||
token validation, token refresh, logout, role-based access control, and
|
||||
API key authentication.
|
||||
|
||||
Admin credentials: username=admin, password=stirling (see docker-compose-security-with-login.yml)
|
||||
Global API key: 123456789
|
||||
|
||||
# =========================================================================
|
||||
# LOGIN SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@login @positive
|
||||
Scenario: Successful admin login returns JWT token
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response should contain a JWT access token
|
||||
And the response JSON should have field "user"
|
||||
And the response JSON should have a user with username "admin"
|
||||
And the response JSON should have a user with role "ROLE_ADMIN"
|
||||
|
||||
@login @positive
|
||||
Scenario: Login response includes token expiry in seconds
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And the response JSON session field "expires_in" should be positive
|
||||
|
||||
@login @positive
|
||||
Scenario: JWT access token has valid three-part structure
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And the JWT access token should have three dot-separated parts
|
||||
|
||||
@login @positive
|
||||
Scenario: Login response user object contains required fields
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And the response JSON user field "email" should not be empty
|
||||
And the response JSON user field "username" should not be empty
|
||||
And the response JSON user field "role" should not be empty
|
||||
And the response JSON user field "enabled" should not be empty
|
||||
|
||||
@login @positive
|
||||
Scenario: Login response user authentication type is WEB
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And the response JSON user field "authenticationType" should equal "web"
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with wrong password returns 401
|
||||
When I login with username "admin" and password "completely_wrong_password_xyz"
|
||||
Then the response status code should be 401
|
||||
And the response JSON error should contain "Invalid"
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with SQL injection in username is safely rejected
|
||||
When I login with username "admin' OR '1'='1" and password "anypass"
|
||||
Then the response status code should be 401
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with script injection in username is safely rejected
|
||||
When I login with username "<script>alert(1)</script>" and password "anypass"
|
||||
Then the response status code should be 401
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with non-existent user returns 401
|
||||
When I login with username "no_such_user_abc999xyz" and password "anypassword123"
|
||||
Then the response status code should be 401
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with empty username returns 400
|
||||
When I login with an empty username and password "stirling"
|
||||
Then the response status code should be 400
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with empty password returns 400
|
||||
When I login with username "admin" and an empty password
|
||||
Then the response status code should be 400
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with null-equivalent username returns 400
|
||||
When I login with only password "stirling"
|
||||
Then the response status code should be 400
|
||||
|
||||
@login @negative
|
||||
Scenario: Login with null-equivalent password returns 400
|
||||
When I login with only username "admin"
|
||||
Then the response status code should be 400
|
||||
|
||||
@login @negative
|
||||
Scenario: Multiple sequential failed login attempts are all rejected
|
||||
When I login with username "admin" and password "badpass1"
|
||||
Then the response status code should be 401
|
||||
When I login with username "admin" and password "badpass2"
|
||||
Then the response status code should be 401
|
||||
When I login with username "admin" and password "badpass3"
|
||||
Then the response status code should be 401
|
||||
|
||||
@login @negative
|
||||
Scenario: Successful login clears lockout after failed attempts
|
||||
When I login with username "admin" and password "wrongpass"
|
||||
Then the response status code should be 401
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And the response should contain a JWT access token
|
||||
|
||||
# =========================================================================
|
||||
# JWT /me ENDPOINT SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@me @positive
|
||||
Scenario: Get current user with valid admin JWT token
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/auth/me" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response JSON should have field "user"
|
||||
And the response JSON should have a user with username "admin"
|
||||
And the response JSON should have a user with role "ROLE_ADMIN"
|
||||
|
||||
@me @positive
|
||||
Scenario: Get current user with JWT shows correct user data
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/auth/me" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON user field "email" should not be empty
|
||||
And the response JSON user field "enabled" should not be empty
|
||||
|
||||
@me @negative
|
||||
Scenario: Get current user without any authentication returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
@me @negative
|
||||
Scenario: Get current user with completely invalid JWT token returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with an invalid JWT token "not.a.jwt"
|
||||
Then the response status code should be 401
|
||||
|
||||
@me @negative
|
||||
Scenario: Get current user with random garbage token returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with an invalid JWT token "eyJhbGciOiJSUzI1NiJ9.ZmFrZXBheWxvYWQ.ZmFrZXNpZ25hdHVyZQ"
|
||||
Then the response status code should be 401
|
||||
|
||||
@me @negative
|
||||
Scenario: Get current user with malformed authorization header returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with a malformed authorization header
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# TOKEN REFRESH SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@refresh @positive
|
||||
Scenario: Refresh a valid JWT token returns a new access token
|
||||
Given I am logged in as admin
|
||||
When I refresh the JWT token
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response should contain a JWT access token
|
||||
And the response JSON should have field "user"
|
||||
And the response JSON should have a user with username "admin"
|
||||
|
||||
@refresh @positive
|
||||
Scenario: Refreshed token has valid three-part JWT structure
|
||||
Given I am logged in as admin
|
||||
When I refresh the JWT token
|
||||
Then the response status code should be 200
|
||||
And the JWT access token should have three dot-separated parts
|
||||
|
||||
@refresh @positive
|
||||
Scenario: Refreshed JWT token can be used to authenticate subsequent requests
|
||||
Given I am logged in as admin
|
||||
When I refresh the JWT token
|
||||
Then the response status code should be 200
|
||||
And I update the stored JWT token from the response
|
||||
When I send a GET request to "/api/v1/auth/me" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON should have a user with username "admin"
|
||||
|
||||
@refresh @positive
|
||||
Scenario: Refreshed token includes positive expiry time
|
||||
Given I am logged in as admin
|
||||
When I refresh the JWT token
|
||||
Then the response status code should be 200
|
||||
And the response JSON session field "expires_in" should be positive
|
||||
|
||||
@refresh @negative
|
||||
Scenario: Refresh without any token returns 401
|
||||
When I send a POST request to "/api/v1/auth/refresh" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
@refresh @negative
|
||||
Scenario: Refresh with invalid token returns 401
|
||||
When I send a POST request to "/api/v1/auth/refresh" with an invalid JWT token "bad.token.value"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# LOGOUT SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@logout @positive
|
||||
Scenario: Logout with valid admin JWT token succeeds
|
||||
Given I am logged in as admin
|
||||
When I logout with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "message" should equal "Logged out successfully"
|
||||
|
||||
@logout @token @positive
|
||||
Scenario: JWT token remains usable after logout (stateless – no server-side revocation)
|
||||
# JWT is stateless: logout only clears the server SecurityContext for that request.
|
||||
# The signed token itself is not blacklisted, so it stays valid until its expiry.
|
||||
# This is expected behaviour; add a token blacklist if revocation is required.
|
||||
Given I am logged in as admin
|
||||
When I logout with JWT authentication
|
||||
Then the response status code should be 200
|
||||
When I send a GET request to "/api/v1/auth/me" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON should have a user with username "admin"
|
||||
|
||||
# =========================================================================
|
||||
# ROLE-BASED ACCESS CONTROL SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@role @admin @positive
|
||||
Scenario: Admin JWT allows access to admin-only MFA management endpoint
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/auth/mfa/disable/admin/admin" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@role @admin @positive
|
||||
Scenario: Admin JWT correctly identifies ROLE_ADMIN in /me response
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/auth/me" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON should have a user with role "ROLE_ADMIN"
|
||||
|
||||
@role @negative
|
||||
Scenario: Request to admin-only endpoint without JWT returns 401
|
||||
When I send a POST request to "/api/v1/auth/mfa/disable/admin/admin" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
@role @negative
|
||||
Scenario: Request to admin-only endpoint with invalid JWT returns 401
|
||||
When I send a POST request to "/api/v1/auth/mfa/disable/admin/admin" with an invalid JWT token "bad.jwt.token"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# API KEY AUTHENTICATION SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@apikey @positive
|
||||
Scenario: Valid API key allows access to /me endpoint
|
||||
When I send a GET request to "/api/v1/auth/me" with API key "123456789"
|
||||
Then the response status code should be 200
|
||||
And the response JSON should have field "user"
|
||||
|
||||
@apikey @positive
|
||||
Scenario: Valid API key /me response contains user information
|
||||
When I send a GET request to "/api/v1/auth/me" with API key "123456789"
|
||||
Then the response status code should be 200
|
||||
And the response JSON user field "username" should not be empty
|
||||
And the response JSON user field "role" should not be empty
|
||||
|
||||
@apikey @negative
|
||||
Scenario: Invalid API key returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with API key "invalid_key_xyz_999"
|
||||
Then the response status code should be 401
|
||||
|
||||
@apikey @negative
|
||||
Scenario: Absent API key with no other auth returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# MFA SETUP SCENARIOS (requires JWT authentication)
|
||||
# =========================================================================
|
||||
|
||||
@mfa @positive
|
||||
Scenario: Authenticated admin can initiate or is already past MFA setup
|
||||
Given I am logged in as admin
|
||||
When I send a GET request to "/api/v1/auth/mfa/setup" with JWT authentication
|
||||
Then the response status code should be one of "200, 409"
|
||||
|
||||
@mfa @negative
|
||||
Scenario: MFA setup endpoint requires authentication
|
||||
When I send a GET request to "/api/v1/auth/mfa/setup" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
@mfa @negative
|
||||
Scenario: MFA enable with a random invalid TOTP code returns an error
|
||||
Given I am logged in as admin
|
||||
When I send a JSON POST request to "/api/v1/auth/mfa/enable" with JWT authentication and body '{"code": "000000"}'
|
||||
Then the response status code should be one of "400, 401, 409"
|
||||
|
||||
@mfa @admin @positive
|
||||
Scenario: Admin can disable MFA for any user via admin endpoint
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/auth/mfa/disable/admin/admin" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "enabled" should equal "false"
|
||||
|
||||
# =========================================================================
|
||||
# USER REGISTRATION SCENARIOS
|
||||
# =========================================================================
|
||||
|
||||
@register @positive
|
||||
Scenario: Register a new unique user account succeeds or reports license limit
|
||||
When I send a JSON POST request to "/api/v1/user/register" with API key "123456789" and body '{"username": "test_register_user_bdd", "password": "SecurePass123!"}'
|
||||
Then the response status code should be one of "200, 201, 400"
|
||||
|
||||
@register @negative
|
||||
Scenario: Register with duplicate username returns error
|
||||
When I send a JSON POST request to "/api/v1/user/register" with API key "123456789" and body '{"username": "admin", "password": "SecurePass123!"}'
|
||||
Then the response status code should be 400
|
||||
And the response JSON error should contain "already exists"
|
||||
|
||||
@register @negative
|
||||
Scenario: Register with empty password returns error
|
||||
When I send a JSON POST request to "/api/v1/user/register" with API key "123456789" and body '{"username": "new_user_empty_pass", "password": ""}'
|
||||
Then the response status code should be 400
|
||||
|
||||
@register @negative
|
||||
Scenario: Newly registered user cannot login before an admin enables the account
|
||||
# Registration creates accounts with enabled=false; immediate login must be rejected.
|
||||
# Username is intentionally unique to avoid conflicts with other test runs.
|
||||
When I send a JSON POST request to "/api/v1/user/register" with API key "123456789" and body '{"username": "bdd_disabled_user_99x", "password": "SecurePass123!"}'
|
||||
Then the response status code should be one of "201, 400"
|
||||
When I login with username "bdd_disabled_user_99x" and password "SecurePass123!"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# TOKEN VALIDATION EDGE CASES
|
||||
# =========================================================================
|
||||
|
||||
@token @negative
|
||||
Scenario: Empty Authorization header value returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with an empty Authorization header
|
||||
Then the response status code should be 401
|
||||
|
||||
@token @negative
|
||||
Scenario: Authorization header without Bearer prefix returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with Authorization header value "Basic dXNlcjpwYXNz"
|
||||
Then the response status code should be 401
|
||||
|
||||
@token @negative
|
||||
Scenario: Authorization header with only the Bearer keyword and no token returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with Authorization header value "Bearer"
|
||||
Then the response status code should be 401
|
||||
|
||||
@token @negative
|
||||
Scenario: Authorization header with Bearer prefix but only whitespace returns 401
|
||||
When I send a GET request to "/api/v1/auth/me" with Authorization header value "Bearer "
|
||||
Then the response status code should be 401
|
||||
|
||||
@token @positive
|
||||
Scenario: Login then immediately use the token verifies token is active
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And I store the JWT token from the login response
|
||||
When I send a GET request to "/api/v1/auth/me" with the stored JWT token
|
||||
Then the response status code should be 200
|
||||
And the response JSON should have a user with username "admin"
|
||||
|
||||
@token @positive
|
||||
Scenario: Full login, use, refresh, and re-use flow
|
||||
When I login with username "admin" and password "stirling"
|
||||
Then the response status code should be 200
|
||||
And I store the JWT token from the login response
|
||||
When I send a GET request to "/api/v1/auth/me" with the stored JWT token
|
||||
Then the response status code should be 200
|
||||
When I refresh the stored JWT token
|
||||
Then the response status code should be 200
|
||||
And I update the stored JWT token from the response
|
||||
When I send a GET request to "/api/v1/auth/me" with the stored JWT token
|
||||
Then the response status code should be 200
|
||||
And the response JSON should have a user with username "admin"
|
||||
@@ -0,0 +1,72 @@
|
||||
@general
|
||||
Feature: Merge and Overlay PDF API Validation
|
||||
|
||||
@merge @positive
|
||||
Scenario: Merge two PDFs with default options
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And I also generate a PDF file as "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/general/merge-pdfs"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@merge @positive
|
||||
Scenario: Merge three PDFs with byFileName sort
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And I also generate a PDF file as "fileInput"
|
||||
And I also generate a PDF file as "fileInput"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| sortType | byFileName |
|
||||
When I send the API request to the endpoint "/api/v1/general/merge-pdfs"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@merge @positive
|
||||
Scenario: Merge PDFs with table of contents
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And I also generate a PDF file as "fileInput"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| generateToc | true |
|
||||
When I send the API request to the endpoint "/api/v1/general/merge-pdfs"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@overlay @positive
|
||||
Scenario: Overlay PDF in sequential mode foreground
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And I also generate a PDF file as "overlayFiles"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| overlayMode | SequentialOverlay |
|
||||
| overlayPosition | 0 |
|
||||
When I send the API request to the endpoint "/api/v1/general/overlay-pdfs"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@overlay @positive
|
||||
Scenario: Overlay PDF in interleaved mode background
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And I also generate a PDF file as "overlayFiles"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| overlayMode | InterleavedOverlay |
|
||||
| overlayPosition | 1 |
|
||||
When I send the API request to the endpoint "/api/v1/general/overlay-pdfs"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
@@ -0,0 +1,224 @@
|
||||
@misc-new
|
||||
Feature: Miscellaneous PDF Operations API Validation
|
||||
|
||||
|
||||
@add-stamp @positive
|
||||
Scenario Outline: add-stamp applies a text stamp at various positions
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| stampType | text |
|
||||
| stampText | Test Stamp |
|
||||
| position | <position> |
|
||||
| fontSize | 20 |
|
||||
| rotation | 0 |
|
||||
| opacity | 0.5 |
|
||||
| customColor | #d3d3d3 |
|
||||
| customMargin | medium |
|
||||
| overrideX | -1 |
|
||||
| overrideY | -1 |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-stamp"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 3 pages
|
||||
|
||||
Examples:
|
||||
| position |
|
||||
| 1 |
|
||||
| 5 |
|
||||
| 9 |
|
||||
|
||||
|
||||
@add-stamp @positive
|
||||
Scenario: add-stamp with rotation and full opacity
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| stampType | text |
|
||||
| stampText | CONFIDENTIAL |
|
||||
| position | 5 |
|
||||
| fontSize | 30 |
|
||||
| rotation | 45 |
|
||||
| opacity | 1.0 |
|
||||
| customColor | #d3d3d3 |
|
||||
| customMargin | medium |
|
||||
| overrideX | -1 |
|
||||
| overrideY | -1 |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-stamp"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 2 pages
|
||||
|
||||
|
||||
@add-page-numbers @positive
|
||||
Scenario Outline: add-page-numbers inserts numbers at various positions
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| startingNumber | 1 |
|
||||
| position | <position> |
|
||||
| fontSize | 12 |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-page-numbers"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 4 pages
|
||||
|
||||
Examples:
|
||||
| position |
|
||||
| 1 |
|
||||
| 2 |
|
||||
| 3 |
|
||||
| 4 |
|
||||
| 5 |
|
||||
| 6 |
|
||||
| 7 |
|
||||
| 8 |
|
||||
| 9 |
|
||||
|
||||
|
||||
@add-page-numbers @positive
|
||||
Scenario: add-page-numbers starting from a custom number
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| startingNumber | 10 |
|
||||
| position | 2 |
|
||||
| fontSize | 14 |
|
||||
When I send the API request to the endpoint "/api/v1/misc/add-page-numbers"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 200
|
||||
And the response PDF should contain 5 pages
|
||||
|
||||
|
||||
@unlock-pdf-forms @positive
|
||||
Scenario: unlock-pdf-forms returns a valid unlocked PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/misc/unlock-pdf-forms"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
@scanner-effect @positive
|
||||
Scenario Outline: scanner-effect applies a scan simulation to a PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| colorspace | <colorspace> |
|
||||
| quality | <quality> |
|
||||
When I send the API request to the endpoint "/api/v1/misc/scanner-effect"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response PDF should contain 1 pages
|
||||
|
||||
Examples:
|
||||
| colorspace | quality |
|
||||
| grayscale | low |
|
||||
| grayscale | medium |
|
||||
| grayscale | high |
|
||||
|
||||
|
||||
@replace-invert-pdf @positive
|
||||
Scenario: replace-invert-pdf returns a valid PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| replaceAndInvertOption | HIGH_CONTRAST_COLOR |
|
||||
| highContrastColorCombination | WHITE_TEXT_ON_BLACK |
|
||||
When I send the API request to the endpoint "/api/v1/misc/replace-invert-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response PDF should contain 2 pages
|
||||
|
||||
|
||||
@replace-invert-pdf @positive
|
||||
Scenario: replace-invert-pdf on a PDF with images returns a valid PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 images of size 100x100 on 1 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| replaceAndInvertOption | FULL_INVERSION |
|
||||
| highContrastColorCombination | WHITE_TEXT_ON_BLACK |
|
||||
When I send the API request to the endpoint "/api/v1/misc/replace-invert-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
@decompress-pdf @positive
|
||||
Scenario: decompress-pdf returns a decompressed PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
When I send the API request to the endpoint "/api/v1/misc/decompress-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response PDF should contain 3 pages
|
||||
|
||||
|
||||
@decompress-pdf @positive
|
||||
Scenario: decompress-pdf on a single-page PDF returns valid output
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
When I send the API request to the endpoint "/api/v1/misc/decompress-pdf"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response PDF should contain 1 pages
|
||||
|
||||
|
||||
@auto-rename @positive
|
||||
Scenario: auto-rename renames PDF using first text content as filename
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| useFirstTextAsFallback | true |
|
||||
When I send the API request to the endpoint "/api/v1/misc/auto-rename"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
@auto-rename @positive
|
||||
Scenario: auto-rename on a plain text PDF returns a PDF with a derived name
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| useFirstTextAsFallback | true |
|
||||
When I send the API request to the endpoint "/api/v1/misc/auto-rename"
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
|
||||
@show-javascript @positive
|
||||
Scenario: show-javascript returns a response for a PDF without JavaScript
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/misc/show-javascript"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
|
||||
@show-javascript @positive
|
||||
Scenario: show-javascript on a multi-page PDF returns status 200
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages with random text
|
||||
When I send the API request to the endpoint "/api/v1/misc/show-javascript"
|
||||
Then the response status code should be 200
|
||||
@@ -0,0 +1,81 @@
|
||||
Feature: Mobile Scanner Session API
|
||||
|
||||
Tests for the mobile scanner REST API, which manages short-lived upload
|
||||
sessions used by mobile scanning clients.
|
||||
|
||||
All endpoints are PUBLIC (no authentication required). Sessions are
|
||||
identified by a caller-supplied session ID string.
|
||||
|
||||
Session IDs must match [a-zA-Z0-9-]+ (alphanumeric and hyphens only –
|
||||
underscores are rejected with 400).
|
||||
|
||||
# =========================================================================
|
||||
# SESSION LIFECYCLE
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Create a new mobile scanner session
|
||||
When I send a POST request to "/api/v1/mobile-scanner/create-session/bdd-test-session-001" with no authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "success" should be true
|
||||
And the response JSON field "sessionId" should not be empty
|
||||
|
||||
@positive
|
||||
Scenario: Validate an existing mobile scanner session
|
||||
When I send a POST request to "/api/v1/mobile-scanner/create-session/bdd-test-session-002" with no authentication
|
||||
Then the response status code should be 200
|
||||
When I send a GET request to "/api/v1/mobile-scanner/validate-session/bdd-test-session-002" with no authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "valid" should be true
|
||||
|
||||
@positive
|
||||
Scenario: List files in an existing session returns empty list initially
|
||||
When I send a POST request to "/api/v1/mobile-scanner/create-session/bdd-test-session-003" with no authentication
|
||||
Then the response status code should be 200
|
||||
When I send a GET request to "/api/v1/mobile-scanner/files/bdd-test-session-003" with no authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@positive
|
||||
Scenario: Delete an existing mobile scanner session
|
||||
When I send a POST request to "/api/v1/mobile-scanner/create-session/bdd-test-session-004" with no authentication
|
||||
Then the response status code should be 200
|
||||
When I send a DELETE request to "/api/v1/mobile-scanner/session/bdd-test-session-004" with no authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "success" should be true
|
||||
|
||||
@positive
|
||||
Scenario: Full session lifecycle – create, validate, list files, delete
|
||||
When I send a POST request to "/api/v1/mobile-scanner/create-session/bdd-test-session-full" with no authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "sessionId" should not be empty
|
||||
When I send a GET request to "/api/v1/mobile-scanner/validate-session/bdd-test-session-full" with no authentication
|
||||
Then the response status code should be 200
|
||||
And the response JSON field "valid" should be true
|
||||
When I send a GET request to "/api/v1/mobile-scanner/files/bdd-test-session-full" with no authentication
|
||||
Then the response status code should be 200
|
||||
When I send a DELETE request to "/api/v1/mobile-scanner/session/bdd-test-session-full" with no authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
# =========================================================================
|
||||
# EDGE CASES
|
||||
# =========================================================================
|
||||
|
||||
@negative
|
||||
Scenario: Session ID with underscores is rejected as invalid format
|
||||
When I send a POST request to "/api/v1/mobile-scanner/create-session/invalid_underscore_id" with no authentication
|
||||
Then the response status code should be 400
|
||||
|
||||
@negative
|
||||
Scenario: Validate a non-existent session returns not-found response
|
||||
When I send a GET request to "/api/v1/mobile-scanner/validate-session/nonexistent-session-xyz" with no authentication
|
||||
Then the response status code should be one of "200, 404"
|
||||
|
||||
@negative
|
||||
Scenario: List files for a non-existent session returns 404 or empty
|
||||
When I send a GET request to "/api/v1/mobile-scanner/files/nonexistent-session-abc" with no authentication
|
||||
Then the response status code should be one of "200, 404"
|
||||
|
||||
@negative
|
||||
Scenario: Delete a non-existent session returns 404 or success
|
||||
When I send a DELETE request to "/api/v1/mobile-scanner/session/nonexistent-session-xyz" with no authentication
|
||||
Then the response status code should be one of "200, 404"
|
||||
@@ -0,0 +1,163 @@
|
||||
@security
|
||||
Feature: Security API Validation
|
||||
|
||||
@sanitize @positive
|
||||
Scenario: Sanitize PDF with all options enabled
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| removeJavaScript | true |
|
||||
| removeEmbeddedFiles | true |
|
||||
| removeXMPMetadata | true |
|
||||
| removeMetadata | true |
|
||||
| removeLinks | true |
|
||||
| removeFonts | false |
|
||||
When I send the API request to the endpoint "/api/v1/security/sanitize-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@sanitize @positive
|
||||
Scenario: Sanitize PDF with default options
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 1 pages
|
||||
When I send the API request to the endpoint "/api/v1/security/sanitize-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@sanitize @positive
|
||||
Scenario: Sanitize PDF removing only metadata
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| removeMetadata | true |
|
||||
| removeXMPMetadata | true |
|
||||
When I send the API request to the endpoint "/api/v1/security/sanitize-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@auto-redact @positive
|
||||
Scenario: Auto-redact searchable text in PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the pdf pages all contain the text "CONFIDENTIAL"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| listOfText | CONFIDENTIAL |
|
||||
| useRegex | false |
|
||||
| wholeWordSearch| true |
|
||||
| convertPDFToImage | false |
|
||||
When I send the API request to the endpoint "/api/v1/security/auto-redact"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@auto-redact @positive
|
||||
Scenario: Auto-redact with regex pattern
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf pages all contain the text "SECRET-1234"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| listOfText | SECRET-\d+ |
|
||||
| useRegex | true |
|
||||
When I send the API request to the endpoint "/api/v1/security/auto-redact"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@auto-redact @positive
|
||||
Scenario: Auto-redact and convert to image
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
And the pdf pages all contain the text "PRIVATE"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| listOfText | PRIVATE |
|
||||
| convertPDFToImage | true |
|
||||
When I send the API request to the endpoint "/api/v1/security/auto-redact"
|
||||
Then the response status code should be 200
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@redact @positive
|
||||
Scenario: Redact specific pages fully
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | 2,4 |
|
||||
| pageRedactionColor | #000000 |
|
||||
When I send the API request to the endpoint "/api/v1/security/redact"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@redact @positive
|
||||
Scenario: Redact single page with custom color
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | 1 |
|
||||
| pageRedactionColor | #ff0000 |
|
||||
When I send the API request to the endpoint "/api/v1/security/redact"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@redact @positive
|
||||
Scenario: Redact all pages
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 4 pages
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| pageNumbers | all |
|
||||
When I send the API request to the endpoint "/api/v1/security/redact"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
|
||||
@verify @positive
|
||||
Scenario: Verify PDF-A compliance
|
||||
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
||||
When I send the API request to the endpoint "/api/v1/security/verify-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 2
|
||||
|
||||
@verify @positive
|
||||
Scenario: Verify standard PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/security/verify-pdf"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/json"
|
||||
And the response file should have size greater than 2
|
||||
|
||||
@remove-cert-sign @positive
|
||||
Scenario: Remove cert signature from unsigned PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 2 pages
|
||||
When I send the API request to the endpoint "/api/v1/security/remove-cert-sign"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response file should have extension ".pdf"
|
||||
|
||||
@remove-cert-sign @positive
|
||||
Scenario: Remove cert signature from multi-page unsigned PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 5 pages
|
||||
When I send the API request to the endpoint "/api/v1/security/remove-cert-sign"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
@@ -0,0 +1,499 @@
|
||||
"""
|
||||
Step definitions for JWT authentication end-to-end tests.
|
||||
|
||||
Covers:
|
||||
- Login / logout (POST /api/v1/auth/login, /logout)
|
||||
- Token refresh (POST /api/v1/auth/refresh)
|
||||
- Current user (GET /api/v1/auth/me)
|
||||
- Role-based access (admin-only endpoints)
|
||||
- API key authentication (X-API-KEY header)
|
||||
- MFA endpoints
|
||||
- User registration
|
||||
"""
|
||||
|
||||
import json as json_module
|
||||
|
||||
import requests
|
||||
from behave import given, then, when
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
# Default test credentials (set in docker-compose-security-with-login.yml)
|
||||
ADMIN_USERNAME = "admin"
|
||||
ADMIN_PASSWORD = "stirling"
|
||||
GLOBAL_API_KEY = "123456789"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _jwt_headers(context):
|
||||
"""Return Authorization: Bearer headers using the stored JWT token."""
|
||||
token = getattr(context, "jwt_token", None)
|
||||
assert token, "No JWT token stored in context – did you use 'Given I am logged in as admin'?"
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _api_key_headers(api_key):
|
||||
"""Return X-API-KEY headers for a given key."""
|
||||
return {"X-API-KEY": api_key}
|
||||
|
||||
|
||||
def _do_login(username, password):
|
||||
"""Perform a login POST and return the raw response."""
|
||||
payload = {"username": username, "password": password}
|
||||
return requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/login",
|
||||
json=payload,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GIVEN – session setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("I am logged in as admin")
|
||||
def step_logged_in_as_admin(context):
|
||||
"""Login as the default admin user and store the JWT token in context."""
|
||||
response = _do_login(ADMIN_USERNAME, ADMIN_PASSWORD)
|
||||
assert response.status_code == 200, (
|
||||
f"Admin login failed (status {response.status_code}): {response.text}"
|
||||
)
|
||||
data = response.json()
|
||||
context.jwt_token = data["session"]["access_token"]
|
||||
|
||||
|
||||
@given("I store the JWT token")
|
||||
def step_store_current_jwt(context):
|
||||
"""Store the currently held jwt_token into context for later comparison."""
|
||||
assert hasattr(context, "jwt_token") and context.jwt_token, (
|
||||
"No JWT token available – did you log in first?"
|
||||
)
|
||||
context.original_jwt_token = context.jwt_token
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – login
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I login with username "{username}" and password "{password}"')
|
||||
def step_login(context, username, password):
|
||||
"""Send a login request with the given username and password."""
|
||||
payload = {"username": username, "password": password}
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/login",
|
||||
json=payload,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
@when('I login with only username "{username}"')
|
||||
def step_login_only_username(context, username):
|
||||
"""Send a login request with only a username field (no password key)."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/login",
|
||||
json={"username": username},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
@when('I login with only password "{password}"')
|
||||
def step_login_only_password(context, password):
|
||||
"""Send a login request with only a password field (no username key)."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/login",
|
||||
json={"password": password},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
@when('I login with an empty username and password "{password}"')
|
||||
def step_login_empty_username(context, password):
|
||||
"""Send a login request with an explicit empty string as the username."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/login",
|
||||
json={"username": "", "password": password},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
@when('I login with username "{username}" and an empty password')
|
||||
def step_login_empty_password(context, username):
|
||||
"""Send a login request with an explicit empty string as the password."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/login",
|
||||
json={"username": username, "password": ""},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – GET with various authentication methods
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with JWT authentication')
|
||||
def step_get_with_jwt(context, endpoint):
|
||||
"""Send GET request using the stored JWT token in the Authorization header."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with no authentication')
|
||||
def step_get_no_auth(context, endpoint):
|
||||
"""Send GET request with no authentication headers whatsoever."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with an invalid JWT token "{token_value}"')
|
||||
def step_get_with_invalid_jwt(context, endpoint, token_value):
|
||||
"""Send GET request with a specific invalid JWT string."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers={"Authorization": f"Bearer {token_value}"},
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with a malformed authorization header')
|
||||
def step_get_with_malformed_auth(context, endpoint):
|
||||
"""Send GET request with a non-Bearer authorization header."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers={"Authorization": "Basic dXNlcjpwYXNz"},
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with API key "{api_key}"')
|
||||
def step_get_with_api_key(context, endpoint, api_key):
|
||||
"""Send GET request using an X-API-KEY header."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_api_key_headers(api_key),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with Authorization header value "{header_value}"')
|
||||
def step_get_with_auth_header_value(context, endpoint, header_value):
|
||||
"""Send GET request with an arbitrary Authorization header value."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers={"Authorization": header_value},
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with an empty Authorization header')
|
||||
def step_get_with_empty_auth_header(context, endpoint):
|
||||
"""Send GET request with an explicitly empty Authorization header."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers={"Authorization": ""},
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with the stored JWT token')
|
||||
def step_get_with_stored_jwt(context, endpoint):
|
||||
"""Send GET request using the JWT token currently stored in context."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – POST with various authentication methods
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I send a POST request to "{endpoint}" with JWT authentication')
|
||||
def step_post_with_jwt(context, endpoint):
|
||||
"""Send POST request (no body) using the stored JWT token."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a POST request to "{endpoint}" with no authentication')
|
||||
def step_post_no_auth(context, endpoint):
|
||||
"""Send POST request with no authentication headers."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a POST request to "{endpoint}" with an invalid JWT token "{token_value}"')
|
||||
def step_post_with_invalid_jwt(context, endpoint, token_value):
|
||||
"""Send POST request with a specific invalid JWT string."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers={"Authorization": f"Bearer {token_value}"},
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I send a JSON POST request to "{endpoint}" with JWT authentication and body \'{json_body}\''
|
||||
)
|
||||
def step_json_post_with_jwt(context, endpoint, json_body):
|
||||
"""Send JSON POST request using the stored JWT token and a JSON body."""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {context.jwt_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=headers,
|
||||
data=json_body,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I send a JSON POST request to "{endpoint}" with API key "{api_key}" and body \'{json_body}\''
|
||||
)
|
||||
def step_json_post_with_api_key(context, endpoint, api_key, json_body):
|
||||
"""Send JSON POST request using X-API-KEY header and a JSON body."""
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=headers,
|
||||
data=json_body,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – token refresh
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I refresh the JWT token")
|
||||
def step_refresh_jwt(context):
|
||||
"""Send POST /api/v1/auth/refresh with the stored JWT token."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/refresh",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when("I refresh the stored JWT token")
|
||||
def step_refresh_stored_jwt(context):
|
||||
"""Send POST /api/v1/auth/refresh with the stored JWT token (alias)."""
|
||||
step_refresh_jwt(context)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – logout
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I logout with JWT authentication")
|
||||
def step_logout_with_jwt(context):
|
||||
"""Send POST /api/v1/auth/logout using the stored JWT token."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}/api/v1/auth/logout",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN – status code variants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the response status code should be one of "{codes}"')
|
||||
def step_status_code_one_of(context, codes):
|
||||
"""Assert that the response status code matches any one of a comma-separated list."""
|
||||
allowed = [int(c.strip()) for c in codes.split(",")]
|
||||
actual = context.response.status_code
|
||||
assert actual in allowed, (
|
||||
f"Expected status code to be one of {allowed} but got {actual}. "
|
||||
f"Body: {context.response.text[:500]}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN – JWT structure assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the response should contain a JWT access token")
|
||||
def step_response_contains_jwt(context):
|
||||
"""Assert the response has a session.access_token that looks like a JWT."""
|
||||
data = context.response.json()
|
||||
assert "session" in data, f"No 'session' key in response: {data}"
|
||||
assert "access_token" in data["session"], (
|
||||
f"No 'access_token' in session: {data['session']}"
|
||||
)
|
||||
token = data["session"]["access_token"]
|
||||
assert token, "access_token is empty"
|
||||
parts = token.split(".")
|
||||
assert len(parts) == 3, (
|
||||
f"JWT should have 3 dot-separated parts but got {len(parts)}: {token[:60]}..."
|
||||
)
|
||||
|
||||
|
||||
@then("the JWT access token should have three dot-separated parts")
|
||||
def step_jwt_three_parts(context):
|
||||
"""Assert the access_token in the response is a three-part JWT."""
|
||||
data = context.response.json()
|
||||
token = data.get("session", {}).get("access_token", "")
|
||||
assert token, "No access_token found in response"
|
||||
parts = token.split(".")
|
||||
assert len(parts) == 3, (
|
||||
f"JWT must have 3 parts (header.payload.signature) but got {len(parts)}: {token[:60]}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN – JSON field assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the response JSON should have field \"{field}\"")
|
||||
def step_json_has_field(context, field):
|
||||
"""Assert the top-level response JSON contains the specified field."""
|
||||
data = context.response.json()
|
||||
assert field in data, (
|
||||
f"Expected field '{field}' in response JSON but only found: {list(data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON should have a user with username "{username}"')
|
||||
def step_json_user_username(context, username):
|
||||
"""Assert response.user.username equals the expected value."""
|
||||
data = context.response.json()
|
||||
assert "user" in data, f"No 'user' in response: {list(data.keys())}"
|
||||
actual = data["user"].get("username") or data["user"].get("email", "")
|
||||
assert actual == username, f"Expected username '{username}' but got '{actual}'"
|
||||
|
||||
|
||||
@then('the response JSON should have a user with role "{role}"')
|
||||
def step_json_user_role(context, role):
|
||||
"""Assert response.user.role equals the expected role string."""
|
||||
data = context.response.json()
|
||||
assert "user" in data, f"No 'user' in response: {list(data.keys())}"
|
||||
actual = data["user"].get("role", "")
|
||||
assert actual == role, f"Expected role '{role}' but got '{actual}'"
|
||||
|
||||
|
||||
@then('the response JSON user field "{field}" should not be empty')
|
||||
def step_json_user_field_not_empty(context, field):
|
||||
"""Assert response.user.<field> exists and is non-empty."""
|
||||
data = context.response.json()
|
||||
assert "user" in data, f"No 'user' in response: {list(data.keys())}"
|
||||
value = data["user"].get(field)
|
||||
assert value is not None and str(value) != "", (
|
||||
f"Expected user field '{field}' to be non-empty, got: {value!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON user field "{field}" should equal "{expected}"')
|
||||
def step_json_user_field_equals(context, field, expected):
|
||||
"""Assert response.user.<field> equals the expected string value.
|
||||
|
||||
JSON booleans are compared as lowercase strings ("true"/"false").
|
||||
"""
|
||||
data = context.response.json()
|
||||
assert "user" in data, f"No 'user' in response: {list(data.keys())}"
|
||||
value = data["user"].get(field, "")
|
||||
actual = str(value).lower() if isinstance(value, bool) else str(value)
|
||||
assert actual == expected, (
|
||||
f"Expected user field '{field}' == '{expected}' but got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON field "{field}" should equal "{expected}"')
|
||||
def step_json_top_field_equals(context, field, expected):
|
||||
"""Assert a top-level JSON field equals the expected string value.
|
||||
|
||||
JSON booleans (true/false) are compared as lowercase strings to match
|
||||
JSON serialisation ("true"/"false"), not Python's "True"/"False".
|
||||
"""
|
||||
data = context.response.json()
|
||||
value = data.get(field, "")
|
||||
actual = str(value).lower() if isinstance(value, bool) else str(value)
|
||||
assert actual == expected, (
|
||||
f"Expected JSON field '{field}' == '{expected}' but got '{actual}'. "
|
||||
f"Full response: {data}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON session field "{field}" should be positive')
|
||||
def step_json_session_field_positive(context, field):
|
||||
"""Assert response.session.<field> is a number greater than zero."""
|
||||
data = context.response.json()
|
||||
assert "session" in data, f"No 'session' in response: {list(data.keys())}"
|
||||
value = data["session"].get(field)
|
||||
assert value is not None, f"Field '{field}' not found in session: {data['session']}"
|
||||
assert int(value) > 0, f"Expected session field '{field}' > 0 but got {value}"
|
||||
|
||||
|
||||
@then('the response JSON error should contain "{error_text}"')
|
||||
def step_json_error_contains(context, error_text):
|
||||
"""Assert the error/message/detail field contains the expected substring (case-insensitive)."""
|
||||
data = context.response.json()
|
||||
error = (
|
||||
data.get("error")
|
||||
or data.get("message")
|
||||
or data.get("detail")
|
||||
or ""
|
||||
)
|
||||
assert error_text.lower() in str(error).lower(), (
|
||||
f"Expected '{error_text}' (case-insensitive) in error response but got: '{error}'. "
|
||||
f"Full response: {data}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN – token storage and chaining
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("I store the JWT token from the login response")
|
||||
def step_store_jwt_from_login(context):
|
||||
"""Extract and store access_token from the login response."""
|
||||
data = context.response.json()
|
||||
assert "session" in data and "access_token" in data["session"], (
|
||||
f"No access_token in login response: {data}"
|
||||
)
|
||||
context.jwt_token = data["session"]["access_token"]
|
||||
assert context.jwt_token, "Stored JWT token is empty"
|
||||
|
||||
|
||||
@then("I update the stored JWT token from the response")
|
||||
def step_update_stored_jwt(context):
|
||||
"""Replace the stored JWT token with the new one from the current response."""
|
||||
data = context.response.json()
|
||||
assert "session" in data and "access_token" in data["session"], (
|
||||
f"No access_token in response: {data}"
|
||||
)
|
||||
new_token = data["session"]["access_token"]
|
||||
assert new_token, "New JWT token from response is empty"
|
||||
context.jwt_token = new_token
|
||||
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Step definitions for enterprise and proprietary API endpoints.
|
||||
|
||||
Covers:
|
||||
- User management (password change, API keys, admin CRUD)
|
||||
- Admin settings
|
||||
- Audit log
|
||||
- Invite links
|
||||
- Mobile scanner sessions
|
||||
- Signatures
|
||||
- Teams
|
||||
"""
|
||||
|
||||
import requests
|
||||
from behave import given, then, when
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
|
||||
def _jwt_headers(context):
|
||||
"""Return Authorization: Bearer headers using the stored JWT token."""
|
||||
token = getattr(context, "jwt_token", None)
|
||||
assert token, "No JWT token stored in context – did you use 'Given I am logged in as admin'?"
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _parse_params(params_str):
|
||||
"""Parse 'key1=val1&key2=val2' into a dict, supporting values that contain '='."""
|
||||
result = {}
|
||||
for part in params_str.split("&"):
|
||||
if "=" in part:
|
||||
k, v = part.split("=", 1)
|
||||
result[k.strip()] = v.strip()
|
||||
return result
|
||||
|
||||
|
||||
def _expand_stored(template, context):
|
||||
"""Replace '{stored}' in an endpoint template with context.stored_value."""
|
||||
return template.replace("{stored}", getattr(context, "stored_value", ""))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – POST with query params
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I send a POST request to "{endpoint}" with JWT authentication and params "{params}"')
|
||||
def step_post_with_jwt_and_params(context, endpoint, params):
|
||||
"""Send POST request with query parameters using the stored JWT token."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
params=_parse_params(params),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a POST request to "{endpoint}" with no authentication and params "{params}"')
|
||||
def step_post_no_auth_and_params(context, endpoint, params):
|
||||
"""Send POST request with query parameters and no authentication."""
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
params=_parse_params(params),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – GET with query params
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I send a GET request to "{endpoint}" with JWT authentication and params "{params}"')
|
||||
def step_get_with_jwt_and_params(context, endpoint, params):
|
||||
"""Send GET request with query parameters using the stored JWT token."""
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
params=_parse_params(params),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – DELETE
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I send a DELETE request to "{endpoint}" with JWT authentication')
|
||||
def step_delete_with_jwt(context, endpoint):
|
||||
"""Send DELETE request using the stored JWT token."""
|
||||
context.response = requests.delete(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a DELETE request to "{endpoint}" with no authentication')
|
||||
def step_delete_no_auth(context, endpoint):
|
||||
"""Send DELETE request with no authentication headers."""
|
||||
context.response = requests.delete(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a DELETE request to "{endpoint}" with JWT authentication and params "{params}"')
|
||||
def step_delete_with_jwt_and_params(context, endpoint, params):
|
||||
"""Send DELETE request with query parameters using the stored JWT token."""
|
||||
context.response = requests.delete(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
params=_parse_params(params),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when('I send a DELETE request to "{endpoint}" with no authentication and params "{params}"')
|
||||
def step_delete_no_auth_and_params(context, endpoint, params):
|
||||
"""Send DELETE request with query parameters and no authentication."""
|
||||
context.response = requests.delete(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
params=_parse_params(params),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WHEN – steps that use a previously stored value in the URL path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when(
|
||||
'I use the stored value to send a GET request to "{endpoint_template}" with JWT authentication'
|
||||
)
|
||||
def step_get_stored_jwt(context, endpoint_template):
|
||||
"""Send GET request substituting {stored} in the path with context.stored_value."""
|
||||
endpoint = _expand_stored(endpoint_template, context)
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I use the stored value to send a GET request to "{endpoint_template}" with no authentication'
|
||||
)
|
||||
def step_get_stored_no_auth(context, endpoint_template):
|
||||
"""Send GET request substituting {stored} in the path with no authentication."""
|
||||
endpoint = _expand_stored(endpoint_template, context)
|
||||
context.response = requests.get(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I use the stored value to send a DELETE request to "{endpoint_template}" with JWT authentication'
|
||||
)
|
||||
def step_delete_stored_jwt(context, endpoint_template):
|
||||
"""Send DELETE request substituting {stored} in the path with context.stored_value."""
|
||||
endpoint = _expand_stored(endpoint_template, context)
|
||||
context.response = requests.delete(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I use the stored value to send a POST request to "{endpoint_template}" with JWT authentication'
|
||||
)
|
||||
def step_post_stored_jwt(context, endpoint_template):
|
||||
"""Send POST request substituting {stored} in the path with context.stored_value."""
|
||||
endpoint = _expand_stored(endpoint_template, context)
|
||||
context.response = requests.post(
|
||||
f"{BASE_URL}{endpoint}",
|
||||
headers=_jwt_headers(context),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN – store response value for later steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('I store the response JSON field "{field}"')
|
||||
def step_store_response_field(context, field):
|
||||
"""Store a top-level JSON field from the current response into context.stored_value."""
|
||||
data = context.response.json()
|
||||
value = data.get(field)
|
||||
assert value is not None, f"Field '{field}' not found in response: {data}"
|
||||
context.stored_value = str(value)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THEN – response body / field assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then('the response JSON field "{field}" should not be empty')
|
||||
def step_json_top_field_not_empty(context, field):
|
||||
"""Assert a top-level JSON field is present and non-empty."""
|
||||
data = context.response.json()
|
||||
value = data.get(field)
|
||||
assert value is not None and str(value) != "", (
|
||||
f"Expected field '{field}' to be non-empty, got: {value!r}. "
|
||||
f"Full response: {data}"
|
||||
)
|
||||
|
||||
|
||||
@then("the response body should not be empty")
|
||||
def step_response_body_not_empty(context):
|
||||
"""Assert that the raw response body contains at least one byte."""
|
||||
assert len(context.response.content) > 0, "Response body is empty"
|
||||
|
||||
|
||||
@then("the response JSON should be a list")
|
||||
def step_response_is_list(context):
|
||||
"""Assert that the top-level response JSON value is a list."""
|
||||
data = context.response.json()
|
||||
assert isinstance(data, list), (
|
||||
f"Expected response to be a JSON list but got: {type(data).__name__}. "
|
||||
f"Content: {str(data)[:200]}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON field "{field}" should be a list')
|
||||
def step_json_field_is_list(context, field):
|
||||
"""Assert a top-level JSON field is a list."""
|
||||
data = context.response.json()
|
||||
value = data.get(field)
|
||||
assert isinstance(value, list), (
|
||||
f"Expected field '{field}' to be a list but got: {type(value).__name__}. "
|
||||
f"Full response: {data}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON field "{field}" should be true')
|
||||
def step_json_field_is_true(context, field):
|
||||
"""Assert a top-level JSON boolean field is true."""
|
||||
data = context.response.json()
|
||||
value = data.get(field)
|
||||
actual = str(value).lower() if isinstance(value, bool) else str(value).lower()
|
||||
assert actual == "true", (
|
||||
f"Expected field '{field}' to be true but got: {value!r}. "
|
||||
f"Full response: {data}"
|
||||
)
|
||||
|
||||
|
||||
@then('the response JSON field "{field}" should be false')
|
||||
def step_json_field_is_false(context, field):
|
||||
"""Assert a top-level JSON boolean field is false."""
|
||||
data = context.response.json()
|
||||
value = data.get(field)
|
||||
actual = str(value).lower() if isinstance(value, bool) else str(value).lower()
|
||||
assert actual == "false", (
|
||||
f"Expected field '{field}' to be false but got: {value!r}. "
|
||||
f"Full response: {data}"
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
import json as json_module
|
||||
import os
|
||||
import requests
|
||||
from behave import given, when, then
|
||||
@@ -289,6 +290,273 @@ def save_generated_pdf(context, filename):
|
||||
print(f"Saved generated PDF content to {filename}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-file accumulation steps (same parameter key sent multiple times)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('I also generate a PDF file as "{param}"')
|
||||
def step_also_generate_pdf(context, param):
|
||||
"""Add an additional generated PDF under the given parameter name (supports duplicate keys)."""
|
||||
count = sum(1 for k, _ in getattr(context, "multi_files", []) if k == param)
|
||||
file_name = f"genericNonCustomisableName_extra_{param}_{count}.pdf"
|
||||
writer = PdfWriter()
|
||||
writer.add_blank_page(width=72, height=72)
|
||||
with open(file_name, "wb") as f:
|
||||
writer.write(f)
|
||||
if not hasattr(context, "multi_files"):
|
||||
context.multi_files = []
|
||||
context.multi_files.append((param, open(file_name, "rb")))
|
||||
|
||||
|
||||
@given('I also use an example file at "{filePath}" as parameter "{param}"')
|
||||
def step_also_use_example_file(context, filePath, param):
|
||||
"""Add an additional file from exampleFiles under the given parameter name."""
|
||||
if not hasattr(context, "multi_files"):
|
||||
context.multi_files = []
|
||||
try:
|
||||
context.multi_files.append((param, open(filePath, "rb")))
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-PDF file generation steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('I generate a PNG image file as "{param}"')
|
||||
def step_generate_png(context, param):
|
||||
"""Generate a simple coloured PNG and register it under the given parameter name."""
|
||||
file_name = f"genericNonCustomisableName_{param}.png"
|
||||
img = Image.new("RGB", (200, 200), color=(73, 109, 137))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rectangle([50, 50, 150, 150], fill=(255, 165, 0))
|
||||
draw.ellipse([75, 75, 125, 125], fill=(255, 255, 255))
|
||||
img.save(file_name, format="PNG")
|
||||
if not hasattr(context, "files"):
|
||||
context.files = {}
|
||||
context.files[param] = open(file_name, "rb")
|
||||
context.param_name = param
|
||||
context.file_name = file_name
|
||||
|
||||
|
||||
@given('I also generate a PNG image file as "{param}"')
|
||||
def step_also_generate_png(context, param):
|
||||
"""Add an additional PNG image under the given parameter name (supports duplicate keys)."""
|
||||
count = sum(1 for k, _ in getattr(context, "multi_files", []) if k == param)
|
||||
file_name = f"genericNonCustomisableName_extra_{param}_{count}.png"
|
||||
img = Image.new("RGB", (200, 200), color=(count * 60 + 40, 100, 180))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rectangle([30, 30, 170, 170], fill=(200 - count * 30, 150, 50))
|
||||
img.save(file_name, format="PNG")
|
||||
if not hasattr(context, "multi_files"):
|
||||
context.multi_files = []
|
||||
context.multi_files.append((param, open(file_name, "rb")))
|
||||
|
||||
|
||||
@given('I generate an SVG file as "{param}"')
|
||||
def step_generate_svg(context, param):
|
||||
"""Generate a minimal SVG file and register it under the given parameter name."""
|
||||
file_name = f"genericNonCustomisableName_{param}.svg"
|
||||
svg_content = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">\n'
|
||||
' <rect width="200" height="200" fill="#4a90e2"/>\n'
|
||||
' <circle cx="100" cy="100" r="60" fill="#f5a623"/>\n'
|
||||
' <text x="100" y="107" font-size="16" text-anchor="middle" fill="white">Test SVG</text>\n'
|
||||
"</svg>\n"
|
||||
)
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
f.write(svg_content)
|
||||
if not hasattr(context, "files"):
|
||||
context.files = {}
|
||||
context.files[param] = open(file_name, "rb")
|
||||
context.param_name = param
|
||||
context.file_name = file_name
|
||||
|
||||
|
||||
@given('I also generate an SVG file as "{param}"')
|
||||
def step_also_generate_svg(context, param):
|
||||
"""Add an additional SVG under the given parameter name (supports duplicate keys)."""
|
||||
count = sum(1 for k, _ in getattr(context, "multi_files", []) if k == param)
|
||||
file_name = f"genericNonCustomisableName_extra_{param}_{count}.svg"
|
||||
svg_content = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">\n'
|
||||
f' <rect width="150" height="150" fill="hsl({count * 60}, 70%, 50%)"/>\n'
|
||||
' <polygon points="75,20 140,130 10,130" fill="white" opacity="0.6"/>\n'
|
||||
"</svg>\n"
|
||||
)
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
f.write(svg_content)
|
||||
if not hasattr(context, "multi_files"):
|
||||
context.multi_files = []
|
||||
context.multi_files.append((param, open(file_name, "rb")))
|
||||
|
||||
|
||||
@given('I generate an EML email file as "{param}"')
|
||||
def step_generate_eml(context, param):
|
||||
"""Generate a minimal RFC-2822 EML file and register it under the given parameter name."""
|
||||
file_name = f"genericNonCustomisableName_{param}.eml"
|
||||
eml_content = (
|
||||
"MIME-Version: 1.0\r\n"
|
||||
"Date: Thu, 19 Feb 2026 10:00:00 +0000\r\n"
|
||||
"Message-ID: <[email protected]>\r\n"
|
||||
"From: [email protected]\r\n"
|
||||
"To: [email protected]\r\n"
|
||||
"Subject: Test Email for PDF Conversion\r\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n"
|
||||
"\r\n"
|
||||
"This is a test email body.\r\n"
|
||||
"It contains multiple lines of text.\r\n"
|
||||
"Used for EML to PDF conversion testing.\r\n"
|
||||
)
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
f.write(eml_content)
|
||||
if not hasattr(context, "files"):
|
||||
context.files = {}
|
||||
context.files[param] = open(file_name, "rb")
|
||||
context.param_name = param
|
||||
context.file_name = file_name
|
||||
|
||||
|
||||
@given('I generate a CBZ comic archive file as "{param}"')
|
||||
def step_generate_cbz(context, param):
|
||||
"""Generate a CBZ file (ZIP of PNG images) and register it under the given parameter name."""
|
||||
file_name = f"genericNonCustomisableName_{param}.cbz"
|
||||
with zipfile.ZipFile(file_name, "w") as cbz:
|
||||
for i in range(3):
|
||||
img = Image.new("RGB", (200, 300), color=(i * 60 + 40, 120, 200 - i * 50))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rectangle([20, 20, 180, 280], outline=(0, 0, 0), width=3)
|
||||
draw.rectangle([40, 40, 160, 100], fill=(200, 200, 255))
|
||||
img_bytes = io.BytesIO()
|
||||
img.save(img_bytes, format="PNG")
|
||||
cbz.writestr(f"page_{i + 1:03d}.png", img_bytes.getvalue())
|
||||
if not hasattr(context, "files"):
|
||||
context.files = {}
|
||||
context.files[param] = open(file_name, "rb")
|
||||
context.param_name = param
|
||||
context.file_name = file_name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PDF modification steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the pdf has form fields")
|
||||
def step_pdf_has_form_fields(context):
|
||||
"""Create a PDF with a basic AcroForm text field on each page."""
|
||||
reader = PdfReader(context.file_name)
|
||||
page_count = len(reader.pages)
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=letter)
|
||||
w, h = letter
|
||||
for i in range(page_count):
|
||||
c.acroForm.textfield(
|
||||
name=f"field_{i + 1}",
|
||||
tooltip=f"Field {i + 1}",
|
||||
x=72,
|
||||
y=h - 72,
|
||||
width=200,
|
||||
height=20,
|
||||
forceBorder=True,
|
||||
)
|
||||
c.showPage()
|
||||
c.save()
|
||||
with open(context.file_name, "wb") as f:
|
||||
f.write(buffer.getvalue())
|
||||
context.files[context.param_name].close()
|
||||
context.files[context.param_name] = open(context.file_name, "rb")
|
||||
|
||||
|
||||
@given('the pdf has an attachment named "{attachment_name}"')
|
||||
def step_pdf_has_attachment(context, attachment_name):
|
||||
"""Embed a small text attachment into the current PDF."""
|
||||
reader = PdfReader(context.file_name)
|
||||
writer = PdfWriter()
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
attachment_bytes = (
|
||||
f"Attachment: {attachment_name}\nThis is test attachment content.".encode("utf-8")
|
||||
)
|
||||
writer.add_attachment(attachment_name, attachment_bytes)
|
||||
with open(context.file_name, "wb") as f:
|
||||
writer.write(f)
|
||||
context.files[context.param_name].close()
|
||||
context.files[context.param_name] = open(context.file_name, "rb")
|
||||
|
||||
|
||||
@given("the pdf has bookmarks")
|
||||
def step_pdf_has_bookmarks(context):
|
||||
"""Add one top-level outline/bookmark entry per page to the current PDF."""
|
||||
reader = PdfReader(context.file_name)
|
||||
writer = PdfWriter()
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
for i in range(len(reader.pages)):
|
||||
writer.add_outline_item(f"Chapter {i + 1}", i)
|
||||
with open(context.file_name, "wb") as f:
|
||||
writer.write(f)
|
||||
context.files[context.param_name].close()
|
||||
context.files[context.param_name] = open(context.file_name, "rb")
|
||||
|
||||
|
||||
@given("the pdf has a Stirling-PDF QR code split marker on page {page_num:d}")
|
||||
def step_pdf_has_qr_split_marker(context, page_num):
|
||||
"""Replace page page_num (1-indexed) with a page containing a Stirling-PDF QR code."""
|
||||
try:
|
||||
import qrcode as _qrcode
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"qrcode package is required for this step. "
|
||||
"Install with: pip install 'qrcode[pil]'"
|
||||
)
|
||||
reader = PdfReader(context.file_name)
|
||||
qr = _qrcode.QRCode(box_size=4, border=2)
|
||||
qr.add_data("https://github.com/Stirling-Tools/Stirling-PDF")
|
||||
qr.make(fit=True)
|
||||
qr_img = qr.make_image(fill_color="black", back_color="white")
|
||||
qr_bytes = io.BytesIO()
|
||||
qr_img.save(qr_bytes, format="PNG")
|
||||
qr_bytes.seek(0)
|
||||
writer = PdfWriter()
|
||||
for i, page in enumerate(reader.pages):
|
||||
if i + 1 == page_num:
|
||||
packet = io.BytesIO()
|
||||
can = canvas.Canvas(packet, pagesize=letter)
|
||||
w, h = letter
|
||||
can.drawImage(
|
||||
ImageReader(qr_bytes), (w - 100) / 2, (h - 100) / 2, width=100, height=100
|
||||
)
|
||||
can.showPage()
|
||||
can.save()
|
||||
packet.seek(0)
|
||||
qr_pdf = PdfReader(packet)
|
||||
writer.add_page(qr_pdf.pages[0])
|
||||
else:
|
||||
writer.add_page(page)
|
||||
with open(context.file_name, "wb") as f:
|
||||
writer.write(f)
|
||||
context.files[context.param_name].close()
|
||||
context.files[context.param_name] = open(context.file_name, "rb")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON multipart part steps (@RequestPart endpoints like /form/fill)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('the request includes a JSON part "{part_name}" with content "{json_content}"')
|
||||
def step_request_json_part(context, part_name, json_content):
|
||||
"""Register a JSON multipart part (sent with Content-Type: application/json)."""
|
||||
if not hasattr(context, "json_parts"):
|
||||
context.json_parts = {}
|
||||
context.json_parts[part_name] = json_content
|
||||
|
||||
|
||||
########
|
||||
# WHEN #
|
||||
########
|
||||
@@ -337,6 +605,17 @@ def step_send_api_request(context, endpoint):
|
||||
print(f"form_data {file.name} with {mime_type}")
|
||||
form_data.append((key, (file.name, file, mime_type)))
|
||||
|
||||
# Multi-file entries (duplicate keys for MultipartFile[] endpoints, e.g. merge-pdfs)
|
||||
for key, file in getattr(context, "multi_files", []):
|
||||
mime_type, _ = mimetypes.guess_type(file.name)
|
||||
mime_type = mime_type or "application/octet-stream"
|
||||
print(f"form_data (multi) {file.name} with {mime_type}")
|
||||
form_data.append((key, (file.name, file, mime_type)))
|
||||
|
||||
# JSON multipart parts for @RequestPart endpoints (e.g. /form/fill)
|
||||
for part_name, json_content in getattr(context, "json_parts", {}).items():
|
||||
form_data.append((part_name, (None, json_content, "application/json")))
|
||||
|
||||
# Set timeout to 300 seconds (5 minutes) to prevent infinite hangs
|
||||
print(f"Sending POST request to {endpoint} with timeout=300s")
|
||||
response = requests.post(url, files=form_data, headers=API_HEADERS, timeout=300)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
@jwt @auth @user_mgmt
|
||||
Feature: User Management API
|
||||
|
||||
Tests for the user management REST API, covering API key operations,
|
||||
password changes, and admin-level user CRUD (create, role change,
|
||||
enable/disable, delete, force password change).
|
||||
|
||||
Admin credentials: username=admin, password=stirling
|
||||
Global API key: 123456789
|
||||
|
||||
Each admin CRUD scenario creates and then deletes the test user within
|
||||
the same scenario to avoid hitting the licence user limit.
|
||||
|
||||
# =========================================================================
|
||||
# API KEY OPERATIONS
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Authenticated user can retrieve their current API key
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/get-api-key" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response body should not be empty
|
||||
|
||||
@positive
|
||||
Scenario: Authenticated user can update their API key
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/update-api-key" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
And the response body should not be empty
|
||||
|
||||
@negative
|
||||
Scenario: Get API key without authentication returns 401
|
||||
When I send a POST request to "/api/v1/user/get-api-key" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
@negative
|
||||
Scenario: Update API key without authentication returns 401
|
||||
When I send a POST request to "/api/v1/user/update-api-key" with no authentication
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# PASSWORD CHANGE
|
||||
# =========================================================================
|
||||
|
||||
@positive
|
||||
Scenario: Admin can change their own password and revert it
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/change-password" with JWT authentication and params "currentPassword=stirling&newPassword=stirling_temp_bdd"
|
||||
Then the response status code should be 200
|
||||
# Revert to original password so other tests are not broken
|
||||
When I send a POST request to "/api/v1/user/change-password" with JWT authentication and params "currentPassword=stirling_temp_bdd&newPassword=stirling"
|
||||
Then the response status code should be 200
|
||||
|
||||
@negative
|
||||
Scenario: Change password with wrong current password returns 401
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/change-password" with JWT authentication and params "currentPassword=completely_wrong_pass_xyz&newPassword=stirling2"
|
||||
Then the response status code should be one of "400, 401"
|
||||
|
||||
@negative
|
||||
Scenario: Change password without authentication returns 401
|
||||
When I send a POST request to "/api/v1/user/change-password" with no authentication and params "currentPassword=stirling&newPassword=stirling2"
|
||||
Then the response status code should be 401
|
||||
|
||||
# =========================================================================
|
||||
# ADMIN USER CRUD
|
||||
# Each scenario is self-contained: creates the test user, runs the
|
||||
# operation under test, then deletes the user to free the licence slot.
|
||||
# =========================================================================
|
||||
|
||||
@admin @positive
|
||||
Scenario: Admin can create and delete a user account
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/admin/saveUser" with JWT authentication and params "username=bdd_mgmt_test_user&password=TestPass123!&role=ROLE_USER&authType=web&forceChange=false"
|
||||
Then the response status code should be one of "200, 201"
|
||||
# Clean up immediately so the licence slot is freed for other scenarios
|
||||
When I send a POST request to "/api/v1/user/admin/deleteUser/bdd_mgmt_test_user" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@admin @positive
|
||||
Scenario: Admin can enable and disable a user account
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/admin/saveUser" with JWT authentication and params "username=bdd_mgmt_test_user&password=TestPass123!&role=ROLE_USER&authType=web&forceChange=false"
|
||||
Then the response status code should be one of "200, 201"
|
||||
When I send a POST request to "/api/v1/user/admin/changeUserEnabled/bdd_mgmt_test_user" with JWT authentication and params "enabled=true"
|
||||
Then the response status code should be 200
|
||||
When I send a POST request to "/api/v1/user/admin/changeUserEnabled/bdd_mgmt_test_user" with JWT authentication and params "enabled=false"
|
||||
Then the response status code should be 200
|
||||
# Clean up
|
||||
When I send a POST request to "/api/v1/user/admin/deleteUser/bdd_mgmt_test_user" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@admin @positive
|
||||
Scenario: Admin can change a user's role
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/admin/saveUser" with JWT authentication and params "username=bdd_mgmt_test_user&password=TestPass123!&role=ROLE_USER&authType=web&forceChange=false"
|
||||
Then the response status code should be one of "200, 201"
|
||||
When I send a POST request to "/api/v1/user/admin/changeRole" with JWT authentication and params "username=bdd_mgmt_test_user&role=ROLE_USER"
|
||||
Then the response status code should be 200
|
||||
# Clean up
|
||||
When I send a POST request to "/api/v1/user/admin/deleteUser/bdd_mgmt_test_user" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@admin @positive
|
||||
Scenario: Admin can force-change a user's password
|
||||
Given I am logged in as admin
|
||||
When I send a POST request to "/api/v1/user/admin/saveUser" with JWT authentication and params "username=bdd_mgmt_test_user&password=TestPass123!&role=ROLE_USER&authType=web&forceChange=false"
|
||||
Then the response status code should be one of "200, 201"
|
||||
When I send a POST request to "/api/v1/user/admin/changePasswordForUser" with JWT authentication and params "username=bdd_mgmt_test_user&newPassword=NewTestPass456!"
|
||||
Then the response status code should be 200
|
||||
# Clean up
|
||||
When I send a POST request to "/api/v1/user/admin/deleteUser/bdd_mgmt_test_user" with JWT authentication
|
||||
Then the response status code should be 200
|
||||
|
||||
@admin @negative
|
||||
Scenario: Non-admin cannot save a user via admin endpoint (returns 401 or 403)
|
||||
When I send a POST request to "/api/v1/user/admin/saveUser" with no authentication and params "username=evil_user&password=pass&role=ROLE_ADMIN&authType=web&forceChange=false"
|
||||
Then the response status code should be one of "401, 403"
|
||||
|
||||
@admin @negative
|
||||
Scenario: Non-admin cannot delete a user via admin endpoint (returns 401 or 403)
|
||||
When I send a POST request to "/api/v1/user/admin/deleteUser/admin" with no authentication
|
||||
Then the response status code should be one of "401, 403"
|
||||
@@ -1,5 +1,7 @@
|
||||
behave
|
||||
behave-html-formatter
|
||||
requests
|
||||
pypdf
|
||||
reportlab
|
||||
PyCryptodome
|
||||
qrcode[pil]
|
||||
|
||||
@@ -295,6 +295,10 @@ pypdf==6.6.2 \
|
||||
--hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \
|
||||
--hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba
|
||||
# via -r testing/cucumber/requirements.in
|
||||
qrcode==8.0 \
|
||||
--hash=sha256:9fc05f03305ad27a709eb742cf3097fa19e6f6f93bb9e2f039c0979190f6f1b1 \
|
||||
--hash=sha256:025ce2b150f7fe4296d116ee9bad455a6643ab4f6e7dce541613a4758cbce347
|
||||
# via -r testing/cucumber/requirements.in
|
||||
reportlab==4.4.9 \
|
||||
--hash=sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd \
|
||||
--hash=sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378
|
||||
|
||||
+9
-1
@@ -108,6 +108,7 @@ capture_file_list() {
|
||||
-not -path '*/tmp/stirling-pdf/jetty-*/*' \
|
||||
-not -path '*/tmp/stirling-pdf/lu*' \
|
||||
-not -path '*/tmp/stirling-pdf/tmp*' \
|
||||
-not -path '*/tmp/stirling-pdf/stirling-pdf-*.pdf' \
|
||||
2>/dev/null | xargs -I{} sh -c 'stat -c \"%n %s %Y\" \"{}\" 2>/dev/null || true' | sort" > "$output_file"
|
||||
|
||||
# Check if the output file has content
|
||||
@@ -131,6 +132,7 @@ capture_file_list() {
|
||||
-not -path '*/tmp/stirling-pdf/jetty-*/*' \
|
||||
-not -path '*/tmp/lu*' \
|
||||
-not -path '*/tmp/tmp*' \
|
||||
-not -path '*/tmp/stirling-pdf/stirling-pdf-*.pdf' \
|
||||
2>/dev/null | sort" > "$output_file"
|
||||
|
||||
if [ ! -s "$output_file" ]; then
|
||||
@@ -539,8 +541,14 @@ main() {
|
||||
|
||||
capture_file_list "$CONTAINER_NAME" "$BEFORE_FILE"
|
||||
|
||||
CUCUMBER_REPORT="$PROJECT_ROOT/testing/cucumber/report.html"
|
||||
CUCUMBER_JUNIT_DIR="$PROJECT_ROOT/testing/cucumber/junit"
|
||||
mkdir -p "$CUCUMBER_JUNIT_DIR"
|
||||
cd "testing/cucumber"
|
||||
if python -m behave; then
|
||||
if python -m behave \
|
||||
-f behave_html_formatter:HTMLFormatter -o "$CUCUMBER_REPORT" \
|
||||
-f pretty \
|
||||
--junit --junit-directory "$CUCUMBER_JUNIT_DIR"; then
|
||||
echo "Waiting 5 seconds for any file operations to complete..."
|
||||
sleep 5
|
||||
|
||||
|
||||
Reference in New Issue
Block a user