Skip to content

test: cover the six untested tRPC routers - #439

Merged
filopedraz merged 1 commit into
mainfrom
kosuke/chat-fff5yc
Sep 7, 2026
Merged

test: cover the six untested tRPC routers#439
filopedraz merged 1 commit into
mainfrom
kosuke/chat-fff5yc

Conversation

@filopedraz

Copy link
Copy Markdown
Member

What's Changed

Tests for the six tRPC routers that had none: auth, chat, documents, orders, organizations, tasks. Only admin, billing and user were covered.

An untested router is an untested authorization boundary. The procedure ladder in lib/trpc/init.ts is where authorization actually lives for most of this app, so this was the largest gap in an otherwise well-covered repo.

Each router covers every procedure's happy path, the authorization case per procedure (unauthenticated against protected, non-member against orgProcedure, non-owner against orgOwnerProcedure, non-admin against superAdminProcedure), input validation rejection, and the error paths already present in the router code.

Also adds expectTRPCError to __tests__/setup/utils.ts, because rejects.toThrow('...') passes for the wrong error code. Authorization tests now assert on the TRPCError code.

Type of Change

  • 🚀 feature - New feature or enhancement

Testing

  • Added new tests for changes (if applicable)

No router code was changed. Where a test documents a bug, it asserts current behavior and the bug is listed below rather than fixed here, so this PR stays reviewable as a test-only change.

Bugs found while writing these

Nothing here is fixed. Triage order would be 3, 4, 5.

3. chat.deleteSession and chat.updateSession are not user-scoped, but the reads are. listSessions / getSession / getMessages filter on chatSessions.userId === ctx.userId. The two mutations filter only on organizationId. Any member of an org can delete or rename another member's private chat session, including sessions they cannot read. Most serious finding.

4. tasks.update moves a task into any organization without a membership check. tasksRouter is protectedProcedure and forwards input.organizationId to taskService.updateTask, which only verifies task ownership. A user can attach their task to an org they do not belong to and it surfaces in that org's task list. Same for tasks.create.

5. organizations.getUserOrganizations takes userId from input and never compares it to ctx.userId. An arbitrary UUID is forwarded to auth.api.listOrganizations. Exploitability depends on whether Better Auth ignores that query for non-admin sessions, which is worth confirming. ctx.userId is right there.

6. The whole organizations router is protectedProcedure. deleteOrganization, removeMember, updateMemberRole, inviteMember take organizationId from input with all enforcement delegated to Better Auth inside the services. Defensible, but the procedure ladder does nothing here and every service is individually load-bearing. orgProcedure / orgOwnerProcedure exist and are unused.

1. orders.list() with no input crashes instead of returning BAD_REQUEST. orgProcedure does (await getRawInput()).organizationId; with an .optional() input schema rawInput is undefined, so it is a TypeError surfacing as INTERNAL_SERVER_ERROR. Affects any orgProcedure with an optional input schema.

2. getOrgMembersSchema.offset can never be sent explicitly. z.number().int().positive().default(0) — Zod does not validate defaults, so the implicit 0 works but a client sending offset: 0 gets BAD_REQUEST. Should be .min(0).

7. auth.requestOtp is a user-enumeration oracle. Sign-in with an unknown email returns NOT_FOUND: User not found; sign-up with a known email returns BAD_REQUEST: User already exists. Both confirm account existence to an unauthenticated caller.

8. auth.requestOtp logs the email before validating it, so rejected attempts still write PII to logs.

9. Dead branch in auth.requestOtp: the !input.terms check is unreachable because signUpSchema already refines terms === true.

10. getContentTypeByExtension returns MIME types uploadDocumentSchema rejects. .ts maps to text/typescript and .ppt to application/vnd.ms-powerpoint, neither in SUPPORTED_MIME_TYPES. Uploading those fails with "Unsupported file type" though the extension map claims support.

11. documents.list counts rows in JS. It selects every matching id then takes .length. chat.listSessions next door uses a SQL count().

12. getUserByEmail is typed non-nullable but returns null.

13. __tests__/lib/trpc/routers/user.test.ts does not test the user router. All seven cases exercise organizations.uploadOrganizationLogo / deleteOrganizationLogo. The user router is still untested; it is a tenth router that the "six of nine" count missed.


🤖 Generated with Claude Code

https://claude.ai/code/session_01GyBfVKhYQGLZhkcWiHaruY

…undaries

Adds router tests for auth, chat, documents, orders, organizations and tasks,
following the mocking conventions in billing.test.ts and admin.test.ts.

Each file covers every procedure's happy path, the authorization outcome for
each procedure (unauthenticated caller, non-member on orgProcedure, non-owner
on role-gated deletes), zod input rejection, and the not-found/conflict/forbidden
paths the routers already implement. No router code was changed; tests that hit
suspicious behavior document it as current behavior.

Also adds expectTRPCError to __tests__/setup/utils.ts so tests assert on the
TRPCError code rather than only the message.
@filopedraz
filopedraz merged commit 08d6bb5 into main Sep 7, 2026
4 checks passed
@filopedraz
filopedraz deleted the kosuke/chat-fff5yc branch September 7, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant