Skip to content
Portrait of Pelin Dragos

Pelin Dragos

Quality assurance · GRC

Test cycle · API · Executed · 14–17 October 2026

API testing of the Toolshop

4 high-severity authorisation defects in a teaching e-commerce API, found in a full, reproducible run.

I wrote and ran a Postman and Newman suite against the Toolshop API, from account setup to a database reset. The run is reproducible from a clean database. It found defects in authorisation and input validation; in the worst of them, customers can change other customers’ invoices and messages, and anyone can change a product’s price.

requests
355
in one Newman run, 2 min 40 s
assertions
582
35 failed, each explained
defects
19
4 of them high severity
test cases
118
written before the run, in 17 folders
Application
Practice Software Testing (Toolshop), sprint5
Commit
c752a70
API version
5.0
Environment
Local Docker · localhost:8091
Tools
Postman, Newman
Period
14–17 October 2026
On this page

Key findings

The high-severity defects; each links to its full report

  1. HighDEF-011Any logged-in user can change the status of another user's invoiceAnyone with an account can falsify order and billing data for other customers.Steps, request and response
  2. HighDEF-012Any logged-in user can change the status of another user's contact messageSupport can lose track of real requests. The content of the messages is not exposed.Steps, request and response
  3. HighDEF-014Any logged-in user can download another customer's invoice PDFThe name, address, products and total of any customer can be downloaded by anyone with an account.Steps, request and response
  4. HighDEF-017Anyone (even without a token) can create products and change their priceAnyone can change catalogue prices or add false products.Steps, request and response

Results of the final run

355 requests · 582 assertions · 35 failed · 2 min 40 s

Failed assertions per folder · 35 in total, across 10 of 17 folders
  • 03 Registration103 Registration1 failed assertions · 26 requestsDefects: DEF-003DEF-003
  • 06 Cart and payment406 Cart and payment4 failed assertions · 55 requestsDefects: DEF-009DEF-009
  • 07 Orders and invoices607 Orders and invoices6 failed assertions · 32 requestsDefects: DEF-010, DEF-011, DEF-013DEF-010DEF-011DEF-013
  • 09 Messages309 Messages3 failed assertions · 21 requestsDefects: DEF-012DEF-012
  • 10 Data isolation610 Data isolation6 failed assertions · 27 requestsDefects: DEF-015, DEF-016DEF-015DEF-016
  • 11 Administration811 Administration8 failed assertions · 29 requestsDefects: DEF-017, DEF-018DEF-017DEF-018
  • 13 Dedicated users313 Dedicated users3 failed assertions · 23 requestsDefects: DEF-005, DEF-006DEF-005DEF-006
  • 14 Contract114 Contract1 failed assertions · 21 requestsDefects: DEF-007DEF-007
  • 15 Time dependent215 Time dependent2 failed assertions · 1 requests
  • 16 Database reset116 Database reset1 failed assertions · 2 requestsDefects: DEF-019DEF-019

No failures: 00 Setup01 Environment02 Catalog04 Authentication05 Profile08 Favorites12 Reports

Show the data as a table
Requests and failed assertions in each folder of the final run
FolderRequestsFailed assertionsDefects shown
00 Setup130—
01 Environment10—
02 Catalog340—
03 Registration261DEF-003
04 Authentication270—
05 Profile60—
06 Cart and payment554DEF-009
07 Orders and invoices326DEF-010, DEF-011, DEF-013
08 Favorites130—
09 Messages213DEF-012
10 Data isolation276DEF-015, DEF-016
11 Administration298DEF-017, DEF-018
12 Reports240—
13 Dedicated users233DEF-005, DEF-006
14 Contract211DEF-007
15 Time dependent12—
16 Database reset21DEF-019

Scope

What the run covers, and what it does not

In scope

  • Authentication, sessions, password change and reset
  • Registration and profile
  • Catalogue: search, filters, product details, invalid input
  • Cart, checkout and payment
  • Orders and invoices, including the invoice PDF
  • Favourites and contact messages
  • Data isolation between two customers and a visitor
  • Administration and roles
  • Reports, validation and error handling
  • Database reset and rate limiting

Out of scope

  • The user interface, end to end
  • SQL and database checks
  • Accessibility
  • Performance and load
  • Social login
  • The server-sent events endpoint
  • The deliberately vulnerable variant of the application

118 test cases were planned across 12 areas (A–L) and built as 17 folders of requests.

Method

From plan to the final run

  1. PlanAreas, risks and the order of the run, with the evidence each test needs.
  2. Write the casesEach case states why it matters, the expected result and the pass and fail criteria, before it is run.
  3. Build the collectionOne Postman request per check, with assertions and the data set up in the first folder.
  4. Run folder by folderTokens expire in five minutes, so each folder starts by refreshing them.
  5. Keep the evidenceResponses and test results saved per test, with tokens and secrets masked.
  6. Report the defectsSteps, expected and observed results, impact, probable cause and a fix, for each one.
  7. Run it all againOne full Newman run from a reset database; every failure explained by a defect or a stated limit.

Defect register

Severity is my decision, based on impact. Open a row for the full report.

