Back to Proof of Work
Independent QA Automation Project

Razorpay Payment Gateway QA & Automation

Testing a payment system beyond the happy path.

An independent QA engineering project built around a real Razorpay Test Mode payment — not just to make checkout work, but to test what happens when it doesn't. I verified the payment lifecycle, probed for tampered signatures and replayed requests, and pushed webhook handling and order state until two genuine defects surfaced. Both were investigated, fixed, and covered by regression tests.

Independent, self-directed project — not client or employer work, and not commissioned by Razorpay. It exists to demonstrate how I approach QA on a real payment integration: the test strategy, the defects found, and the two things I couldn't fully prove are documented in the repository, the same way the passing tests are.

37
Automated Tests
2
High-Severity Defects Found & Fixed
34
CI-Safe Tests
1
Real Test Mode Payment Completed

Why I Built This

Most payment-testing demos stop at the same place: the checkout succeeds, the order updates, done. I wanted something small enough to fully own, so I could test it the way a payment actually gets used, not the way a demo gets shown.

That meant asking the questions a happy-path run never answers. What if the same verification request gets replayed? What if the amount is manipulated between the client and the server? What if a signature is invalid, or the payload has been tampered with after signing? What if the same webhook event arrives twice, or a state transition happens in an order the application didn't expect? What if a third-party SDK throws an error shape my code didn't plan for?

A successful checkout doesn't answer any of that. It just proves the happy path is happy.

The Testing Problem

A payment crosses several boundaries before it's actually done: the checkout UI, the application, the payment gateway, an asynchronous webhook, and whatever local record represents the order's state. Each boundary can disagree with the others — a UI success doesn't guarantee the server verified the payment correctly, and a verified payment doesn't guarantee a webhook, if one ever arrives, won't try to reapply the same event a second time.

UI → Application → Payment Gateway → Webhook → Order State

The job wasn't to make the payment work. It was to find out whether the system under test stayed correct when payment behavior became unreliable — because eventually, it will.

Payment / System Flow

Two paths can influence the same payment state. They need to remain consistent.

Client Verification Path
Checkout
Application
Payment Verification
Webhook Path
Razorpay
Webhook Handler
Order State · SQLite

The client-verification path confirms the payment the moment Checkout completes. The webhook path is supposed to confirm it independently, asynchronously, whenever Razorpay's event arrives. Both write to the same order state — which means either path repeating itself, arriving late, or arriving out of order has to leave the record correct, not just whichever path gets there first.

How I Tested It

Rather than one long checklist, the suite is organized around what actually needed proving.

Functional Testing

Order creation, a completed Test Mode payment, refund request validation, and the cancellation/retry path — the baseline every payment flow has to get right before anything else matters.

API Testing

Request and response validation across the order and configuration endpoints, payment verification behavior, and error handling that never leaks raw SDK internals back to the client.

Security Testing

Signature verification under tampering, amount integrity — the server trusts its own records, not client-supplied amounts — and replay protection on the verification endpoint.

Webhook Testing

Raw-body handling ahead of any parsing middleware, HMAC validation over that untouched body, and idempotency so a duplicated delivery can't be applied twice.

State & Persistence Testing

Every transition in the order state machine validated explicitly — invalid transitions rejected, duplicate transitions treated as safe no-ops, not errors.

UI Testing

Playwright driving the actual Checkout interaction in Chromium — not a mocked payment form — through both the success and the failure path.

CI Testing

34 of the 37 tests deliberately isolated from real credentials and live gateway state, so the suite can run unattended on every push.

Two failures I didn't expect to find

Both were genuine defects the automation surfaced — not bugs planted to make a portfolio project look interesting.

Refund Error Handling — Server Crash

Risk

A payment gateway rejection should never take the application down with it.

What I observed

A refund request Razorpay rejected didn't fail cleanly — it crashed the server process.

Investigation

I reproduced the crash with a refund I knew Test Mode would reject, then looked at the actual shape of the thrown error instead of trusting the SDK's documented format. The refund route's catch block was calling .message.includes() on it, assuming a .message string the way a standard JavaScript Error exposes one.

Root cause

The SDK error didn't populate .message the way the handler assumed. Calling .includes() on undefined threw a second, unhandled exception inside the error handler itself — and that's what actually took the process down. The information the handler needed was available on error.statusCode.

Fix

Rewrote the handler to read error.statusCode and return a sanitized, controlled error response instead of trusting SDK internals it hadn't verified.

Regression

REF-ERR-001 and REF-ERR-002 now assert the server stays alive and returns a proper error response whenever Razorpay rejects a refund.

Result

The same rejected refund that used to crash the process now returns a controlled response, and the server keeps running.

Webhook Signature Validation — Raw Body Failure

Risk

A webhook signature has to be calculated against the exact bytes Razorpay signed — anything that touches those bytes first breaks verification silently.

What I observed

