PIPERLINK · TEST REPORTFictional example

Sample test report: Acme Portal

Release report · version 1.4

App
Acme Portal — web app (Lovable + Supabase, Stripe payments)
Package
Bugs + vulnerabilities · 1 platform (web)
Build
staging, 14 October 2026
Period
14–18 October 2026
Tested by
A piperlink QA engineer and cybersecurity specialist
Report
PL-R-0042 · version 1

This is a fictional example to show the format and depth of a report. The app, bugs, data and screenshots are not real.

01Summary

VerdictDo not release until 2 critical and 3 high issues are fixed.

Found

2Critical
3High
3Medium
2Low

Main risks

  1. Pressing “Pay” twice charges the customer twice.
  2. Anyone with the app’s public key can read every order, with addresses and phone numbers.
  3. A payment can be “confirmed” with a forged request — no money needed.

What is good

Sign-up, the catalogue, the cart and the product admin work reliably on both devices. No secret keys in the page code.

Every critical and high issue can be fixed with the prompts in this report. After the fixes, retests are free for 14 days.

02What we tested

Devices and browsers

DeviceSystemBrowserScreen
MacBook Air M2macOS 15.0Chrome 1291440 × 900
iPhone 15iOS 18.0Safari393 × 852

Roles

Customer · Store owner · Admin · Signed out

Out of scope

Load testing; Nova Poshta delivery prices (no test access); the iOS and Android apps.

5 main scenarios

  1. Sign-up and login (e-mail, Google, password reset)
  2. Catalogue → cart → payment with a test card
  3. E-mails: confirmation, password reset, receipt
  4. Customer account: orders, profile
  5. Admin: products, orders, roles

Screens: 22 of the 25 included. Test data: a separate test account for each role, a Stripe test card.

03How we tested

Scenario testing

A test plan of 64 test cases across the 5 main scenarios — each run on two devices.

Exploratory testing

Four 60-minute sessions looking for where an ordinary customer gets stuck or does something unexpected.

Negative and boundary values

Empty fields, 1,000 characters, emoji, phone numbers with “+” and spaces, past dates, repeated clicks.

Roles and access

Every action checked under three roles and signed out: who sees what and who can change what.

Non-destructive vulnerability testing

Database access rules (Supabase RLS), keys in the code, admin and API without login, forged payments, password guessing. Only on test accounts and our own test orders.

Regression

After the fixes, a retest of every issue and of the scenarios it touches.

How we rate severity

Critical
Money or data lost, or the main scenario broken for everyone.
High
An important scenario breaks for some people; no workaround or a hard one.
Medium
Gets in the way, but there is a simple workaround.
Low
Cosmetic, wording, a rare case.

04Results by scenario

ScenarioTest casesPassedFailedBlockedIssues
Sign-up and login141220B-02, V-03
Catalogue, cart, payment161132B-01, B-03, V-02
E-mails10910B-05
Customer account12930B-04, B-06, V-01
Admin121011B-07
Total6451103

Blocked: delivery prices (no Nova Poshta test access) and the CSV export of orders (cut off after 30 s — raised with the team as a question, not a bug).

05Bugs

B-01CriticalPriority P1

Pressing “Pay” twice creates two orders and charges twice

Scenario
Catalogue, cart, payment
Environment
MacBook Air · Chrome 129 · macOS 15; also iPhone 15 · Safari
Frequency
5 of 5
Preconditions
The customer is signed in; one $24 item in the cart.

Steps to reproduce

  1. Open the payment page /checkout.
  2. Enter the test card 4242 4242 4242 4242.
  3. Quickly press “Pay” twice.

Expected result

One order and one charge. The button is disabled while the payment runs.

Actual result

