test(backend): cover phase 5 and 6 migrations
This commit is contained in:
+5
-1
@@ -17,7 +17,11 @@
|
|||||||
"phase2:domain-smoke": "node scripts/phase2-domain-smoke.mjs",
|
"phase2:domain-smoke": "node scripts/phase2-domain-smoke.mjs",
|
||||||
"phase3:storage-smoke": "node scripts/phase3-storage-smoke.mjs",
|
"phase3:storage-smoke": "node scripts/phase3-storage-smoke.mjs",
|
||||||
"phase2:smoke": "node scripts/phase2-auth-smoke.mjs",
|
"phase2:smoke": "node scripts/phase2-auth-smoke.mjs",
|
||||||
"phase4:ui-boundary": "node scripts/phase4-ui-boundary.mjs"
|
"phase4:ui-boundary": "node scripts/phase4-ui-boundary.mjs",
|
||||||
|
"phase5:backend-boundary": "node scripts/phase5-backend-boundary.mjs",
|
||||||
|
"phase5:smoke": "node scripts/phase5-freelancer-smoke.mjs",
|
||||||
|
"phase6:portal-boundary": "node scripts/phase6-portal-boundary.mjs",
|
||||||
|
"phase6:smoke": "node scripts/phase6-portal-smoke.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/google": "^3.0.80",
|
"@ai-sdk/google": "^3.0.80",
|
||||||
|
|||||||
@@ -96,6 +96,19 @@ try {
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
"insert into projects (id, owner_user_id, client_id, name, status) values (?, ?, ?, ?, ?)",
|
"insert into projects (id, owner_user_id, client_id, name, status) values (?, ?, ?, ?, ?)",
|
||||||
).run("project-alpha", ownerUserId, "client-alpha", "Alpha Project", "active");
|
).run("project-alpha", ownerUserId, "client-alpha", "Alpha Project", "active");
|
||||||
|
db.prepare("update projects set revision_quota = ? where id = ?").run(2, "project-alpha");
|
||||||
|
db.prepare(
|
||||||
|
"insert into projects (id, owner_user_id, client_id, name, status) values (?, ?, ?, ?, ?)",
|
||||||
|
).run("project-foreign", ownerUserId, "client-expired", "Foreign Project", "active");
|
||||||
|
db.prepare(
|
||||||
|
"insert into tasks (id, owner_user_id, client_id, project_id, title, status, priority, is_public_to_client) values (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
).run("task-portal-public", ownerUserId, "client-alpha", "project-alpha", "Portal Public Task", "todo", "medium", 1);
|
||||||
|
db.prepare(
|
||||||
|
"insert into tasks (id, owner_user_id, client_id, project_id, title, status, priority, is_public_to_client) values (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
).run("task-portal-private", ownerUserId, "client-alpha", "project-alpha", "Portal Private Task", "todo", "medium", 0);
|
||||||
|
db.prepare(
|
||||||
|
"insert into project_planning_sections (id, owner_user_id, project_id, category, title, content, metadata, sort_order) values (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
).run("planning-portal", ownerUserId, "project-alpha", "overview", "Portal Plan", "Visible planning content", "{}", 0);
|
||||||
|
|
||||||
const rejectedRegistration = await authPost("/api/auth/sign-up/email", {
|
const rejectedRegistration = await authPost("/api/auth/sign-up/email", {
|
||||||
name: "Public Attacker",
|
name: "Public Attacker",
|
||||||
@@ -116,6 +129,27 @@ try {
|
|||||||
}
|
}
|
||||||
assert.ok(ownerCookie, "Owner session cookie must be issued");
|
assert.ok(ownerCookie, "Owner session cookie must be issued");
|
||||||
|
|
||||||
|
for (const pathname of [
|
||||||
|
"/",
|
||||||
|
"/clients",
|
||||||
|
"/clients/client-alpha",
|
||||||
|
"/projects",
|
||||||
|
"/projects/project-alpha",
|
||||||
|
"/tasks",
|
||||||
|
"/calendar",
|
||||||
|
"/finance",
|
||||||
|
"/journal",
|
||||||
|
"/analytics",
|
||||||
|
"/settings",
|
||||||
|
]) {
|
||||||
|
const page = await fetch(`${baseUrl}${pathname}`, {
|
||||||
|
headers: { cookie: ownerCookie },
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
assert.equal(page.status, 200, `Freelancer SSR route failed: ${pathname}`);
|
||||||
|
assert.doesNotMatch(await page.text(), /lib\/supabase|supabase\.co/i, `SSR output leaked Supabase: ${pathname}`);
|
||||||
|
}
|
||||||
|
|
||||||
const anonymousUpload = await uploadFile("avatar", { fileName: "anonymous.png" });
|
const anonymousUpload = await uploadFile("avatar", { fileName: "anonymous.png" });
|
||||||
assert.equal(anonymousUpload.response.status, 401, "Anonymous file upload must fail");
|
assert.equal(anonymousUpload.response.status, 401, "Anonymous file upload must fail");
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -241,6 +275,17 @@ try {
|
|||||||
"Client-Password-123",
|
"Client-Password-123",
|
||||||
"Client password must be hashed",
|
"Client password must be hashed",
|
||||||
);
|
);
|
||||||
|
db.prepare(
|
||||||
|
"insert into project_revisions (id, owner_user_id, project_id, client_id, requested_by_user_id, description, status) values (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
).run(
|
||||||
|
"revision-portal",
|
||||||
|
ownerUserId,
|
||||||
|
"project-alpha",
|
||||||
|
"client-alpha",
|
||||||
|
clientAuthUserId,
|
||||||
|
"Portal Revision Request",
|
||||||
|
"pending",
|
||||||
|
);
|
||||||
|
|
||||||
const replayed = await acceptInvite(rawClientToken);
|
const replayed = await acceptInvite(rawClientToken);
|
||||||
assert.equal(replayed.response.status, 409, "Accepted invitation must be single-use");
|
assert.equal(replayed.response.status, 409, "Accepted invitation must be single-use");
|
||||||
@@ -252,6 +297,39 @@ try {
|
|||||||
assert.equal(clientSignIn.response.ok, true, JSON.stringify(clientSignIn.payload));
|
assert.equal(clientSignIn.response.ok, true, JSON.stringify(clientSignIn.payload));
|
||||||
const clientCookie = cookieHeader(clientSignIn.response);
|
const clientCookie = cookieHeader(clientSignIn.response);
|
||||||
|
|
||||||
|
for (const pathname of [
|
||||||
|
"/portal",
|
||||||
|
"/portal/projects",
|
||||||
|
"/portal/projects/project-alpha",
|
||||||
|
"/portal/tasks",
|
||||||
|
"/portal/revisions",
|
||||||
|
]) {
|
||||||
|
const page = await fetch(`${baseUrl}${pathname}`, {
|
||||||
|
headers: { cookie: clientCookie },
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
assert.equal(page.status, 200, `Portal SSR route failed: ${pathname}`);
|
||||||
|
const html = await page.text();
|
||||||
|
assert.doesNotMatch(html, /lib\/supabase|supabase\.co/i, `Portal SSR output leaked Supabase: ${pathname}`);
|
||||||
|
if (pathname === "/portal") assert.match(html, /Neta Smoke Studio/, "Portal must render local branding");
|
||||||
|
if (pathname === "/portal/tasks" || pathname === "/portal/projects/project-alpha") {
|
||||||
|
assert.match(html, /Portal Public Task/, `Public task missing from ${pathname}`);
|
||||||
|
assert.doesNotMatch(html, /Portal Private Task/, `Private task leaked from ${pathname}`);
|
||||||
|
}
|
||||||
|
if (pathname === "/portal/projects/project-alpha") {
|
||||||
|
assert.match(html, /Visible planning content/, "Portal planning section must be visible");
|
||||||
|
}
|
||||||
|
if (pathname === "/portal/revisions") {
|
||||||
|
assert.match(html, /Portal Revision Request/, "Portal revision history must be visible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const foreignProject = await fetch(`${baseUrl}/portal/projects/project-foreign`, {
|
||||||
|
headers: { cookie: clientCookie },
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
assert.equal(foreignProject.status, 404, "Client must not open another client's project");
|
||||||
|
|
||||||
const clientPortalAsset = await fetch(`${baseUrl}/api/files/${portalAssetFileId}`, {
|
const clientPortalAsset = await fetch(`${baseUrl}/api/files/${portalAssetFileId}`, {
|
||||||
headers: { cookie: clientCookie },
|
headers: { cookie: clientCookie },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const ownerOne: DomainActor = { authUserId: "owner-1", role: "freelancer", clien
|
|||||||
const ownerTwo: DomainActor = { authUserId: "owner-2", role: "freelancer", clientId: null, disabled: false };
|
const ownerTwo: DomainActor = { authUserId: "owner-2", role: "freelancer", clientId: null, disabled: false };
|
||||||
const clientOne: DomainActor = { authUserId: "client-user-1", role: "client", clientId: "client-1", disabled: false };
|
const clientOne: DomainActor = { authUserId: "client-user-1", role: "client", clientId: "client-1", disabled: false };
|
||||||
const clientTwo: DomainActor = { authUserId: "client-user-2", role: "client", clientId: "client-2", disabled: false };
|
const clientTwo: DomainActor = { authUserId: "client-user-2", role: "client", clientId: "client-2", disabled: false };
|
||||||
|
const spoofedClient: DomainActor = { authUserId: "client-user-2", role: "client", clientId: "client-1", disabled: false };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const actor of [ownerOne, ownerTwo, clientOne, clientTwo]) {
|
for (const actor of [ownerOne, ownerTwo, clientOne, clientTwo]) {
|
||||||
@@ -58,6 +59,22 @@ try {
|
|||||||
});
|
});
|
||||||
service.createProject(ownerOne, { id: "project-2", name: "Second Client", clientId: "client-2" });
|
service.createProject(ownerOne, { id: "project-2", name: "Second Client", clientId: "client-2" });
|
||||||
service.createProject(ownerTwo, { id: "project-other", name: "Other Project", clientId: "client-other" });
|
service.createProject(ownerTwo, { id: "project-other", name: "Other Project", clientId: "client-other" });
|
||||||
|
assert.deepEqual(service.listProjects(clientOne).map((project) => project.id), ["project-1"]);
|
||||||
|
assert.deepEqual(service.listProjects(spoofedClient), []);
|
||||||
|
assertDomainError(() => service.getProject(spoofedClient, "project-1"), "NOT_FOUND");
|
||||||
|
service.addClientActivity(ownerOne, {
|
||||||
|
id: "activity-1",
|
||||||
|
clientId: "client-1",
|
||||||
|
type: "meeting",
|
||||||
|
title: "Kickoff",
|
||||||
|
activityDate: new Date("2026-07-15T09:00:00.000Z"),
|
||||||
|
});
|
||||||
|
assert.deepEqual(service.listClientActivities(ownerOne, "client-1").map((item) => item.id), ["activity-1"]);
|
||||||
|
assert.deepEqual(service.listAllClientActivities(ownerOne).map((item) => item.id), ["activity-1"]);
|
||||||
|
assertDomainError(() => service.listClientActivities(ownerTwo, "client-1"), "NOT_FOUND");
|
||||||
|
service.updateClient(ownerOne, "client-1", { pipelineStage: "contacted" });
|
||||||
|
service.updateClient(ownerOne, "client-1", { status: "paused" });
|
||||||
|
assert.equal(service.getClient(ownerOne, "client-1").pipelineStage, "contacted");
|
||||||
|
|
||||||
service.createTask(ownerOne, {
|
service.createTask(ownerOne, {
|
||||||
id: "task-public",
|
id: "task-public",
|
||||||
@@ -78,11 +95,32 @@ try {
|
|||||||
assert.equal(service.getProject(ownerOne, "project-1").progress, 50, "Auto progress must aggregate active tasks");
|
assert.equal(service.getProject(ownerOne, "project-1").progress, 50, "Auto progress must aggregate active tasks");
|
||||||
assert.deepEqual(service.listTasks(clientOne).map((task) => task.id), ["task-public"]);
|
assert.deepEqual(service.listTasks(clientOne).map((task) => task.id), ["task-public"]);
|
||||||
assertDomainError(() => service.getProject(clientOne, "project-2"), "NOT_FOUND");
|
assertDomainError(() => service.getProject(clientOne, "project-2"), "NOT_FOUND");
|
||||||
|
assertDomainError(() => service.listTasks(clientOne, "project-2"), "NOT_FOUND");
|
||||||
|
assertDomainError(() => service.listTasks(spoofedClient), "NOT_FOUND");
|
||||||
assertDomainError(() => service.listFinanceTransactions(clientOne), "FORBIDDEN");
|
assertDomainError(() => service.listFinanceTransactions(clientOne), "FORBIDDEN");
|
||||||
assertDomainError(() => service.updateTask(ownerTwo, "task-public", { status: "done" }), "NOT_FOUND");
|
assertDomainError(() => service.updateTask(ownerTwo, "task-public", { status: "done" }), "NOT_FOUND");
|
||||||
|
|
||||||
service.updateTask(ownerOne, "task-private", { status: "done" });
|
service.updateTask(ownerOne, "task-private", { status: "done" });
|
||||||
|
service.updateTask(ownerOne, "task-public", { description: "Patch without default resets" });
|
||||||
|
assert.equal(service.listTasks(clientOne)[0]?.isPublicToClient, true);
|
||||||
assert.equal(service.getProject(ownerOne, "project-1").progress, 100);
|
assert.equal(service.getProject(ownerOne, "project-1").progress, 100);
|
||||||
|
service.updateProject(ownerOne, "project-1", { progress: 10 });
|
||||||
|
assert.equal(service.getProject(ownerOne, "project-1").progress, 100, "Auto progress must ignore manual overwrite");
|
||||||
|
|
||||||
|
service.createCalendarEvent(ownerOne, {
|
||||||
|
id: "calendar-1",
|
||||||
|
clientId: "client-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
taskId: "task-public",
|
||||||
|
title: "Review",
|
||||||
|
type: "meeting",
|
||||||
|
startsAt: new Date("2026-07-16T10:00:00.000Z"),
|
||||||
|
});
|
||||||
|
assert.equal(service.listCalendarEvents(ownerOne)[0]?.title, "Review");
|
||||||
|
service.updateCalendarEvent(ownerOne, "calendar-1", { title: "Final review" });
|
||||||
|
assert.equal(service.listCalendarEvents(ownerOne)[0]?.title, "Final review");
|
||||||
|
assert.equal(service.listCalendarEvents(ownerOne)[0]?.type, "meeting");
|
||||||
|
assertDomainError(() => service.deleteCalendarEvent(ownerTwo, "calendar-1"), "NOT_FOUND");
|
||||||
|
|
||||||
service.addPlanningSection(ownerOne, {
|
service.addPlanningSection(ownerOne, {
|
||||||
id: "planning-1",
|
id: "planning-1",
|
||||||
@@ -90,11 +128,26 @@ try {
|
|||||||
category: "overview",
|
category: "overview",
|
||||||
title: "Overview",
|
title: "Overview",
|
||||||
content: "Visible project context",
|
content: "Visible project context",
|
||||||
|
sortOrder: 3,
|
||||||
});
|
});
|
||||||
|
service.updatePlanningSection(ownerOne, "planning-1", { title: "Updated overview" });
|
||||||
|
assert.equal(service.listPlanningSections(ownerOne, "project-1")[0]?.sortOrder, 3);
|
||||||
assert.equal(service.listPlanningSections(clientOne, "project-1").length, 1);
|
assert.equal(service.listPlanningSections(clientOne, "project-1").length, 1);
|
||||||
assertDomainError(() => service.listPlanningSections(clientTwo, "project-1"), "NOT_FOUND");
|
assertDomainError(() => service.listPlanningSections(clientTwo, "project-1"), "NOT_FOUND");
|
||||||
|
|
||||||
|
assert.deepEqual(service.getRevisionAllowance(clientOne, "project-1"), {
|
||||||
|
quota: 1,
|
||||||
|
used: 0,
|
||||||
|
remaining: 1,
|
||||||
|
canRequest: true,
|
||||||
|
});
|
||||||
assert.equal(service.requestRevision(clientOne, { id: "revision-1", projectId: "project-1", description: "Please revise" }).status, "pending");
|
assert.equal(service.requestRevision(clientOne, { id: "revision-1", projectId: "project-1", description: "Please revise" }).status, "pending");
|
||||||
|
assert.deepEqual(service.getRevisionAllowance(clientOne, "project-1"), {
|
||||||
|
quota: 1,
|
||||||
|
used: 1,
|
||||||
|
remaining: 0,
|
||||||
|
canRequest: false,
|
||||||
|
});
|
||||||
assertDomainError(
|
assertDomainError(
|
||||||
() => service.requestRevision(clientOne, { id: "revision-2", projectId: "project-1", description: "Quota overflow" }),
|
() => service.requestRevision(clientOne, { id: "revision-2", projectId: "project-1", description: "Quota overflow" }),
|
||||||
"CONFLICT",
|
"CONFLICT",
|
||||||
@@ -103,8 +156,22 @@ try {
|
|||||||
() => service.requestRevision(clientTwo, { id: "revision-3", projectId: "project-1", description: "Wrong client" }),
|
() => service.requestRevision(clientTwo, { id: "revision-3", projectId: "project-1", description: "Wrong client" }),
|
||||||
"NOT_FOUND",
|
"NOT_FOUND",
|
||||||
);
|
);
|
||||||
assert.equal(service.updateRevisionStatus(ownerOne, "revision-1", "completed").status, "completed");
|
assertDomainError(
|
||||||
|
() => service.requestRevision(spoofedClient, { id: "revision-spoof", projectId: "project-1", description: "Spoofed link" }),
|
||||||
|
"NOT_FOUND",
|
||||||
|
);
|
||||||
|
assertDomainError(
|
||||||
|
() => service.requestRevision(clientTwo, { id: "revision-inactive", projectId: "project-2", description: "Inactive project" }),
|
||||||
|
"INVARIANT_VIOLATION",
|
||||||
|
);
|
||||||
|
assertDomainError(
|
||||||
|
() => service.updateRevisionStatus(ownerOne, "revision-1", "completed", "project-2"),
|
||||||
|
"NOT_FOUND",
|
||||||
|
);
|
||||||
|
assert.equal(service.updateRevisionStatus(ownerOne, "revision-1", "completed", "project-1").status, "completed");
|
||||||
assert.deepEqual(service.listRevisions(clientOne, "project-1").map((revision) => revision.id), ["revision-1"]);
|
assert.deepEqual(service.listRevisions(clientOne, "project-1").map((revision) => revision.id), ["revision-1"]);
|
||||||
|
assert.deepEqual(service.listPortalRevisions(clientOne).map((revision) => revision.id), ["revision-1"]);
|
||||||
|
assertDomainError(() => service.listRevisions(clientTwo, "project-1"), "NOT_FOUND");
|
||||||
|
|
||||||
service.createFinanceTransaction(ownerOne, {
|
service.createFinanceTransaction(ownerOne, {
|
||||||
id: "income-1", type: "income", amountMinor: 150_00, currency: "try", transactionDate: "2026-07-16", paymentStatus: "paid",
|
id: "income-1", type: "income", amountMinor: 150_00, currency: "try", transactionDate: "2026-07-16", paymentStatus: "paid",
|
||||||
@@ -124,11 +191,33 @@ try {
|
|||||||
plannedMinor: 75_00,
|
plannedMinor: 75_00,
|
||||||
netMinor: 110_00,
|
netMinor: 110_00,
|
||||||
});
|
});
|
||||||
|
service.createFinanceTransaction(ownerOne, {
|
||||||
|
id: "project-income",
|
||||||
|
clientId: "client-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
type: "income",
|
||||||
|
amountMinor: 10_00,
|
||||||
|
currency: "TRY",
|
||||||
|
transactionDate: "2026-07-16",
|
||||||
|
paymentStatus: "paid",
|
||||||
|
});
|
||||||
|
service.updateFinanceTransaction(ownerOne, "project-income", { description: "Patched" });
|
||||||
|
assert.equal(
|
||||||
|
service.listFinanceTransactions(ownerOne).find((item) => item.id === "project-income")?.paymentStatus,
|
||||||
|
"paid",
|
||||||
|
);
|
||||||
|
|
||||||
service.saveJournalEntry(ownerOne, { id: "journal-1", entryDate: "2026-07-16", moodScore: 3, note: "First" });
|
service.saveJournalEntry(ownerOne, { id: "journal-1", entryDate: "2026-07-16", moodScore: 3, note: "First" });
|
||||||
service.saveJournalEntry(ownerOne, { entryDate: "2026-07-16", moodScore: 5, note: "Updated" });
|
service.saveJournalEntry(ownerOne, { entryDate: "2026-07-16", moodScore: 5, note: "Updated" });
|
||||||
assert.equal(service.listJournalEntries(ownerOne).length, 1, "Journal date must upsert per owner");
|
assert.equal(service.listJournalEntries(ownerOne).length, 1, "Journal date must upsert per owner");
|
||||||
assert.equal(service.listJournalEntries(ownerOne)[0]?.moodScore, 5);
|
assert.equal(service.listJournalEntries(ownerOne)[0]?.moodScore, 5);
|
||||||
|
service.updateJournalEntry(ownerOne, "journal-1", {
|
||||||
|
entryDate: "2026-07-16",
|
||||||
|
moodScore: 4,
|
||||||
|
energyScore: 3,
|
||||||
|
note: "Edited",
|
||||||
|
});
|
||||||
|
assert.equal(service.listJournalEntries(ownerOne)[0]?.note, "Edited");
|
||||||
assertDomainError(
|
assertDomainError(
|
||||||
() => service.createTask(ownerTwo, { title: "Foreign journal", sourceJournalEntryId: "journal-1" }),
|
() => service.createTask(ownerTwo, { title: "Foreign journal", sourceJournalEntryId: "journal-1" }),
|
||||||
"NOT_FOUND",
|
"NOT_FOUND",
|
||||||
@@ -143,6 +232,23 @@ try {
|
|||||||
service.createInvoice(ownerOne, { id: "invoice-1", clientId: "client-1", projectId: "project-1", invoiceNumber: "INV-001", amountMinor: 100_00, issueDate: "2026-07-16" });
|
service.createInvoice(ownerOne, { id: "invoice-1", clientId: "client-1", projectId: "project-1", invoiceNumber: "INV-001", amountMinor: 100_00, issueDate: "2026-07-16" });
|
||||||
service.createSubscription(ownerOne, { id: "subscription-1", name: "Hosting", amountMinor: 500_00 });
|
service.createSubscription(ownerOne, { id: "subscription-1", name: "Hosting", amountMinor: 500_00 });
|
||||||
|
|
||||||
|
const analyticsRange = {
|
||||||
|
startDate: "2026-01-01",
|
||||||
|
endDate: "2026-12-31",
|
||||||
|
startAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||||
|
endAt: new Date("2026-12-31T23:59:59.999Z"),
|
||||||
|
};
|
||||||
|
const dashboard = service.getFreelancerDashboard(ownerOne, analyticsRange);
|
||||||
|
assert.equal(dashboard.metrics.netProfit, 120);
|
||||||
|
assert.equal(dashboard.metrics.avgMood, "4.0");
|
||||||
|
assert.deepEqual(
|
||||||
|
new Set(dashboard.projects.map((project) => project.id)),
|
||||||
|
new Set(["project-1", "project-2"]),
|
||||||
|
);
|
||||||
|
const rangedAnalytics = service.getFreelancerAnalytics(ownerOne, analyticsRange);
|
||||||
|
assert.deepEqual(rangedAnalytics.projectIncomeData, [{ name: "Client Project", value: 10 }]);
|
||||||
|
assert.equal(rangedAnalytics.completedTasks, 2);
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => sqlite.prepare("insert into finance_transactions (id, owner_user_id, type, amount_minor, currency, transaction_date, payment_status) values (?, ?, ?, ?, ?, ?, ?)").run("invalid-finance", ownerOne.authUserId, "income", -1, "TRY", "2026-07-16", "paid"),
|
() => sqlite.prepare("insert into finance_transactions (id, owner_user_id, type, amount_minor, currency, transaction_date, payment_status) values (?, ?, ?, ?, ?, ?, ?)").run("invalid-finance", ownerOne.authUserId, "income", -1, "TRY", "2026-07-16", "paid"),
|
||||||
/CHECK constraint failed/,
|
/CHECK constraint failed/,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const ownerOne: DomainActor = { authUserId: "owner-1", role: "freelancer", clien
|
|||||||
const ownerTwo: DomainActor = { authUserId: "owner-2", role: "freelancer", clientId: null, disabled: false };
|
const ownerTwo: DomainActor = { authUserId: "owner-2", role: "freelancer", clientId: null, disabled: false };
|
||||||
const clientOne: DomainActor = { authUserId: "client-user-1", role: "client", clientId: "client-1", disabled: false };
|
const clientOne: DomainActor = { authUserId: "client-user-1", role: "client", clientId: "client-1", disabled: false };
|
||||||
const clientTwo: DomainActor = { authUserId: "client-user-2", role: "client", clientId: "client-2", disabled: false };
|
const clientTwo: DomainActor = { authUserId: "client-user-2", role: "client", clientId: "client-2", disabled: false };
|
||||||
|
const spoofedClient: DomainActor = { authUserId: "client-user-2", role: "client", clientId: "client-1", disabled: false };
|
||||||
const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4]);
|
const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -104,6 +105,7 @@ try {
|
|||||||
assert.equal(fileService.read(clientOne, portalAsset.id).metadata.id, portalAsset.id);
|
assert.equal(fileService.read(clientOne, portalAsset.id).metadata.id, portalAsset.id);
|
||||||
assertDomainError(() => fileService.read(clientOne, privateAsset.id), "NOT_FOUND");
|
assertDomainError(() => fileService.read(clientOne, privateAsset.id), "NOT_FOUND");
|
||||||
assertDomainError(() => fileService.read(clientTwo, portalAsset.id), "NOT_FOUND");
|
assertDomainError(() => fileService.read(clientTwo, portalAsset.id), "NOT_FOUND");
|
||||||
|
assertDomainError(() => fileService.read(spoofedClient, portalAsset.id), "FORBIDDEN");
|
||||||
assertDomainError(
|
assertDomainError(
|
||||||
() => fileService.upload(clientOne, { ...imageInput("project_asset", "attack.png"), projectId: "project-1" }),
|
() => fileService.upload(clientOne, { ...imageInput("project_asset", "attack.png"), projectId: "project-1" }),
|
||||||
"FORBIDDEN",
|
"FORBIDDEN",
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const roots = [
|
||||||
|
"app/(dashboard)/settings",
|
||||||
|
"app/(dashboard)/clients",
|
||||||
|
"app/(dashboard)/projects",
|
||||||
|
"app/(dashboard)/tasks",
|
||||||
|
"app/(dashboard)/calendar",
|
||||||
|
"app/(dashboard)/finance",
|
||||||
|
"app/(dashboard)/journal",
|
||||||
|
];
|
||||||
|
const files = [
|
||||||
|
...roots.flatMap(walk),
|
||||||
|
"app/(dashboard)/page.tsx",
|
||||||
|
"app/(dashboard)/analytics/page.tsx",
|
||||||
|
].filter((file) => /\.(ts|tsx)$/.test(file));
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(path.join(process.cwd(), file), "utf8");
|
||||||
|
if (/[@/]lib\/supabase|createServiceRoleClient|\bsupabase\b/i.test(content)) {
|
||||||
|
violations.push(`${file}: Supabase runtime reference`);
|
||||||
|
}
|
||||||
|
if (/NEXT_PUBLIC_SUPABASE|SUPABASE_SERVICE_ROLE/.test(content)) {
|
||||||
|
violations.push(`${file}: Supabase environment dependency`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
violations,
|
||||||
|
[],
|
||||||
|
`Phase 5 freelancer backend boundary violations:\n${violations.join("\n")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const required of [
|
||||||
|
"server/web/freelancer.ts",
|
||||||
|
"server/services/analytics-range.ts",
|
||||||
|
"server/settings/ai.ts",
|
||||||
|
"server/db/migrations/0005_brief_black_bolt.sql",
|
||||||
|
]) {
|
||||||
|
assert.ok(fs.existsSync(path.join(process.cwd(), required)), `Missing Phase 5 backend artifact: ${required}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Phase 5 backend boundary passed (${files.length} files scanned).`);
|
||||||
|
|
||||||
|
function walk(relativePath) {
|
||||||
|
const absolutePath = path.join(process.cwd(), relativePath);
|
||||||
|
if (!fs.existsSync(absolutePath)) return [];
|
||||||
|
const stat = fs.statSync(absolutePath);
|
||||||
|
if (stat.isFile()) return [relativePath];
|
||||||
|
return fs.readdirSync(absolutePath, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const child = path.join(relativePath, entry.name);
|
||||||
|
return entry.isDirectory() ? walk(child) : [child];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
for (const [command, args] of [
|
||||||
|
[process.execPath, ["scripts/phase5-backend-boundary.mjs"]],
|
||||||
|
[process.execPath, ["scripts/phase2-domain-smoke.mjs"]],
|
||||||
|
[process.execPath, ["scripts/phase1-auth-smoke.mjs"]],
|
||||||
|
]) {
|
||||||
|
execFileSync(command, args, { cwd: process.cwd(), stdio: "inherit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Phase 5 freelancer backend smoke passed.");
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const files = walk("app/portal").filter((file) => /\.(ts|tsx)$/.test(file));
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(path.join(process.cwd(), file), "utf8");
|
||||||
|
if (/[@/]lib\/supabase|createServiceRoleClient|\bsupabase\b/i.test(content)) {
|
||||||
|
violations.push(`${file}: Supabase runtime reference`);
|
||||||
|
}
|
||||||
|
if (/NEXT_PUBLIC_SUPABASE|SUPABASE_SERVICE_ROLE/.test(content)) {
|
||||||
|
violations.push(`${file}: Supabase environment dependency`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
violations,
|
||||||
|
[],
|
||||||
|
`Phase 6 portal boundary violations:\n${violations.join("\n")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const file of [
|
||||||
|
"app/portal/layout.tsx",
|
||||||
|
"app/portal/page.tsx",
|
||||||
|
"app/portal/projects/page.tsx",
|
||||||
|
"app/portal/projects/[id]/page.tsx",
|
||||||
|
"app/portal/projects/[id]/actions.ts",
|
||||||
|
"app/portal/tasks/page.tsx",
|
||||||
|
"app/portal/revisions/page.tsx",
|
||||||
|
]) {
|
||||||
|
const content = fs.readFileSync(path.join(process.cwd(), file), "utf8");
|
||||||
|
assert.match(content, /requirePortalBackend/, `${file} must derive its actor from the portal session adapter`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(path.join(process.cwd(), "server/web/portal.ts")), "Missing portal session adapter");
|
||||||
|
console.log(`Phase 6 portal boundary passed (${files.length} files scanned).`);
|
||||||
|
|
||||||
|
function walk(relativePath) {
|
||||||
|
const absolutePath = path.join(process.cwd(), relativePath);
|
||||||
|
return fs.readdirSync(absolutePath, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const child = path.join(relativePath, entry.name);
|
||||||
|
return entry.isDirectory() ? walk(child) : [child];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
|
for (const [command, args] of [
|
||||||
|
[process.execPath, ["scripts/phase6-portal-boundary.mjs"]],
|
||||||
|
[process.execPath, ["scripts/phase2-domain-smoke.mjs"]],
|
||||||
|
[process.execPath, ["scripts/phase3-storage-smoke.mjs"]],
|
||||||
|
[process.execPath, ["scripts/phase1-auth-smoke.mjs"]],
|
||||||
|
]) {
|
||||||
|
execFileSync(command, args, { cwd: process.cwd(), stdio: "inherit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Phase 6 portal backend smoke passed.");
|
||||||
Reference in New Issue
Block a user