Showing 19 of 19 defects

  1. DEF-001MediumProduct search misses products when a 4+ character term appears inside a word

    Product search gives different results depending on the length of the term. The short term “ham” finds Sledgehammer; the longer, more precise term “hammer” does not.

    Evidence: Found by comparing searches. The suite records the results but does not assert on this difference; the request below shows the “ham” search.

    Expected
    Both searches return every product whose name contains the term, Sledgehammer included.
    Observed
    “ham” returns 7 products including Sledgehammer. “hammer” and “HAMMER” return 6, without it.
    Impact
    A customer searching for the most natural term does not see a product that exists in the catalogue. No security or data impact.
    Probable cause
    Terms under 4 characters use a substring match. Longer terms use a full-text match, which only matches the start of a word.
    Suggested fix
    Use one matching strategy for every term, or a full-text index that matches inside words.
    Linked tests · Retest
    T-C05, T-C07 · not yet retested

    From the final run

    T-C07 Edge-case search · partial

    Request

    GET /products/search?q=ham
    Accept: application/json

    Response 200 OK · 144 ms

    {
      "current_page": 1,
      "data": [
        {
          "id": "01M552JH2F4ZF8Y8F7T1EBMA3H",
          "name": "Claw Hammer with Shock Reduction Grip",
          "description": "Ergonomic claw hammer featuring an advanced shock reduction grip system that absorbs up to 70% of impact vibrations, protecting your wrist, elbow, and shoulder …",
          "price": 13.41,
          "is_location_offer": true,
          "is_rental": false,
          "co2_rating": "D",
          "in_stock": true,
          "is_eco_friendly": false,
          "product_image": {
            "id": "01M552JH1JNQZMARG3VDF4TG7Q",
            "by_name": "iMattSmart",
            "by_url": "https://unsplash.com/@imattsmart",
            "source_name": "Unsplash",
            "source_url": "https://unsplash.com/photos/jaLaLQdkBOE",
            "file_name": "hammer01.avif",
            "title": "Claw Hammer"
          },
          "category": {
            "id": "01M552JH0YW2D66AXDEDJGQGW9",
            "name": "Hammer"
          },
          "brand": {
            "id": "01M552JGRRJK0308ZCZCH32JAH",
            "name": "ForgeFlex Tools"
          }
        },
        {
          "id": "01M552JH2GD6Q6AVJJSHVS7GKD",
          "name": "Hammer",
          "description": "A dependable standard claw hammer suitable for driving and removing nails in everyday construction, carpentry, and home improvement projects. The 350g carbon st…",
          "price": 12.58,
          "is_location_offer": false,
          "is_rental": false,
          "co2_rating": "D",
          "in_stock": true,
    …
    • PassNo server error (5xx)
    • PassDoes not return the whole catalog
  2. DEF-002LowPostcode lookup returns different localities for the same postcode, depending on spacing (to be confirmed)

    The same Dutch postcode returns different localities depending on the space: “1011AB” and “1011 AB” resolve to different towns.

    Evidence: Closed as a limitation of the test postcode driver, still marked to be confirmed. The setup uses one fixed format, so the final run does not exercise it.

    Expected
    The same postcode returns the same locality, regardless of spacing or case.
    Observed
    “1011AB” returns one town, “1011 AB” another. Upper and lower case give the same result.
    Impact
    Orders sent with one spelling and the city of the other are rejected with 422. In the web checkout the code is sent as typed, so the error is rare.
    Probable cause
    The lookup driver seeds the locality from the postcode as received, without normalising it.
    Suggested fix
    Normalise the postcode (remove spaces, upper case) before the lookup.
    Linked tests · Retest
    T-E01, T-E03 · not yet retested
  3. DEF-003MediumRegistration accepts an email address without a valid format

    Registration accepts an email without a valid format (no @, no domain) and creates the account.

    Expected
    422 with an error on the email field.
    Observed
    201. The account is created and receives an ID.
    Impact
    The user cannot receive a password reset, so the account is effectively locked. Contact data in the database is invalid.
    Probable cause
    The registration rules check that the email is present, unique and short enough, but not its format.
    Suggested fix
    Add the email format rule, as the contact form already has.
    Linked tests · Retest
    T-B04 · not yet retested

    From the final run

    T-B04 Register invalid email

    Request

    POST /users/register
    Accept: application/json
    Content-Type: application/json
    
    {
      "first_name": "QA",
      "last_name": "Tester",
      "address": {
        "street": "Teststraat 1",
        "city": "Amsterdam",
        "state": "Noord-Holland",
        "country": "NL",
        "postal_code": "1011 AB"
      },
      "phone": "0612345678",
      "dob": "1990-01-01",
      "email": "qa-fara-arond-1792245587396",
      "password": "Qa-Test!2026#1792245614972"
    }

    Response 201 Created · 234 ms

    {
      "first_name": "QA",
      "last_name": "Tester",
      "phone": "0612345678",
      "dob": "1990-01-01",
      "email": "qa-fara-arond-1792245587396",
      "id": "01m552kjkvc18nkd0f3z14bxqh",
      "created_at": "2026-10-17 14:00:15",
      "address": {
        "street": "Teststraat 1",
        "house_number": null,
        "city": "Amsterdam",
        "state": "Noord-Holland",
        "country": "NL",
        "postal_code": "1011 AB"
      }
    }
    • FailStatus 422 — expected response to have status code 422 but got 201
    • PassValidation error on email
  4. DEF-004MediumChange-password validation returns a redirect (302) instead of 422 for clients that do not send Accept: application/json

    Password-change validation responds with a 302 redirect instead of 422 when the client does not ask for JSON. Postman follows the redirect and shows 404.

    Evidence: Seen in the earlier Postman runs. In the final run the collection sends Accept: application/json, so this test returns 422 and passes.

    Expected
    422 with the validation errors, whatever the Accept header.
    Observed
    With Accept: */*: 302 to the home page, then 404. With Accept: application/json: 422 with the errors.
    Impact
    Clients that do not send the header never see validation errors. The same cause produced most early failures in cart and payment tests.
    Probable cause
    Validation runs inline in the controller, using the framework default instead of the JSON error handler used elsewhere.
    Suggested fix
    Move the validation into a request class that always returns JSON errors.
    Linked tests · Retest
    T-A14 · not yet retested
  5. DEF-005MediumForgot-password reveals whether an email has an account

    Forgot-password answers differently for an existing and an unknown email, which reveals who has an account.

    Expected
    The same status and a neutral message in both cases.
    Observed
    Existing email: 200. Unknown email: 422 “The selected email is invalid”.
    Impact
    Anyone can check which addresses are registered and target them. No passwords are exposed.
    Probable cause
    The validation rule that the email must exist is the mechanism that gives the answer away.
    Suggested fix
    Always answer 200 with a neutral message, and send the reset email only if the account exists.
    Linked tests · Retest
    T-A11 · not yet retested

    From the final run

    T-A11 · 2 existing email

    Request

    POST /users/forgot-password
    Accept: application/json
    Content-Type: application/json
    
    {
      "email": "qa+fp-1792245718882@test.local"
    }

    Response 200 OK · 161 ms

    {
      "success": true
    }
    • PassNo server error (5xx)

    T-A11 · 3 unknown email

    Request

    POST /users/forgot-password
    Accept: application/json
    Content-Type: application/json
    
    {
      "email": "nu-exista-1792245587396@test.local"
    }

    Response 422 Unprocessable Content · 128 ms

    {
      "message": "The selected email is invalid.",
      "errors": {
        "email": [
          "The selected email is invalid."
        ]
      }
    }
    • PassNo server error (5xx)
    • FailSame status as for the existing email — expected 422 to deeply equal 200
    • FailSame message as for the existing email — expected '{"message":"The selected email is inv…' to deeply equal '{"success":true}'
  6. DEF-006Low/totp/login/totp route points to a method that does not exist (500)

    The route /totp/login/totp points to a controller method that does not exist. Every call returns 500.

    Expected
    The same behaviour as the TOTP step of /users/login, or no route at all.
    Observed
    500 with the framework error and a stack trace.
    Impact
    A dead route that fails with a server error. The working TOTP login goes through /users/login.
    Probable cause
    The route is declared, but the method was removed or never written.
    Suggested fix
    Remove the route, or implement it by calling the same login logic. Return JSON errors either way.
    Linked tests · Retest
    T-A15 · not yet retested

    From the final run

    T-A15 · 9 route /totp/login/totp

    Request

    POST /totp/login/totp
    Authorization: Bearer …[masked]
    Accept: application/json
    Content-Type: application/json
    
    {
      "access_token": "eyJ…[masked]…",
      "totp": "251063"
    }

    Response 500 Internal Server Error · 130 ms

    {
      "message": "Method App\\Http\\Controllers\\TOTPController::loginWithTOTP does not exist.",
      "exception": "BadMethodCallException",
      "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
      "line": 68,
      "trace": [
        {
          "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
          "line": 54,
          "function": "__call",
          "class": "Illuminate\\Routing\\Controller",
          "type": "->"
        },
        {
          "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
          "line": 43,
          "function": "callAction",
          "class": "Illuminate\\Routing\\Controller",
          "type": "->"
        },
        "… 28 more items"
      ]
    }
    • FailNo server error (5xx) — expected 500 to be below 500
  7. DEF-007MediumProduct list with an unknown sort column returns 500

    Sorting the product list by an unknown column returns 500.

    Expected
    422, or the default sort. An unknown sort column must not reach the database.
    Observed
    500 “Something went wrong”. The server log shows an SQL error for the unknown column.
    Impact
    Anyone can trigger server errors with arbitrary parameters, which also pollutes monitoring.
    Probable cause
    The sort parameter becomes the ORDER BY clause without a list of allowed columns.
    Suggested fix
    Allow only price and name, ascending or descending; reject anything else.
    Linked tests · Retest
    T-K06 · not yet retested

    From the final run

    T-K06 Invalid query parameters · unknown sort

    Request

    GET /products?sort=nu_exista,asc
    Accept: application/json

    Response 500 Internal Server Error · 124 ms

    {
      "message": "Something went wrong"
    }
    • FailNo server error (5xx) — expected 500 to be below 500
    • PassNo internal details in response
  8. DEF-008MediumServer errors expose the full stack trace (APP_DEBUG=true) and a page beyond the range takes 30+ seconds

    A very high page number returned 500 after 33–38 seconds, with an 887 KB HTML page containing the full stack trace and server paths.

    Evidence: Seen in the earlier runs. In the final run the same request returned 200, which points to the cache write in the test environment. The debug-mode exposure stands.

    Expected
    An empty list or 404, without a stack trace.
    Observed
    500 with file paths and the failing cache write in the body. Debug mode is on.
    Impact
    The structure of the server is exposed, and a slow request can tie up resources.
    Probable cause
    Debug mode is enabled, and a cache write failed in the container, possibly because of file permissions in the test environment.
    Suggested fix
    Turn debug mode off outside local development, fix the cache permissions, and return 404 for missing pages.
    Linked tests · Retest
    T-K06, T-K09 · not yet retested
  9. DEF-009MediumPayment check accepts an unknown payment method

    The payment check accepts an unknown payment method, and an empty body, as a successful payment.

    Expected
    422 with an error on payment_method.
    Observed
    200 “Payment was successful”.
    Impact
    Checkout can be “paid” with a method that does not exist.
    Probable cause
    Validation runs only inside branches for the known methods; there is no branch for anything else.
    Suggested fix
    Make payment_method required and restricted to the known values, before anything else.
    Linked tests · Retest
    T-E10 · not yet retested

    From the final run

    T-E10 Unknown payment method · bitcoin

    Request

    POST /payment/check
    Accept: application/json
    Content-Type: application/json
    
    {
      "payment_method": "bitcoin",
      "payment_details": {}
    }

    Response 200 OK · 101 ms

    {
      "message": "Payment was successful"
    }
    • FailStatus 422 — expected response to have status code 422 but got 200
    • FailDoes not confirm payment — expected '{"message":"Payment was successful"}' to not include 'Payment was successful'
  10. DEF-010MediumAn empty cart can be turned into an invoice (201)

    An order can be created from an empty cart.

    Expected
    422 or 404: no order without products.
    Observed
    201 with an invoice number, a total of 0 and no product lines.
    Impact
    Empty orders appear in the customer's invoices and in the sales reports.
    Probable cause
    The order request checks that a cart ID is present, not that the cart exists or has products.
    Suggested fix
    Check that the cart exists and has at least one product.
    Linked tests · Retest
    T-E09 · not yet retested

    From the final run

    T-E09 · 3 empty cart

    Request

    POST /invoices
    Authorization: Bearer …[masked]
    Accept: application/json
    Content-Type: application/json
    
    {
      "billing_street": "Teststraat 1",
      "billing_city": "Wezuperbrug",
      "billing_state": "Drenthe",
      "billing_country": "NL",
      "billing_postal_code": "1011 AB",
      "payment_method": "cash-on-delivery",
      "payment_details": {},
      "cart_id": "01m552n1g4xw4mknqkgas61k4a"
    }

    Response 201 Created · 257 ms

    {
      "billing_street": "Teststraat 1",
      "billing_city": "Wezuperbrug",
      "billing_state": "Drenthe",
      "billing_country": "NL",
      "billing_postal_code": "1011 AB",
      "user_id": "01m552jz1by17vr3sx9zcb1275",
      "invoice_date": "2026-10-17T14:01:03.658890Z",
      "invoice_number": "INV-2026000009",
      "id": "01m552n1xetfjdmzm1xg896wts",
      "created_at": "2026-10-17T14:01:03.000000Z",
      "subtotal": 0,
      "total": 0,
      "additional_discount_percentage": null,
      "additional_discount_amount": 0,
      "eco_discount_percentage": 0,
      "eco_discount_amount": 0
    }
    • FailStatus in [404, 422] — expected 201 to be one of [ 404, 422 ]
    • PassNo internal details in response
  11. DEF-011HighAny logged-in user can change the status of another user's invoice

    The endpoint that changes an invoice's status checks neither the role nor the owner. A customer can mark another customer's order as completed.

    Steps to reproduce

    1. Customer A places an order and receives the invoice ID.
    2. Customer B, with a different account, sends a status change for that invoice.
    3. The invoice is read back as an admin.
    Expected
    The change is refused with 403 or 404, and the status stays the same.
    Observed
    200, and the status becomes COMPLETED.
    Impact
    Anyone with an account can falsify order and billing data for other customers.
    Probable cause
    The status update filters neither by owner nor by role. The other invoice actions do filter by owner; this route is the exception.
    Suggested fix
    Restrict the route to the admin role. If customers may cancel orders, allow only that, and only on their own.
    Linked tests · Retest
    T-F04 · not yet retested

    From the final run

    T-F04 · 1 user B

    Request

    PUT /invoices/01m552msn8pxbeh77194dfb5kb/status
    Authorization: Bearer …[masked]
    Accept: application/json
    Content-Type: application/json
    
    {
      "status": "COMPLETED"
    }

    Response 200 OK · 161 ms

    {
      "success": true
    }
    • FailStatus 403 — expected response to have status code 403 but got 200

    T-F04 · 3 check as admin

    Request

    GET /invoices/01m552msn8pxbeh77194dfb5kb
    Authorization: Bearer …[masked]
    Accept: application/json

    Response 200 OK · 130 ms

    {
      "id": "01m552msn8pxbeh77194dfb5kb",
      "invoice_date": "2026-10-17 14:00:55",
      "additional_discount_percentage": null,
      "additional_discount_amount": 0,
      "eco_discount_percentage": 0,
      "eco_discount_amount": 0,
      "invoice_number": "INV-2026000006",
      "billing_street": "Teststraat 1",
      "billing_city": "Wezuperbrug",
      "billing_state": "Drenthe",
      "billing_country": "NL",
      "billing_postal_code": "1011 AB",
      "subtotal": 1400.85,
      "total": 1400.85,
      "status": "COMPLETED",
      "status_message": "Order shipped",
      "created_at": "2026-10-17T14:00:55.000000Z",
      "user_id": "01m552jz1by17vr3sx9zcb1275",
      "invoicelines": [
        {
          "id": "01m552mt5prtepky39mtmwjhch",
          "invoice_id": "01m552msn8pxbeh77194dfb5kb",
          "product_id": "01M552JH28NG35XD8J04PGAJXN",
          "unit_price": 14.15,
          "quantity": 99,
          "discount_percentage": null,
          "discounted_price": null,
          "product": {
            "id": "01M552JH28NG35XD8J04PGAJXN",
            "name": "Combination Pliers",
            "description": "Versatile combination pliers designed for gripping, bending, and cutting wire with ease. Featuring chrome vanadium steel construction with induction-hardened cu…",
            "price": 14.15,
            "co2_rating": "D",
            "is_rental": false,
            "in_stock": null,
            "is_eco_friendly": false,
            "product_image": {
              "id": "01M552JH1JNQZMARG3VDF4TG7J",
              "by_name": "Helinton Fantin",
    …
    • PassStatus 200
    • FailStatus is not COMPLETED — expected 'COMPLETED' to not deeply equal 'COMPLETED'
  12. DEF-012HighAny logged-in user can change the status of another user's contact message

    The same flaw as DEF-011, on contact messages: a customer can change the status of another customer's message.

    Steps to reproduce

    1. Customer A sends a contact message.
    2. Customer B sends a status change for that message.
    3. The message is read back as an admin.
    Expected
    The change is refused with 403 or 404, and the status stays the same.
    Observed
    200, and the status changes.
    Impact
    Support can lose track of real requests. The content of the messages is not exposed.
    Probable cause
    The status route checks the allowed values only, with no role or owner check.
    Suggested fix
    Restrict the route to the admin role, as the reports already are.
    Linked tests · Retest
    T-H07 · not yet retested

    From the final run

    T-H07 · 1 B changes status

    Request

    PUT /messages/01m552nbn2vxb5meqh0gmw7ab2/status
    Authorization: Bearer …[masked]
    Accept: application/json
    Content-Type: application/json
    
    {
      "status": "NEW"
    }

    Response 200 OK · 121 ms

    {
      "success": true
    }
    • FailStatus in [403, 404] — expected 200 to be one of [ 403, 404 ]

    T-H07 · 2 check as admin

    Request

    GET /messages/01m552nbn2vxb5meqh0gmw7ab2
    Authorization: Bearer …[masked]
    Accept: application/json

    Response 200 OK · 126 ms

    {
      "id": "01m552nbn2vxb5meqh0gmw7ab2",
      "user_id": "01m552jz1by17vr3sx9zcb1275",
      "name": null,
      "email": null,
      "subject": "customer-service",
      "message": "Test message sent by user A from Postman.",
      "status": "NEW",
      "created_at": "2026-10-17 14:01:13",
      "user": {
        "id": "01m552jz1by17vr3sx9zcb1275",
        "provider": null,
        "first_name": "QA-Modificat",
        "last_name": "Tester",
        "phone": "0612345678",
        "dob": "1990-01-01",
        "email": "qa+a-1792245587396@test.local",
        "totp_enabled": false,
        "created_at": "2026-10-17 13:59:55",
        "address": {
          "street": "Teststraat 2",
          "house_number": null,
          "city": "Amsterdam",
          "state": null,
          "country": "NL",
          "postal_code": "1011 AB"
        },
        "enabled": true,
        "role": "user",
        "failed_login_attempts": 0
      },
      "replies": [
        {
          "id": "01m552ng4nw25e7fm798yyvdtd",
          "message": "Test reply from the admin.",
          "created_at": "2026-10-17 14:01:18",
          "user": {
            "id": "01M552JGSDY6PGB1Z42PFGTHSE",
            "provider": null,
            "first_name": "John",
    …
    • PassStatus 200
    • FailStatus stayed RESOLVED — expected 'NEW' to deeply equal 'RESOLVED'
  13. DEF-013MediumInvoice PDF generation fails for new invoices; download returns 404

    The PDF of a new invoice is never generated, so the download returns 404.

    Expected
    The PDF is generated and the download returns 200 with application/pdf.
    Observed
    Download: 404 “Document not created. Try again later.” The background job fails in the invoice template.
    Impact
    Customers cannot download new invoices. No security impact.
    Probable cause
    The template reads the payment method from a relation that is empty inside the job. The exact reason is not yet isolated.
    Suggested fix
    Confirm the relation inside the job, guard the template, and fix the cause.
    Linked tests · Retest
    T-F02 · not yet retested

    From the final run

    T-F02 · 1 status PDF

    Request

    GET /invoices/INV-2026000006/download-pdf-status
    Authorization: Bearer …[masked]
    Accept: application/json

    Response 400 Bad Request · 141 ms

    {
      "status": "NOT_INITIATED"
    }
    • PassStatus in [200, 400]

    T-F02 · 2 download PDF

    Request

    GET /invoices/INV-2026000006/download-pdf
    Authorization: Bearer …[masked]
    Accept: application/json

    Response 404 Not Found · 309 ms

    {
      "message": "Document not created. Try again later."
    }
    • FailStatus 200 — expected response to have status code 200 but got 404
    • FailContent-Type PDF — expected 'application/json' to include 'application/pdf'
    • PassFile is not empty
  14. DEF-014HighAny logged-in user can download another customer's invoice PDF

    The invoice PDF download looks the file up by invoice number only. Invoice numbers are sequential, so another customer's PDF is easy to fetch.

    Evidence: Confirmed in the Postman runner and by direct reproduction with an invoice whose PDF existed. In the final run no new PDF was generated (DEF-013), so the download returned 404 and the test passed.

    Steps to reproduce

    1. An invoice that belongs to customer A is chosen.
    2. Customer B, with no link to A, requests that invoice's PDF with their own token.
    Expected
    403 or 404, with no file.
    Observed
    200 with application/pdf, 11,376 bytes, containing customer A's billing data.
    Impact
    The name, address, products and total of any customer can be downloaded by anyone with an account.
    Probable cause
    The download searches by invoice number and does not check the owner, unlike the single-invoice read.
    Suggested fix
    Look the invoice up by number and owner (except for the admin), and return 404 otherwise.
    Linked tests · Retest
    T-J03 · not yet retested
  15. DEF-015MediumFavorites and message replies by id skip the owner check

    Reading a favourite by ID and replying to a message by ID both skip the owner check.

    Expected
    404 or 403, as for the other resources of another user.
    Observed
    Favourite: 200 with customer A's data. Reply: 201, saved under customer A's message.
    Impact
    A customer sees another's favourites and can write into another's conversation.
    Probable cause
    Both lookups find the record by ID only. The list of favourites does filter by owner.
    Suggested fix
    Filter by the current user, as the delete action already does; allow replies only for the owner or an admin.
    Linked tests · Retest
    T-J06, T-J07 · not yet retested

    From the final run

    T-J06 B reads A's favorite

    Request

    GET /favorites/01m552n6pqq6shecbw6zfbprcf
    Authorization: Bearer …[masked]
    Accept: application/json

    Response 200 OK · 121 ms

    {
      "id": "01m552n6pqq6shecbw6zfbprcf",
      "user_id": "01m552jz1by17vr3sx9zcb1275",
      "product_id": "01M552JH28NG35XD8J04PGAJXN"
    }
    • FailStatus in [401, 403, 404] — expected 200 to be one of [ 401, 403, 404 ]
    • PassNo data in response

    T-J07 B replies to A's message

    Request

    POST /messages/01m552nbn2vxb5meqh0gmw7ab2/reply
    Authorization: Bearer …[masked]
    Accept: application/json
    Content-Type: application/json
    
    {
      "message": "Reply from another account."
    }

    Response 201 Created · 127 ms

    {
      "message": "Reply from another account.",
      "id": "01m552nqc63tw7zd92nkacj95s",
      "created_at": "2026-10-17 14:01:25"
    }
    • FailStatus in [403, 404] — expected 201 to be one of [ 403, 404 ]
  16. DEF-016MediumCarts can be read, changed and deleted by anyone who knows the cart id

    Carts have no owner: anyone who knows a cart ID can read it, add to it or delete it.

    Evidence: Treated as a defect for changing and deleting. Reading a cart by its hard-to-guess ID is documented as the anonymous cart model.

    Expected
    Changes and deletion refused for anyone but the cart's owner.
    Observed
    Read: 200. Add: 200. Delete by a visitor: 204.
    Impact
    A customer's cart can be emptied or changed before they order. No personal data is exposed.
    Probable cause
    The cart has no owner field, and the cart routes have no authentication.
    Suggested fix
    Add an owner to the cart (empty for guests) and check it on change and delete.
    Linked tests · Retest
    T-J01 · not yet retested

    From the final run

    T-J01 · 3 B reads A's cart

    Request

    GET /carts/01m552nqnn2cgav3zka12fx3nd
    Authorization: Bearer …[masked]
    Accept: application/json

    Response 200 OK · 135 ms

    {
      "id": "01m552nqnn2cgav3zka12fx3nd",
      "additional_discount_percentage": null,
      "lat": null,
      "lng": null,
      "cart_items": [
        {
          "id": "01m552nqzghs1zadqzaprayfmg",
          "quantity": 1,
          "discount_percentage": null,
          "cart_id": "01m552nqnn2cgav3zka12fx3nd",
          "product_id": "01M552JH28NG35XD8J04PGAJXN",
          "product": {
            "id": "01M552JH28NG35XD8J04PGAJXN",
            "name": "Combination Pliers",
            "description": "Versatile combination pliers designed for gripping, bending, and cutting wire with ease. Featuring chrome vanadium steel construction with induction-hardened cu…",
            "price": 14.15,
            "is_location_offer": false,
            "is_rental": false,
            "co2_rating": "D",
            "in_stock": true,
            "is_eco_friendly": false
          }
        }
      ]
    }
    • FailStatus in [403, 404] — expected 200 to be one of [ 403, 404 ]

    T-J01 · 7 visitor deletes cart

    Request

    DELETE /carts/01m552ns84664b7atw0ehavqcf
    Accept: application/json

    Response 204 No Content · 107 ms

    Empty body.

    • FailStatus in [401, 403, 404] — expected 204 to be one of [ 401, 403, 404 ]
  17. DEF-017HighAnyone (even without a token) can create products and change their price

    The create and update routes for products have no authentication and no role check. A visitor can create products, and anyone can change a price.

    Steps to reproduce

    1. A price change is sent to an existing product, with no token.
    2. The same change is sent with a customer's token.
    3. The product is read back.
    Expected
    401 without a token, 403 for a customer. The price does not change.
    Observed
    Both changes return 200. The price changes from 14.15 to 0.01.
    Impact
    Anyone can change catalogue prices or add false products.
    Probable cause
    The role check applies to the delete action only. Create, update and patch have no authentication.
    Suggested fix
    Require authentication and the admin role on every write action.
    Linked tests · Retest
    T-I03 · not yet retested

    From the final run

    T-I03 · 3 change price without token

    Request

    PATCH /products/01M552JH28NG35XD8J04PGAJXN
    Accept: application/json
    Content-Type: application/json
    
    {
      "price": 0.01
    }

    Response 200 OK · 138 ms

    {
      "success": true
    }
    • FailStatus 401 — expected response to have status code 401 but got 200

    T-I03 · 5 price unchanged

    Request

    GET /products/01M552JH28NG35XD8J04PGAJXN
    Accept: application/json

    Response 200 OK · 139 ms

    {
      "id": "01M552JH28NG35XD8J04PGAJXN",
      "name": "Combination Pliers",
      "description": "Versatile combination pliers designed for gripping, bending, and cutting wire with ease. Featuring chrome vanadium steel construction with induction-hardened cu…",
      "price": 0.01,
      "is_location_offer": false,
      "is_rental": false,
      "co2_rating": "D",
      "in_stock": true,
      "is_eco_friendly": false,
      "product_image": {
        "id": "01M552JH1JNQZMARG3VDF4TG7J",
        "by_name": "Helinton Fantin",
        "by_url": "https://unsplash.com/@fantin",
        "source_name": "Unsplash",
        "source_url": "https://unsplash.com/photos/W8BNwvOvW4M",
        "file_name": "pliers01.avif",
        "title": "Combination pliers"
      },
      "category": {
        "id": "01M552JH0YW2D66AXDEDJGQGWD",
        "name": "Pliers",
        "slug": "pliers",
        "parent_id": "01M552JH0THC23EZ570GKVAFFH"
      },
      "brand": {
        "id": "01M552JGRRJK0308ZCZCH32JAH",
        "name": "ForgeFlex Tools"
      },
      "specs": [
        {
          "id": "01M552JH6P79413ZXA0S6SD6KS",
          "product_id": "01M552JH28NG35XD8J04PGAJXN",
          "spec_name": "Handle Material",
          "spec_value": "Bi-component",
          "spec_unit": null
        },
        {
          "id": "01M552JH6P79413ZXA0S6SD6KQ",
          "product_id": "01M552JH28NG35XD8J04PGAJXN",
    …
    • PassStatus 200
    • FailOriginal price — expected 0.01 to deeply equal 14.15
  18. DEF-018MediumBrands, categories and product specifications can be created without admin role

    Brands and categories can be created without a token, and product specifications by any customer.

    Expected
    401 or 403, as for deletion, which does check the role.
    Observed
    Brand without a token: 201. Specification as a customer: 201.
    Impact
    The catalogue can be filled with false brands and categories. No personal data is exposed.
    Probable cause
    The admin role is applied only to the delete actions.
    Suggested fix
    Apply the admin role to create and update, as in DEF-017.
    Linked tests · Retest
    T-I06, T-C16 · not yet retested

    From the final run

    T-I06 · 3 brand without token

    Request

    POST /brands
    Accept: application/json
    Content-Type: application/json
    
    {
      "name": "Brand QA anon-1792245587396",
      "slug": "brand-qa-anon-1792245587396"
    }

    Response 201 Created · 109 ms

    {
      "name": "Brand QA anon-1792245587396",
      "slug": "brand-qa-anon-1792245587396",
      "id": "01m552p17vfxne05ngjpf40rx3"
    }
    • FailStatus 401 — expected response to have status code 401 but got 201

    T-C16 · 2 regular user

    Request

    POST /products/01M552JH28NG35XD8J04PGAJXN/specs
    Authorization: Bearer …[masked]
    Accept: application/json
    Content-Type: application/json
    
    {
      "spec_name": "Test",
      "spec_value": "QA",
      "spec_unit": ""
    }

    Response 201 Created · 126 ms

    {
      "product_id": "01M552JH28NG35XD8J04PGAJXN",
      "spec_name": "Test",
      "spec_value": "QA",
      "spec_unit": null,
      "id": "01m552p3hxb48dvzqrd7hdhh4p"
    }
    • FailStatus in [401, 403] — expected 201 to be one of [ 401, 403 ]
  19. DEF-019MediumThe /refresh rate limit is cleared by the same request that triggers it

    The database reset route is limited to one call every five minutes, but the reset clears the cache that holds the limit. A second call succeeds at once.

    Expected
    The second call returns 429, with no second reset.
    Observed
    The second call returns 200, and the reset runs twice within a minute.
    Impact
    The protection against repeated resets does not work. On a public server it would be an availability problem.
    Probable cause
    The route clears the whole cache after the reset, which also removes the rate-limit counter.
    Suggested fix
    Clear only the reset's own key, or keep the limit in a separate store.
    Linked tests · Retest
    T-K10 · not yet retested

    From the final run

    T-K10 · 1 refresh

    Request

    POST /refresh
    Accept: application/json

    Response 200 OK · 8338 ms

    {
      "result": "refresh done"
    }
    • PassStatus 200
    • Passrefresh done

    T-K10 · 2 immediate refresh

    Request

    POST /refresh
    Accept: application/json

    Response 200 OK · 4691 ms

    {
      "result": "refresh done"
    }
    • FailStatus 429 — expected response to have status code 429 but got 200

Every request of the final run

Search and filter the whole run

355 of 355 requests

Requests of the final run, filtered
RequestCallStatusResult
S1 · 1 register A00 SetupPOST /users/register201Passed
S1 · 2 login A00 SetupPOST /users/login200Passed
S2 · 1 register B00 SetupPOST /users/register201Passed
S2 · 2 login B00 SetupPOST /users/login200Passed
S3 · login admin00 SetupPOST /users/login200Passed
S3b · products00 SetupGET /products200Passed
S4 · 1 create cart00 SetupPOST /carts201Passed
S4 · 2 add product00 SetupPOST /carts/01m552k2aw0jynek4z20tphp4w200Passed
S5 · 1 address lookup00 SetupGET /postcode-lookup?country=NL&postcode=1011%20AB200Passed
S5 · 2 order A00 SetupPOST /invoices201Passed
S5 · 3 create cart B00 SetupPOST /carts201Passed
S5 · 4 add product B00 SetupPOST /carts/01m552k4gf7hn8erq3g0c4gdgy200Passed
S5 · 5 order B00 SetupPOST /invoices201Passed
T-K11 Status01 EnvironmentGET /status200Passed
T-C01 Product list02 CatalogGET /products200Passed
T-C02 Product details02 CatalogGET /products/01M552JH28NG35XD8J04PGAJXN200Passed
T-C03 Non-existent product02 CatalogGET /products/01AAAAAAAAAAAAAAAAAAAAAAAA404Passed
T-C04 Invalid id format · text02 CatalogGET /products/abc404Passed
T-C04 Invalid id format · negative02 CatalogGET /products/-1404Passed
T-C04 Invalid id format · SQL injection02 CatalogGET /products/%27%20OR%201%3D1%20--404Passed
T-C04 Invalid id format · 300 characters02 CatalogGET /products/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx404Passed
T-C05 Search with matches02 CatalogGET /products/search?q=hammer200Passed
T-C06 Search without matches02 CatalogGET /products/search?q=zzzxxyyqqq200Passed
T-C07 Edge-case search · partial02 CatalogGET /products/search?q=ham200Passed
T-C07 Edge-case search · uppercase02 CatalogGET /products/search?q=HAMMER200Passed

Evidence

Screenshots from Postman, cropped to what matters. Select one to enlarge.

Limits

What this result does not show

  • The target is a local copy of a teaching application. The defects are real for this version; the conclusions do not describe a live service.
  • Three defects are not visible in the final run (DEF-004, DEF-008, DEF-014). Each one says where its evidence comes from.
  • One test, the expired token, cannot pass in a full run, because the token is still valid. It needs its own run after five minutes, and is reported as a limit of the test design.
  • One result is an observation, not a check: another user’s message returns 200 with an empty body. No content is exposed.
  • Folder-level evidence shows that a test passed in a run, not that every part of its response was checked.
  • Newman counts 582 assertions; the per-request export adds up to 600. The 18-assertion difference has not been fully traced, so the summary figure is used.
  • No fix has been applied, so nothing has been retested.

Contribution

What was mine, what was assisted

My work

  • Choosing the scope, the areas and the severity of each defect.
  • Reviewing every test, correcting the wrong ones, and running the folders in Postman.
  • Saving and masking the evidence, and checking results against the saved responses.
  • Deciding what counts as a defect and writing the defect reports.

Assisted by AI

  • Drafting the test cases and the Postman scripts, which I reviewed and corrected.
  • Running the final collection through Newman and building the report from the run files.
  • Writing the English text of this page and of the downloads, from the project files.

Downloads

Everything needed to check the work or run it yourself

More projects in software testing are planned.

Back to software testing