mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
Setup wizard step 1/5 (#409). The list is a Server Component, so the visibility rule runs before anything reaches the browser and a project the user may not see is never serialised to it. The rule itself is not restated: `listVisibleProjects` loads each project's facts and asks `canViewProject` from `@/lib/projects/authz`. Writing the `domna_admin_access OR owning-org OR contractor-org` disjunction a second time in SQL would have given it two definitions free to drift, and the acceptance criteria (client sees its own, contractor sees what it is assigned to, internal sees all subject to `domna_admin_access`) fall out of the one definition instead of being re-derived. The cost is filtering in the app rather than the database, over two round trips and no N+1. That is the right trade while a project is a works programme and they number in the tens; if the table grows, the fix is a superset prefilter that still lets `canViewProject` decide, not a WHERE clause that re-implements it. Flagged in the function's own docs. Creation reuses the same guard rather than inventing a "can create" rule: `prospectiveProjectFacts` builds the facts the project *would* have, and `canManageProject` judges them. One consequence is worth knowing, and is covered by a test — `domna_admin_access` gates the `internal` role, so a Domna user creating for an organisation they are not a member of must grant that access. Without it they would be creating a project they could not then see, and the guard refuses rather than producing an orphan. The modal offers only organisations the user may pick, but the route handler re-checks the submitted one: a hidden option is not a permission. `createProjectSchema` is shared by the form (as a zodResolver) and the route handler (as the body parser), so the two cannot disagree about what a valid draft is. Empty strings from untouched inputs normalise to undefined rather than reaching the columns as "". Also updates projects-shell.cy.js. Its "renders the per-project dashboard shell" test passed only because the authz stub could never return false; with the real lib wired in, /projects/<id> is a 404 without standing on that project, so the test now asserts that instead. TESTS: the vitest unit tests pass (50, no database connection). Cypress was NOT run — this environment's DATABASE_URL points at production. The new spec is therefore unverified, and its end-to-end half, which INSERTs, is gated behind CYPRESS_ARA_PROJECTS_E2E_WRITES so it cannot fire by accident; it also needs the #407 seed migration (0274) applied, which has not been run against any database yet either. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
3.7 KiB
JavaScript
114 lines
3.7 KiB
JavaScript
/**
|
|
* Ara Projects — route shell smoke test (issue #406)
|
|
*
|
|
* Covers the two acceptance criteria for the shell:
|
|
* 1. an unauthenticated visit to /projects is redirected to /,
|
|
* 2. an authenticated user reaches /projects and the layout + nav render.
|
|
*
|
|
* Uses the `cy.login` command from cypress/support/commands.ts, which mints a
|
|
* real next-auth JWE cookie so the *server* middleware sees a session (the
|
|
* /api/auth/session stub alone would not be enough — middleware reads the
|
|
* cookie). Requires NEXTAUTH_JWT_SECRET in the Cypress env, same as the
|
|
* live-tracking specs.
|
|
*/
|
|
|
|
const JWT_SECRET = Cypress.env("NEXTAUTH_JWT_SECRET");
|
|
|
|
describe("Ara Projects route shell", function () {
|
|
before(function () {
|
|
if (!JWT_SECRET) {
|
|
cy.log("NEXTAUTH_JWT_SECRET not set — skipping projects shell spec");
|
|
this.skip();
|
|
}
|
|
});
|
|
|
|
it("redirects an unauthenticated visitor to the sign-in page", function () {
|
|
cy.clearCookies();
|
|
cy.visit("/projects", { failOnStatusCode: false });
|
|
cy.location("pathname").should("eq", "/");
|
|
});
|
|
|
|
it("renders the shell and nav for an authenticated internal user", function () {
|
|
cy.login({
|
|
email: "dev@domna.homes",
|
|
name: "Internal User",
|
|
onboarded: true,
|
|
sub: "cypress-internal",
|
|
});
|
|
|
|
cy.visit("/projects");
|
|
|
|
cy.location("pathname").should("eq", "/projects");
|
|
cy.get("[data-testid=projects-shell]").should("exist");
|
|
cy.get("[data-testid=projects-nav-projects]")
|
|
.should("be.visible")
|
|
.and("have.attr", "aria-current", "page");
|
|
cy.contains("h1", "Projects").should("be.visible");
|
|
});
|
|
|
|
it("sends a non-onboarded user to onboarding", function () {
|
|
// /onboarding captures user details and the required privacy-policy
|
|
// acceptance — it is not Portfolio onboarding, so contractors complete it
|
|
// too and nobody reaches /projects without it.
|
|
cy.login({
|
|
email: "site@contractor.example",
|
|
name: "Contractor User",
|
|
onboarded: false,
|
|
sub: "cypress-contractor",
|
|
});
|
|
|
|
cy.visit("/projects");
|
|
|
|
cy.location("pathname").should("eq", "/onboarding");
|
|
});
|
|
|
|
it("lets an onboarded contractor into /projects", function () {
|
|
cy.login({
|
|
email: "site@contractor.example",
|
|
name: "Contractor User",
|
|
onboarded: true,
|
|
sub: "cypress-contractor",
|
|
});
|
|
|
|
cy.visit("/projects");
|
|
|
|
cy.location("pathname").should("eq", "/projects");
|
|
cy.get("[data-testid=projects-shell]").should("exist");
|
|
});
|
|
|
|
// This assertion changed with #409. While the authz stub was in place,
|
|
// `canViewProject` could never return false, so /projects/<anything> rendered
|
|
// the dashboard shell for any signed-in user — which is what this test used
|
|
// to check. The real lib (#408) is now wired in, so a project the user has no
|
|
// standing on is a 404, and a project id that does not exist is the same 404
|
|
// (deliberately indistinguishable, so Project existence does not leak).
|
|
//
|
|
// Rendering the dashboard shell therefore now needs a real, visible project,
|
|
// which is covered by the create-project happy path in create-project.cy.js
|
|
// rather than by a hardcoded id here.
|
|
it("404s on a project the user has no standing on", function () {
|
|
cy.login({
|
|
email: "dev@domna.homes",
|
|
name: "Internal User",
|
|
onboarded: true,
|
|
sub: "cypress-internal",
|
|
});
|
|
|
|
cy.request({ url: "/projects/999999999", failOnStatusCode: false })
|
|
.its("status")
|
|
.should("eq", 404);
|
|
});
|
|
|
|
it("404s on a malformed project id", function () {
|
|
cy.login({
|
|
email: "dev@domna.homes",
|
|
name: "Internal User",
|
|
onboarded: true,
|
|
sub: "cypress-internal",
|
|
});
|
|
|
|
cy.request({ url: "/projects/not-an-id", failOnStatusCode: false })
|
|
.its("status")
|
|
.should("eq", 404);
|
|
});
|
|
});
|