Correctly signed webhook requests were being rejected as invalid, every time.

Investigation

The cryptographic logic itself was correct — I confirmed that by hand-building signatures with the right secret and watching them fail too. That pointed away from the HMAC comparison and toward what the route was actually receiving.

Root cause

Express's global express.json() middleware was mounted ahead of the webhook route. It parsed and re-serialized the request body before the webhook handler ever saw it, so the bytes being hashed weren't the exact bytes Razorpay had signed. The signature check was correct; it was checking the wrong payload.

Fix

Mounted the webhook route before the global JSON parser, so it receives the untouched raw body before any middleware can transform it.

Regression

Deterministic coverage across a valid signature, an invalid signature, a tampered payload, and a duplicate event ID — each confirming the handler now sees and verifies the exact bytes it's supposed to.

Result

Signature validation now works correctly and deterministically, on every run.

What this doesn't prove: a real Razorpay-originated webhook was never observed arriving at the running application. That's a separate, honest limitation, covered below — not something this fix papers over.

The State Machine

State-transition testing, not just architecture. The order state machine has five states, and the transitions between them are validated explicitly rather than left to application logic scattered across routes.

created
authorized
paid
refunded (terminal)
Alternative outcome — from created or authorized, not from paid or refunded:
failed (terminal)

Invalid transitions are rejected — paid can't move back to authorized, and neither failed nor refunded transition anywhere else; they're terminal. Repeating the same transition is treated as a safe no-op, not an error — which is exactly what a webhook redelivering the same event needs to be able to rely on, and exactly what stops a late or out-of-order event from incorrectly reapplying state.

37 Tests, But the Number Is Only Useful With Context

37
Total Tests
34
CI-Safe
3
Razorpay-Live

The 34-test suite is deliberately isolated from credentials and live gateway state, so it can run unattended on every push and pull request. The remaining 3 — SIG-001, REPLAY-001, UI-002 — need genuine Razorpay Test Mode conditions and are run separately, on purpose. Mixing them into CI would mean either committing real credentials or making the pipeline flaky against a service it doesn't control. That's a test-strategy decision, not a shortcut.

CI / Reproducibility

Regression confidence, not a deployment feature. The CI-safe suite runs automatically through the "Playwright CI" GitHub Actions workflow — Node 24, Chromium, npm ci, placeholder Razorpay configuration so no real secrets sit in CI. Every run produces an HTML report; a failing test captures its trace and screenshot automatically.

Keeping this suite separate from live gateway dependencies is what makes it trustworthy for regression testing — it doesn't need Razorpay to be reachable, or a Test Mode account to be in a particular state, to tell me whether something broke.

What I Could Not Fully Prove

Real webhook delivery

I never observed a genuine Razorpay-originated webhook arrive at the running application, even with a reachable public tunnel confirmed. What I did verify deterministically: raw-body handling, HMAC validation, correctly and incorrectly signed payloads, tampered payloads, and duplicate event IDs — all generated by the test suite itself, not received live from Razorpay. Those are two different claims, and I'm only making the one I can back up.

Successful real refund

No real Razorpay refund was completed — an account/environment restriction on this specific Test Mode account blocked it, confirmed to be external to the application. The refund route, its validation, its error handling (see DEF-001 above), and its state transitions are implemented and tested; the one thing untested is a live, successful refund response from Razorpay itself.

What I Learned

Payment testing turned out to be state testing more than checkout testing — most of the interesting failures lived in what happens between systems, not inside any single request.

DEF-001 changed how I treat third-party SDK errors — "the docs describe a .message property" isn't the same as verifying what actually gets thrown, and I don't assume SDK error shapes anymore. DEF-002 turned something I only knew abstractly into something concrete: webhook correctness depends entirely on the exact bytes you hash, which makes middleware ordering a security decision, not just a code-organization one.

The two limitations taught me the most, honestly. "Implemented," "automated," and "observed working end-to-end" are three different claims, and it's easy to blur them together in a portfolio. This project is where I practiced keeping them separate.

What This Demonstrates

Risk-Based Testing
Functional Testing
API Testing
Security Testing
Payment Lifecycle Testing
Defect Investigation
Root-Cause Analysis
Regression Automation
State-Transition Testing
Webhook Testing
CI Regression
Honest Test Reporting

Evidence & Verification

All project artifacts are publicly available on GitHub for independent inspection and verification.

Source Code

Complete Express application, Playwright test suite, state machine, and route handlers.

View Repository →

CI Execution

GitHub Actions "Playwright CI" workflow — run history, HTML reports, and failure artifacts.

View CI/CD Logs →

QA Documentation

Test strategy, full test case inventory, defect reports with root causes, and the real-payment evidence walkthrough.

View Test Strategy →

Want to see the testing evidence?

The source code, test strategy, defect documentation and CI execution are all in the repository — including the two things I couldn't fully prove.