It Ran Fine in Production. Then I Wrote the Tests.
Source: Dev.to
The app worked. Users could log in. Data was saving. Nobody was on fire.
So naturally, I went looking for the fire.
The assignment
I was asked to audit a new Agent Referral feature on a school‑management system — agents refer schools, earn commissions, the usual. No brief. No checklist. Just “make sure it’s fine.”
The hidden privilege escalation
A helper method resolveAgent() was responsible for figuring out which agent was logged in:
- Check if the authenticated user is an Agent ✅
- If not, check if the URL has an
agent_idparameter and, if so, load that agent from the database ❌
Step 2 is a classic privilege escalation. Any authenticated user — a school admin, a teacher, literally anyone — could append ?agent_id=some‑uuid to the URL and gain full access to that agent’s dashboard, commissions, and payout history.
The fix (PHP)
return $user instanceof Agent ? $user : null;Now you’re an agent or you get nothing. No fallbacks. No trusting what someone typed in a URL.
Core principle: never trust the client
Anything arriving from a browser — URL parameters, form fields, headers, cookies — can be faked. Assume it is. This isn’t a Laravel‑specific rule; it’s a law of the internet, as immutable as gravity and far less forgiving.
Proving the fix with tests
I wrote tests, ran them, and the whole suite detonated before a single test executed because the database couldn’t rebuild itself from the migration files.
Issue 1 – Default on a TEXT column
A migration tried to set a default value on a TEXT column. TEXT is unbounded, so MariaDB refused:
“I’m not pre‑filling every essay box. That’s expensive and I refuse.”
MySQL might allow it depending on configuration, but MariaDB does not.
Issue 2 – Nullable column in a composite primary key
A composite primary key included a nullable column. A primary key that can be blank can’t identify anything, so the database correctly rejected it.
Why production seemed fine
The production database had been built incrementally over months and never torn down. My tests used RefreshDatabase, which destroys everything and rebuilds from scratch on each run. The clean slate exposed every shortcut that had been quietly accumulating.
Separation of concerns: where defaults belong
Think of your backend as a bank:
- Vault (database) – stores things safely. It doesn’t think.
- Clerk (application) – handles logic. Decides what to fill in when nothing is specified.
The original code asked the vault to think; the vault refused. I moved the defaults to the Model layer:
protected $attributes = [
'overall_comment' => 'This student is good.',
'principal_comment' => 'This student is hardworking.',
];Now the application handles the logic, and the database simply stores the data. Swapping PostgreSQL for MySQL tomorrow won’t affect the defaults, and rewriting the frontend won’t impact the backend.
Test suite as living documentation
I wrote eight tests:
- 5 security tests – a school admin tries every angle to access an agent’s dashboard; each returns
401 Unauthorized. - 3 auth tests – agents can still register, log in, and are rejected with the wrong password. The front door still works.
“I fixed it” is a belief. Eight green checkmarks are evidence. Six months from now, when someone asks, “Are we certain a school admin can’t get into agent data?” the answer isn’t a Slack message—it’s a test suite that runs on every code change.
Principles vs. design patterns
| First principles (the physics) | Design patterns (the blueprints) |
|---|---|
| User input is untrusted. Always. | Zero‑trust → authentication guards, no URL‑parameter fallbacks |
| A primary key that might not exist can’t identify anything. | Move defaults out of the DB, into the Model |
| A database won’t efficiently enforce constraints it wasn’t built for. | Use factories and tests to prove behavior |
The pattern is the tool; the principle explains why you pick it up.
Three questions to ask about any system
- Who owns this responsibility? If the answer isn’t obvious, that’s the bug.
- What is fundamentally true here? Strip away assumptions.
- Can you prove it? If not, you don’t know—you just haven’t been wrong yet.
The recipes change. The physics don’t.
Call for stories
Drop a comment — genuinely curious: what’s the worst “it worked in production” moment you’ve walked into? The more specific the horror, the better. 👇
Backend engineer. I write about systems, architecture, and what happens when you poke at things that were better left unexamined. Portfolio: cycy.is-a.dev 🚀