All files / routes/internal test-cleanup.ts

100% Statements 41/41
100% Branches 22/22
100% Functions 4/4
100% Lines 40/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134                          1x     1x 14x 14x 1x   13x         1x 10x 10x 3x     7x 7x     7x           7x 7x 7x 7x 7x 7x     7x   3x       3x     3x       3x     3x       3x     3x       3x       7x       7x   10x                           1x 3x 3x 1x     2x 2x                                 2x 1x     1x        
// Test Cleanup - Delete test data created by E2E tests
// ONLY available in non-production environments
 
import { Hono } from "hono";
import { getDb } from "../../db";
import * as schema from "../../db/schema";
import { like, eq, and, isNull, desc } from "drizzle-orm";
import { success, error } from "../../lib/response";
 
type Env = {
	Bindings: CloudflareBindings;
};
 
const testCleanup = new Hono<Env>();
 
// Guard: block in production
testCleanup.use("*", async (c, next) => {
	const env = c.env.ENVIRONMENT || "local";
	if (env === "production") {
		return error(c, "FORBIDDEN", "Test cleanup is not available in production", 403);
	}
	await next();
});
 
// DELETE /test-cleanup?emailPrefix=e2e-
// Deletes users, accounts, sessions, roles, and invitations matching the email prefix
testCleanup.delete("/", async (c) => {
	const emailPrefix = c.req.query("emailPrefix");
	if (!emailPrefix || emailPrefix.length < 8) {
		return error(c, "BAD_REQUEST", "emailPrefix query param required (min 8 chars)", 400);
	}
 
	const db = getDb(c.env.DB);
	const emailPattern = `${emailPrefix}%`;
 
	// Find users matching prefix
	const users = await db
		.select({ id: schema.users.id, email: schema.users.email })
		.from(schema.users)
		.where(like(schema.users.email, emailPattern))
		.all();
 
	const userIds = users.map((u) => u.id);
	let deletedUsers = 0;
	let deletedRoles = 0;
	let deletedInvitations = 0;
	let deletedSessions = 0;
	let deletedAccounts = 0;
 
	// Delete related data for each user
	for (const userId of userIds) {
		// Delete sessions
		const sessionResult = await db
			.delete(schema.sessions)
			.where(eq(schema.sessions.userId, userId))
			.run();
		deletedSessions += sessionResult.meta.changes ?? 0;
 
		// Delete accounts
		const accountResult = await db
			.delete(schema.accounts)
			.where(eq(schema.accounts.userId, userId))
			.run();
		deletedAccounts += accountResult.meta.changes ?? 0;
 
		// Delete user_tenant_roles
		const roleResult = await db
			.delete(schema.userTenantRoles)
			.where(eq(schema.userTenantRoles.userId, userId))
			.run();
		deletedRoles += roleResult.meta.changes ?? 0;
 
		// Delete user
		const userResult = await db
			.delete(schema.users)
			.where(eq(schema.users.id, userId))
			.run();
		deletedUsers += userResult.meta.changes ?? 0;
	}
 
	// Delete invitations matching email prefix
	const invitationResult = await db
		.delete(schema.teamInvitations)
		.where(like(schema.teamInvitations.email, emailPattern))
		.run();
	deletedInvitations += invitationResult.meta.changes ?? 0;
 
	return success(c, {
		message: "Test cleanup complete",
		deleted: {
			users: deletedUsers,
			sessions: deletedSessions,
			accounts: deletedAccounts,
			roles: deletedRoles,
			invitations: deletedInvitations,
		},
	});
});
 
// GET /test-cleanup/invitation-token?email=...
// Look up the most recent pending invitation token for an email (for E2E test link navigation)
testCleanup.get("/invitation-token", async (c) => {
	const email = c.req.query("email");
	if (!email) {
		return error(c, "BAD_REQUEST", "email query param required", 400);
	}
 
	const db = getDb(c.env.DB);
	const invitation = await db
		.select({
			token: schema.teamInvitations.token,
			proId: schema.teamInvitations.proId,
			role: schema.teamInvitations.role,
			expiresAt: schema.teamInvitations.expiresAt,
		})
		.from(schema.teamInvitations)
		.where(
			and(
				eq(schema.teamInvitations.email, email.toLowerCase().trim()),
				isNull(schema.teamInvitations.acceptedAt),
			),
		)
		.orderBy(desc(schema.teamInvitations.dateCreated))
		.get();
 
	if (!invitation) {
		return error(c, "NOT_FOUND", "No pending invitation found for this email", 404);
	}
 
	return success(c, { token: invitation.token });
});
 
export default testCleanup;