meta: task ref cleanup
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Unit tests for the generalized account-deletion-request email helpers
|
||||
* (task 11).
|
||||
* (see `misc.ts`).
|
||||
*
|
||||
* These are the pure, env-free helpers consumed by the
|
||||
* `misc.sendDeletionRequestEmail` tRPC mutation (re-exported from `misc.ts`).
|
||||
@@ -25,11 +25,11 @@ import {
|
||||
} from "~/server/api/routers/deletion-email";
|
||||
|
||||
describe("DELETION_PRODUCT_SCHEMA", () => {
|
||||
it("accepts \"lineage\"", () => {
|
||||
it('accepts "lineage"', () => {
|
||||
expect(DELETION_PRODUCT_SCHEMA.safeParse("lineage").success).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts \"nessa\"", () => {
|
||||
it('accepts "nessa"', () => {
|
||||
expect(DELETION_PRODUCT_SCHEMA.safeParse("nessa").success).toBe(true);
|
||||
});
|
||||
|
||||
@@ -50,9 +50,7 @@ describe("deletionCookieName", () => {
|
||||
});
|
||||
|
||||
it("returns distinct names per product", () => {
|
||||
expect(deletionCookieName("lineage")).not.toBe(
|
||||
deletionCookieName("nessa")
|
||||
);
|
||||
expect(deletionCookieName("lineage")).not.toBe(deletionCookieName("nessa"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Pure helpers for the generalized account-deletion-request email flow
|
||||
* (task 11).
|
||||
* (see `misc.ts`).
|
||||
*
|
||||
* Extracted from `src/server/api/routers/misc.ts` so they can be unit-tested
|
||||
* in `bun:test` WITHOUT importing `~/env/server` (which validates ~30 secrets
|
||||
@@ -49,7 +49,7 @@ export interface DeletionEmailContent {
|
||||
* The operator email identifies the request name + requester email; the user
|
||||
* email identifies the account being deleted + the 24h cancellation window.
|
||||
* The `product` discriminator switches branding between Lineage (the original
|
||||
* flow) and Nessa (task 11 — Nessa stores user data in its own Turso DB).
|
||||
* flow) and Nessa (Nessa stores user data in its own Turso DB).
|
||||
*
|
||||
* `email` is interpolated verbatim into the HTML bodies. It has already been
|
||||
* validated as a well-formed email by the tRPC input schema, and Sendinblue
|
||||
|
||||
@@ -50,7 +50,7 @@ mock.module("~/env/server", () => ({
|
||||
TURSO_DB_API_TOKEN: "test-token",
|
||||
NESSA_DB_URL: "libsql://nessa-test.turso.io",
|
||||
NESSA_DB_TOKEN: "test-token",
|
||||
// Clerk env vars (required after migration in task 02)
|
||||
// Clerk env vars (required after migration)
|
||||
NESSA_CLERK_SECRET: "sk_test_test-secret",
|
||||
NESSA_CLERK_JWT_ISSUER: "https://nessa-test.clerk.accounts.dev"
|
||||
},
|
||||
@@ -61,9 +61,8 @@ mock.module("~/env/server", () => ({
|
||||
|
||||
// Import after env mock is registered. These are the real verification
|
||||
// functions used by web and Lineage surfaces respectively.
|
||||
const { verifyAuthToken, verifyLineageAuthToken } = await import(
|
||||
"~/server/auth"
|
||||
);
|
||||
const { verifyAuthToken, verifyLineageAuthToken } =
|
||||
await import("~/server/auth");
|
||||
// Issuer/audience claims the Lineage router stamps onto its tokens.
|
||||
const { LINEAGE_CONFIG } = await import("~/config");
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* p8-001 / p8-008 regression tests — S3 procedure lockdown & input sanitization.
|
||||
*
|
||||
* These tests verify the security remediation from task 02 without standing up
|
||||
* These tests verify the security remediation without standing up
|
||||
* the full tRPC router (which requires S3 / env / database / vinxi-runtime
|
||||
* mocking that is unreliable under `bun test`). They follow the proven pattern
|
||||
* from task 03 (p8-002): direct unit tests of the authz/sanitization helpers
|
||||
* (p8-002): direct unit tests of the authz/sanitization helpers
|
||||
* plus a static source-code audit that the previously-`publicProcedure` S3
|
||||
* endpoints are now `csrfProtectedProcedure` (i.e. no longer anonymous).
|
||||
*
|
||||
@@ -136,7 +136,9 @@ describe("p8-001 / p8-008 static source audit", () => {
|
||||
for (const proc of S3_PROCEDURES) {
|
||||
it(`${proc} is not declared as publicProcedure`, () => {
|
||||
// Match the procedure declaration line and ensure it is not publicProcedure.
|
||||
const re = new RegExp(`\\b${proc}\\s*:\\s*(publicProcedure|csrfProtectedProcedure|protectedProcedure|adminProcedure|nessaProcedure)`);
|
||||
const re = new RegExp(
|
||||
`\\b${proc}\\s*:\\s*(publicProcedure|csrfProtectedProcedure|protectedProcedure|adminProcedure|nessaProcedure)`
|
||||
);
|
||||
const m = SOURCE.match(re);
|
||||
expect(m, `${proc} declaration not found`).not.toBeNull();
|
||||
expect(m![1]).not.toBe("publicProcedure");
|
||||
@@ -144,7 +146,9 @@ describe("p8-001 / p8-008 static source audit", () => {
|
||||
}
|
||||
|
||||
it("getDownloadUrl (Sparkle updater) remains the only public S3 endpoint", () => {
|
||||
const m = SOURCE.match(/\bgetDownloadUrl\s*:\s*(publicProcedure|csrfProtectedProcedure|protectedProcedure)/);
|
||||
const m = SOURCE.match(
|
||||
/\bgetDownloadUrl\s*:\s*(publicProcedure|csrfProtectedProcedure|protectedProcedure)/
|
||||
);
|
||||
expect(m, "getDownloadUrl declaration not found").not.toBeNull();
|
||||
expect(m![1]).toBe("publicProcedure");
|
||||
});
|
||||
@@ -153,7 +157,9 @@ describe("p8-001 / p8-008 static source audit", () => {
|
||||
// Both simpleDeleteImage and deleteImage must call the ownership guard.
|
||||
const deleteBlocks = SOURCE.split(/(\bsimpleDeleteImage:|\bdeleteImage:)/);
|
||||
// Count occurrences of the ownership call within the delete mutation bodies.
|
||||
const occurrences = (SOURCE.match(/assertS3KeyOwnership\(input\.key/g) || []).length;
|
||||
const occurrences = (
|
||||
SOURCE.match(/assertS3KeyOwnership\(input\.key/g) || []
|
||||
).length;
|
||||
expect(occurrences).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ export function assertS3KeyOwnership(key: string, userId: string | null): void {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Account-deletion request email (task 11 — product-aware)
|
||||
// Account-deletion request email — product-aware
|
||||
// ============================================================
|
||||
//
|
||||
// Pure helpers live in `./deletion-email.ts` (env-free) so they can be unit-
|
||||
@@ -368,7 +368,7 @@ export const miscRouter = createTRPCRouter({
|
||||
turnstileToken: z.string().min(1, "Please complete the security check"),
|
||||
/**
|
||||
* Per-site subject prefix injected into the outbound email subject
|
||||
* (task 09). Defaults to `"freno.me"` so existing callers (pre-task-09
|
||||
* Defaults to `"freno.me"` so existing callers
|
||||
* main-site contact form) keep emitting the byte-identical legacy
|
||||
* subject `"freno.me Contact Request"`.
|
||||
*/
|
||||
@@ -499,7 +499,7 @@ export const miscRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
/** Product discriminator (task 11) — defaults to "lineage" for backward compat. */
|
||||
/** Product discriminator — defaults to "lineage" for backward compat. */
|
||||
product: DELETION_PRODUCT_SCHEMA.default("lineage")
|
||||
})
|
||||
)
|
||||
|
||||
@@ -65,9 +65,15 @@ function initSchema() {
|
||||
db = new Database(":memory:");
|
||||
db.run("PRAGMA foreign_keys = ON");
|
||||
|
||||
db.run("CREATE TABLE clubMemberships (id TEXT PRIMARY KEY, clubId TEXT, userId TEXT, role TEXT, joinedAt TEXT)");
|
||||
db.run("CREATE TABLE clubPosts (id TEXT PRIMARY KEY, clubId TEXT, userId TEXT, content TEXT, postType TEXT, challengeId TEXT, createdAt TEXT, updatedAt TEXT)");
|
||||
db.run("CREATE TABLE clubChallenges (id TEXT PRIMARY KEY, clubId TEXT, title TEXT, description TEXT, goalType TEXT, goalValue REAL, startDate TEXT, endDate TEXT, createdBy TEXT, status TEXT, createdAt TEXT, updatedAt TEXT)");
|
||||
db.run(
|
||||
"CREATE TABLE clubMemberships (id TEXT PRIMARY KEY, clubId TEXT, userId TEXT, role TEXT, joinedAt TEXT)"
|
||||
);
|
||||
db.run(
|
||||
"CREATE TABLE clubPosts (id TEXT PRIMARY KEY, clubId TEXT, userId TEXT, content TEXT, postType TEXT, challengeId TEXT, createdAt TEXT, updatedAt TEXT)"
|
||||
);
|
||||
db.run(
|
||||
"CREATE TABLE clubChallenges (id TEXT PRIMARY KEY, clubId TEXT, title TEXT, description TEXT, goalType TEXT, goalValue REAL, startDate TEXT, endDate TEXT, createdBy TEXT, status TEXT, createdAt TEXT, updatedAt TEXT)"
|
||||
);
|
||||
}
|
||||
|
||||
function seed() {
|
||||
@@ -86,7 +92,17 @@ function seed() {
|
||||
// Challenge CH in club C, created by A.
|
||||
db.run(
|
||||
"INSERT INTO clubChallenges (id, clubId, title, description, goalType, goalValue, startDate, endDate, createdBy, status, createdAt, updatedAt) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))",
|
||||
[CHALLENGE_CH, CLUB_C, "Run 5k", "distance", 5000, "2025-01-01", "2025-12-31", USER_A, "active"]
|
||||
[
|
||||
CHALLENGE_CH,
|
||||
CLUB_C,
|
||||
"Run 5k",
|
||||
"distance",
|
||||
5000,
|
||||
"2025-01-01",
|
||||
"2025-12-31",
|
||||
USER_A,
|
||||
"active"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,7 +142,9 @@ describe("p8-003: resolveClubIdFromPost", () => {
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND for a missing post", async () => {
|
||||
expect(await errCode(resolveClubIdFromPost(conn, "no-such-post"))).toBe("NOT_FOUND");
|
||||
expect(await errCode(resolveClubIdFromPost(conn, "no-such-post"))).toBe(
|
||||
"NOT_FOUND"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,17 +154,23 @@ describe("p8-003: resolveClubIdFromChallenge", () => {
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND for a missing challenge", async () => {
|
||||
expect(await errCode(resolveClubIdFromChallenge(conn, "no-such-challenge"))).toBe("NOT_FOUND");
|
||||
expect(
|
||||
await errCode(resolveClubIdFromChallenge(conn, "no-such-challenge"))
|
||||
).toBe("NOT_FOUND");
|
||||
});
|
||||
});
|
||||
|
||||
describe("p8-003: requireClubMembership", () => {
|
||||
it("passes silently for a member", async () => {
|
||||
await expect(requireClubMembership(conn, CLUB_C, USER_A)).resolves.toBeUndefined();
|
||||
await expect(
|
||||
requireClubMembership(conn, CLUB_C, USER_A)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws FORBIDDEN for a non-member", async () => {
|
||||
expect(await errCode(requireClubMembership(conn, CLUB_C, USER_B))).toBe("FORBIDDEN");
|
||||
expect(await errCode(requireClubMembership(conn, CLUB_C, USER_B))).toBe(
|
||||
"FORBIDDEN"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,23 +184,31 @@ describe("p8-003: endpoint authorization sequences (resolve → require)", () =>
|
||||
// social.getPost / addComment / comments / like / unlike
|
||||
it("getPost/addComment/comments/like/unlike: non-member B rejected with FORBIDDEN", async () => {
|
||||
const clubId = await resolveClubIdFromPost(conn, POST_P);
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe("FORBIDDEN");
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe(
|
||||
"FORBIDDEN"
|
||||
);
|
||||
});
|
||||
|
||||
it("getPost/addComment/comments/like/unlike: member A allowed", async () => {
|
||||
const clubId = await resolveClubIdFromPost(conn, POST_P);
|
||||
await expect(requireClubMembership(conn, clubId, USER_A)).resolves.toBeUndefined();
|
||||
await expect(
|
||||
requireClubMembership(conn, clubId, USER_A)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
// challenges.leave / challenges.submitProgress
|
||||
it("challenges.leave / submitProgress: non-member B rejected with FORBIDDEN", async () => {
|
||||
const clubId = await resolveClubIdFromChallenge(conn, CHALLENGE_CH);
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe("FORBIDDEN");
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe(
|
||||
"FORBIDDEN"
|
||||
);
|
||||
});
|
||||
|
||||
it("challenges.leave / submitProgress: member A allowed", async () => {
|
||||
const clubId = await resolveClubIdFromChallenge(conn, CHALLENGE_CH);
|
||||
await expect(requireClubMembership(conn, clubId, USER_A)).resolves.toBeUndefined();
|
||||
await expect(
|
||||
requireClubMembership(conn, clubId, USER_A)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,21 +216,27 @@ describe("p8-003: join then allowed / leave then blocked (integration)", () => {
|
||||
it("B is blocked, allowed after joining C, blocked again after leaving", async () => {
|
||||
// Initially blocked.
|
||||
const clubId = await resolveClubIdFromPost(conn, POST_P);
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe("FORBIDDEN");
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe(
|
||||
"FORBIDDEN"
|
||||
);
|
||||
|
||||
// B joins.
|
||||
db.run(
|
||||
"INSERT INTO clubMemberships (id, clubId, userId, role, joinedAt) VALUES (?, ?, ?, ?, datetime('now'))",
|
||||
["mem-b", CLUB_C, USER_B, "member"]
|
||||
);
|
||||
await expect(requireClubMembership(conn, clubId, USER_B)).resolves.toBeUndefined();
|
||||
await expect(
|
||||
requireClubMembership(conn, clubId, USER_B)
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
// B leaves.
|
||||
db.run("DELETE FROM clubMemberships WHERE clubId = ? AND userId = ?", [
|
||||
CLUB_C,
|
||||
USER_B
|
||||
]);
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe("FORBIDDEN");
|
||||
expect(await errCode(requireClubMembership(conn, clubId, USER_B))).toBe(
|
||||
"FORBIDDEN"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,7 +262,9 @@ function initUsersTable() {
|
||||
email TEXT,
|
||||
clerkUserId TEXT
|
||||
)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_users_clerkUserId ON users(clerkUserId)`);
|
||||
db.run(
|
||||
`CREATE INDEX IF NOT EXISTS idx_users_clerkUserId ON users(clerkUserId)`
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveLocalUserId(clerkUserId: string): Promise<string | null> {
|
||||
@@ -246,31 +286,34 @@ describe("clerkUserId lookup (migrate-to-clerk-auth-03)", () => {
|
||||
});
|
||||
|
||||
it("resolves local users.id for a seeded clerkUserId", async () => {
|
||||
db.run(
|
||||
"INSERT INTO users (id, email, clerkUserId) VALUES (?, ?, ?)",
|
||||
[LOCAL_USER_A, "a@nessa.app", CLERK_USER_ID]
|
||||
);
|
||||
db.run("INSERT INTO users (id, email, clerkUserId) VALUES (?, ?, ?)", [
|
||||
LOCAL_USER_A,
|
||||
"a@nessa.app",
|
||||
CLERK_USER_ID
|
||||
]);
|
||||
expect(await resolveLocalUserId(CLERK_USER_ID)).toBe(LOCAL_USER_A);
|
||||
});
|
||||
|
||||
it("returns null when no local row matches the clerkUserId", async () => {
|
||||
// No users seeded — the webhook (task 04) has not run yet.
|
||||
// No users seeded — the webhook has not run yet.
|
||||
expect(await resolveLocalUserId(CLERK_USER_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a Clerk id that exists but maps to a different local user", async () => {
|
||||
db.run(
|
||||
"INSERT INTO users (id, email, clerkUserId) VALUES (?, ?, ?)",
|
||||
[LOCAL_USER_B, "b@nessa.app", "user_test_other"]
|
||||
);
|
||||
db.run("INSERT INTO users (id, email, clerkUserId) VALUES (?, ?, ?)", [
|
||||
LOCAL_USER_B,
|
||||
"b@nessa.app",
|
||||
"user_test_other"
|
||||
]);
|
||||
expect(await resolveLocalUserId(CLERK_USER_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it("ctx.nessaUserId is the LOCAL id, never the Clerk sub", async () => {
|
||||
db.run(
|
||||
"INSERT INTO users (id, email, clerkUserId) VALUES (?, ?, ?)",
|
||||
[LOCAL_USER_A, "a@nessa.app", CLERK_USER_ID]
|
||||
);
|
||||
db.run("INSERT INTO users (id, email, clerkUserId) VALUES (?, ?, ?)", [
|
||||
LOCAL_USER_A,
|
||||
"a@nessa.app",
|
||||
CLERK_USER_ID
|
||||
]);
|
||||
const resolved = await resolveLocalUserId(CLERK_USER_ID);
|
||||
expect(resolved).toBe(LOCAL_USER_A);
|
||||
expect(resolved).not.toBe(CLERK_USER_ID);
|
||||
|
||||
@@ -23,7 +23,7 @@ mock.module("~/env/server", () => ({
|
||||
TURSO_LINEAGE_TOKEN: "test-token",
|
||||
TURSO_DB_API_TOKEN: "test-token",
|
||||
NODE_ENV: "test",
|
||||
// Clerk env vars (required after migration in task 02)
|
||||
// Clerk env vars (required after migration)
|
||||
NESSA_CLERK_SECRET: "sk_test_test-secret",
|
||||
NESSA_CLERK_JWT_ISSUER: "https://nessa-test.clerk.accounts.dev"
|
||||
},
|
||||
@@ -55,7 +55,11 @@ const PROVIDER_ID = "prov-1";
|
||||
// create/update/deleteWorkoutSplit
|
||||
|
||||
describe("assertWorkoutOwned helper", () => {
|
||||
let assertWorkoutOwned: (conn: Client, workoutId: string, userId: string) => Promise<void>;
|
||||
let assertWorkoutOwned: (
|
||||
conn: Client,
|
||||
workoutId: string,
|
||||
userId: string
|
||||
) => Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mod = await import("./nessa");
|
||||
@@ -64,16 +68,16 @@ describe("assertWorkoutOwned helper", () => {
|
||||
|
||||
it("rejects when workout belongs to another user", async () => {
|
||||
const conn = makeMockConn([{ userId: USER_B }]);
|
||||
await expect(
|
||||
assertWorkoutOwned(conn, WORKOUT_ID, USER_A)
|
||||
).rejects.toThrow(/owner/);
|
||||
await expect(assertWorkoutOwned(conn, WORKOUT_ID, USER_A)).rejects.toThrow(
|
||||
/owner/
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects when workout does not exist", async () => {
|
||||
const conn = makeMockConn([]);
|
||||
await expect(
|
||||
assertWorkoutOwned(conn, WORKOUT_ID, USER_A)
|
||||
).rejects.toThrow(/not found/i);
|
||||
await expect(assertWorkoutOwned(conn, WORKOUT_ID, USER_A)).rejects.toThrow(
|
||||
/not found/i
|
||||
);
|
||||
});
|
||||
|
||||
it("succeeds when workout belongs to the caller", async () => {
|
||||
@@ -88,7 +92,11 @@ describe("assertWorkoutOwned helper", () => {
|
||||
// Used by: updateAuthProvider, deleteAuthProvider
|
||||
|
||||
describe("assertAuthProviderOwned helper", () => {
|
||||
let assertAuthProviderOwned: (conn: Client, providerId: string, userId: string) => Promise<void>;
|
||||
let assertAuthProviderOwned: (
|
||||
conn: Client,
|
||||
providerId: string,
|
||||
userId: string
|
||||
) => Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mod = await import("./nessa");
|
||||
@@ -121,7 +129,11 @@ describe("assertAuthProviderOwned helper", () => {
|
||||
// Used by: updateExerciseLibrary, deleteExerciseLibrary
|
||||
|
||||
describe("assertExerciseLibraryOwned helper", () => {
|
||||
let assertExerciseLibraryOwned: (conn: Client, exerciseId: string, userId: string) => Promise<void>;
|
||||
let assertExerciseLibraryOwned: (
|
||||
conn: Client,
|
||||
exerciseId: string,
|
||||
userId: string
|
||||
) => Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mod = await import("./nessa");
|
||||
@@ -273,9 +285,7 @@ describe("static audit: every targeted mutation handler uses ctx", () => {
|
||||
];
|
||||
|
||||
it("no mutation handler in the list uses async ({ input }) without ctx", async () => {
|
||||
const source = await Bun.file(
|
||||
import.meta.dir + "/nessa.ts"
|
||||
).text();
|
||||
const source = await Bun.file(import.meta.dir + "/nessa.ts").text();
|
||||
|
||||
for (const name of MUTATIONS) {
|
||||
// Match: name: nessaProcedure ... .mutation(async ({ input }) — but NOT ({ input, ctx
|
||||
@@ -284,14 +294,15 @@ describe("static audit: every targeted mutation handler uses ctx", () => {
|
||||
"s"
|
||||
);
|
||||
const match = source.match(re);
|
||||
expect(match, `${name} should not use async ({ input }) — must use ctx`).toBeNull();
|
||||
expect(
|
||||
match,
|
||||
`${name} should not use async ({ input }) — must use ctx`
|
||||
).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("every mutation handler in the list references ctx", async () => {
|
||||
const source = await Bun.file(
|
||||
import.meta.dir + "/nessa.ts"
|
||||
).text();
|
||||
const source = await Bun.file(import.meta.dir + "/nessa.ts").text();
|
||||
|
||||
for (const name of MUTATIONS) {
|
||||
// Find the block for this mutation and check it references ctx
|
||||
@@ -301,19 +312,16 @@ describe("static audit: every targeted mutation handler uses ctx", () => {
|
||||
);
|
||||
const match = source.match(re);
|
||||
expect(match, `${name} mutation block not found`).toBeTruthy();
|
||||
expect(
|
||||
match![0].includes("ctx"),
|
||||
`${name} must reference ctx`
|
||||
).toBe(true);
|
||||
expect(match![0].includes("ctx"), `${name} must reference ctx`).toBe(
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("bulkUpsert filters exerciseLibrary by userId", async () => {
|
||||
const source = await Bun.file(
|
||||
import.meta.dir + "/nessa.ts"
|
||||
).text();
|
||||
const source = await Bun.file(import.meta.dir + "/nessa.ts").text();
|
||||
const bulkSection = source.match(
|
||||
/if \(input\.exerciseLibrary\?\.length\) \{[\s\S]*?\n \}/
|
||||
/if \(input\.exerciseLibrary\?\.length\) \{[\s\S]*?\n {8}\}/
|
||||
);
|
||||
expect(bulkSection).toBeTruthy();
|
||||
expect(bulkSection![0]).toContain("userId !== ctx.nessaUserId");
|
||||
|
||||
Reference in New Issue
Block a user