Two orders (#1043 and #1044) and two $24 charges in the Stripe test dashboard.

Screenshot

Stripe · Payments (test mode)

$24.00 · Succeeded · 14:02:11

$24.00 · Succeeded · 14:02:11

AI prompt

On the payment page, disable the “Pay” button and show a spinner from the first click until the request finishes. In the create-payment function, make the request idempotent: generate one key per cart and return the existing payment when the same key arrives again. Add a test: two clicks within 300 ms create exactly one order and one charge.

B-02HighPriority P2

The password-reset link is “invalid” at once for customers ahead of UTC

Scenario
Sign-up and login
Environment
Both devices
Frequency
5 of 5 (time zone UTC+3)
Preconditions
A customer account with a password, time zone Kyiv.

Steps to reproduce

  1. Press “Forgot password?” and enter the e-mail.
  2. Open the e-mail within 2 minutes.
  3. Press the link in the e-mail.

Expected result

The new-password form; the link works for 60 minutes.

Actual result

A page “The link is invalid or expired”. Likely cause: the expiry is compared in local time, not UTC — in Kyiv the link is 3 hours “older”.

Screenshot

acme-portal.app/reset

The link is invalid or expired

Request a new link

AI prompt

Store and compare the password-reset token expiry in UTC (timestamptz), not in local time. Add a test: a user in UTC+3 opens the link after 2 minutes and sees the new-password form.

B-03HighPriority P2

After payment the account says “Awaiting payment” until the page is reloaded

Scenario
Catalogue, cart, payment
Environment
Both devices
Frequency
4 of 5
Preconditions
The customer paid an order with the test card.

Steps to reproduce

  1. Pay for an order.
  2. Wait to return to “My orders”.
  3. Do not reload the page for 60 seconds.

Expected result

“Paid” appears a few seconds after the payment is confirmed.

Actual result

The status stays “Awaiting payment” and “Download” is disabled. After a reload it says “Paid”.

Screenshot

acme-portal.app/orders

Order #1045 · Awaiting payment

Download (disabled)

AI prompt

On “My orders”, subscribe to order status changes (Supabase Realtime) or check the status every 3 seconds for 60 seconds after returning from payment. As soon as the status is “paid”, update the card and enable “Download”.

B-04MediumPriority P3

The “Phone” field rejects numbers with “+” and spaces

Scenario
Customer account
Environment
Both devices
Frequency
5 of 5
Preconditions
The customer is on the profile page.

Steps to reproduce

  1. Open “Profile”.
  2. Enter +380 67 123 45 67.
  3. Press “Save”.

Expected result

The number is saved as +380671234567.

Actual result

An “Invalid format” error. Only 0671234567 is accepted.

Screenshot

acme-portal.app/profile

Phone: +380 67 123 45 67

Invalid format

AI prompt

In the “Phone” field, strip spaces, brackets and dashes before validation and accept the international format with “+”. Store the number in E.164 (+380671234567). Add tests for “+380 67 123 45 67”, “067-123-45-67” and “(067) 123 45 67”.

B-05MediumPriority P3

The “Order received” e-mail arrives in English while the interface is Ukrainian

Scenario
E-mails
Environment
Gmail and Apple Mail
Frequency
5 of 5
Preconditions
The interface language is Ukrainian.

Steps to reproduce

  1. Place an order.
  2. Open the confirmation e-mail.

Expected result

The e-mail is in Ukrainian, like the interface.

Actual result

The subject and text are in English: “Your order has been received”.

Screenshot

Gmail · Inbox

Your order has been received

Order #1045 · $24.00

AI prompt

Store the language the customer chose in the interface on the profile and the order. Send the confirmation e-mail with that language’s template; if there is none, use the default language.

B-06LowPriority P4

An empty “My orders” page shows “undefined”

Scenario
Customer account
Environment
Both devices
Frequency
5 of 5
Preconditions
A new account without orders.

Steps to reproduce

  1. Sign in to a new account.
  2. Open “My orders”.

Expected result

The text “You have no orders yet” and a “Go to catalogue” button.

Actual result

The page says “undefined”.

Screenshot

acme-portal.app/orders

undefined

AI prompt

On “My orders”, when the list is empty, show “You have no orders yet” and a “Go to catalogue” button instead of the variable’s value.

B-07LowPriority P4

Admin: sorting orders by date works like sorting text

Scenario
Admin
Environment
MacBook Air · Chrome 129
Frequency
5 of 5
Preconditions
Admin role; orders from 9.10 and 10.10.

Steps to reproduce

  1. Open “Orders”.
  2. Sort by date.

Expected result

10.10 after 9.10.

Actual result

10.10 comes before 9.10 — the dates are compared as strings.

Screenshot

acme-portal.app/admin/orders

10.10.2026 · #1040

9.10.2026 · #1039

AI prompt

In the admin, sort orders by the created_at field (date and time), not by the formatted date string.

06Vulnerabilities

V-01CriticalOWASP A01 · Broken Access Control (CWE-284)

The orders table is open to anyone with the public key

Where
Supabase, table orders: Row Level Security is off.
Impact
Every customer’s personal data leaks. This breaks data-protection law and GDPR.

How we checked

Signed in as test customer B, we called the Supabase REST API with the public key visible in the page code. We got every order, including test customer A’s: names, addresses, phone numbers. Nothing was changed.

AI prompt

Enable Row Level Security on public.orders. Add policies: a customer sees only rows where customer_id = auth.uid(); a store owner only their store’s orders; no insert, update or delete from the browser — only the create-order function creates orders. Check with two test accounts: customer B cannot see customer A’s orders.

V-02HighOWASP A08 · Software and Data Integrity Failures (CWE-345)

The payment webhook does not check the signature — payments can be forged

Where
The payment-webhook function.
Impact
Anyone can get goods or access without paying.

How we checked

We sent /functions/v1/payment-webhook a test request without a Stripe signature for our own test order. The order became “Paid” without any payment.

AI prompt

In the payment-webhook function, verify the Stripe-Signature header with the webhook secret (stripe.webhooks.constructEvent) and reject requests without a valid signature with a 400. Mark an order paid only when the payment amount and currency match the order.

V-03MediumOWASP A07 · Identification and Authentication Failures (CWE-307)

Login does not limit password attempts

Where
The login page and Supabase Auth.
Impact
A password can be guessed by brute force.

How we checked

On our own test account we entered 50 wrong passwords in a minute — no lock, delay or captcha.

AI prompt

Limit login attempts: after 5 failed attempts in 15 minutes for one e-mail or IP, add a delay and a captcha. Turn on brute-force protection in Supabase Auth and show a clear “Too many attempts, try again in a few minutes”.

07What works well

  • Sign-up with e-mail and Google on both devices.
  • The catalogue, filters and search are fast; an empty search shows a hint.
  • The cart is kept between sessions and devices.
  • Admin: adding and editing products, assigning roles.
  • The admin is closed without login and to the Customer role.
  • No secret keys in the page code — only the public Supabase key, which is fine.

08Retest and verdict

Retest on 21 October 2026 (example)

#IssueStatus
V-01Orders table open to everyoneFixed ✓
B-01Double chargeFixed ✓
V-02Forged paymentFixed ✓
B-02Password-reset linkFixed ✓
B-03Status after paymentFixed ✓
V-03Password guessingFixed ✓
B-04“Phone” fieldStill failing
B-05E-mail languageNot fixed yet
B-06“undefined”Not fixed yet
B-07Date sortingNot fixed yet
VerdictReady to release: no critical or high issues left. B-04 and the low-severity issues are up to you in the next update.

The release decision is yours. We give you evidence within the test plan; testing does not guarantee that no bugs are left.