From 104f4cc7bd81e3ee9f1242187349e250bf8be2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 02:31:56 +0200 Subject: [PATCH] test(e2e): assert the guardian's connect controls on a managed profile A MANAGE grant on a managed profile now connects its OAuth providers: each of the six cards offers one connect and says whose record it is for, while Fitbit and Nightscout stay status-only. A view or view-and-add grant still gets no integration surface and a 403 from the connect route. --- e2e/v137-sharing-managed-profiles.spec.ts | 72 +++++++++++++++++++---- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/e2e/v137-sharing-managed-profiles.spec.ts b/e2e/v137-sharing-managed-profiles.spec.ts index 59928b471..5be444f83 100644 --- a/e2e/v137-sharing-managed-profiles.spec.ts +++ b/e2e/v137-sharing-managed-profiles.spec.ts @@ -1,5 +1,7 @@ import type { Page } from "@playwright/test"; +import { MANAGED_OAUTH_PROVIDERS } from "@/lib/integrations/oauth-kit/managed-provider"; + import { expect, test } from "./setup/test"; import { E2E_LEVEL_RECORDS, @@ -581,7 +583,7 @@ test.describe.serial("scoped sharing browser journeys", () => { await leaveRecord(page); }); - test("[J2-manage-edits-without-settings] keeps managed Settings on the active record and integrations status-only", async ({ + test("[J2-manage-edits-without-settings] keeps managed Settings on the active record and connects integrations for it", async ({ page, }) => { const record = E2E_LEVEL_RECORDS.find( @@ -628,22 +630,40 @@ test.describe.serial("scoped sharing browser journeys", () => { ); await expect(integrations).toBeVisible(); await expect(integrations).toHaveAttribute("data-record-id", accountId); - // Read-only: the shared integration cards render, but nothing on them - // acts on a provider. The only buttons are each card's own "details for a - // bug report" toggle and its copy button. + // A guardian holds MANAGE on the profile, so each OAuth provider's card + // carries one control, connect, and says whose record it connects for. + // Fitbit takes no new connections and Nightscout is not an OAuth flow, so + // those two stay status-only. No card takes typed input: provider + // credentials and sync settings stay with the guardian's own Settings. await expect( integrations.locator('[data-slot="integration-card"]'), ).toHaveCount(8); - await expect( - integrations.locator( - '[data-slot="integration-primary-action"], [data-slot="integration-menu-trigger"], input, select, textarea', - ), - ).toHaveCount(0); + for (const provider of MANAGED_OAUTH_PROVIDERS) { + const card = integrations.getByTestId(`managed-integration-${provider}`); + await expect( + card.getByTestId(`managed-integration-${provider}-connect`), + ).toHaveText(`Connect for ${record.username}`); + await expect( + card.locator('[data-slot="managed-integration-for"]'), + ).toHaveText(`Connects for ${record.username}`); + } await expect( integrations.locator( 'button:not([data-slot="integration-details-toggle"]):not([data-slot="integration-diagnostics-copy"])', ), - ).toHaveCount(0); + ).toHaveCount(MANAGED_OAUTH_PROVIDERS.length); + for (const provider of ["fitbit", "nightscout"]) { + await expect( + integrations + .getByTestId(`managed-integration-${provider}`) + .locator( + 'button:not([data-slot="integration-details-toggle"]):not([data-slot="integration-diagnostics-copy"])', + ), + ).toHaveCount(0); + } + await expect(integrations.locator("input, select, textarea")).toHaveCount( + 0, + ); await page.goBack(); await expect(profile).toHaveAttribute("data-record-id", accountId); @@ -656,6 +676,38 @@ test.describe.serial("scoped sharing browser journeys", () => { await expect(banner).toHaveCount(0); }); + test("[J2-manage-edits-without-settings] keeps integrations shut for a view or add grant", async ({ + page, + }) => { + // The connect controls above are MANAGE-only. A view or view-and-add grant + // gets no integration surface at all, and the connect route refuses it. + for (const access of ["READ", "WRITE"] as const) { + const fenceHeaders = trackFenceAssertion(page); + const record = E2E_LEVEL_RECORDS.find( + (candidate) => candidate.access === access, + ); + if (!record) throw new Error(`${access} fixture is missing`); + await openRecord(page, record.username); + + await page.goto("/settings/integrations"); + await expect( + page.locator('[data-slot="shared-record-unavailable"]'), + ).toBeVisible(); + await expect(page.locator('[data-slot="integration-card"]')).toHaveCount( + 0, + ); + const connectStatus = await page.evaluate(async (headers) => { + const response = await fetch( + "/api/record-settings/integrations/withings/connect", + { method: "POST", headers }, + ); + return response.status; + }, fenceHeaders()); + expect(connectStatus).toBe(403); + await leaveRecord(page); + } + }); + test("[J1-adult-levels-lifecycle] refuses direct ungranted routes before coach reads start", async ({ page, }) => {