From 2027d0c16070452a662587c3d94701ed957d53fb Mon Sep 17 00:00:00 2001 From: Flea Flicker <22+gb_flea@noreply.git.farh.net> Date: Fri, 26 Jun 2026 13:38:46 +0000 Subject: [PATCH] =?UTF-8?q?Promote=20uat=20=E2=86=92=20main=20(PROD):=20GR?= =?UTF-8?q?O-2572=20SSO=20redirect=20fix=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UAT_PLAYBOOK.md | 11 ++++- src/App.tsx | 6 +++ src/__tests__/App.test.tsx | 60 +++++++++++++++++++++++++ src/index.css | 9 ++++ src/portal/sections/BillingPayments.tsx | 2 +- src/portal/sections/PetProfiles.tsx | 4 +- 6 files changed, 88 insertions(+), 4 deletions(-) diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index 59d6de8..9bbef37 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -86,7 +86,7 @@ export const { signIn, signOut, useSession, changePassword } = authClient; | # | Scenario | Steps | Pass Criteria | Fail Criteria | |---|----------|-------|---------------|---------------| | TC-WEB-SSO-1 | Sign-in page shows SSO button | Navigate to app root URL | Sign-in page displayed with "Sign in with SSO" button visible | No SSO button, 403 before page loads | -| TC-WEB-SSO-2 | Click SSO redirects to Authentik | Click "Sign in with SSO" button | Browser redirected to Authentik login at auth.farh.net | No redirect, error shown, button does nothing | +| TC-WEB-SSO-2 | Click SSO redirects to Authentik (GRO-2572) | **Fresh session only (no pre-existing auth cookie).** Click "Sign in with SSO" button | Browser navigates to Authentik login at auth.farh.net within ~1 s — address bar changes to auth.farh.net URL | No redirect, error shown, button stays disabled, user remains on /login. Regression: prior to GRO-2572 fix the client never followed the `data.url` returned by Better Auth. Run from a clean incognito context to avoid a stale cookie masking the defect. | | TC-WEB-SSO-3 | Valid OIDC credentials authenticate | At Authentik, enter valid credentials and authenticate | Redirected back to app with active session | Redirect loop, 403, session not established | | TC-WEB-SSO-4 | Post-login dashboard accessible | After SSO flow completes, dashboard loads | Dashboard displays correctly with user identity shown | Blank page, 403, session not active | | TC-WEB-SSO-5 | User identity displayed correctly | After SSO login, check header/nav | User name/email/initials shown in nav, role reflected in UI | No user indicator, wrong user shown | @@ -320,6 +320,15 @@ the seeded UAT customer (`uat-customer@groombook.dev`), not just unit-rendered. | TC-WEB-5.16.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met | | TC-WEB-5.16.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input | +#### 5.16a Portal Tab Rows — Mobile Overflow (GRO-730 / GRO-1026) + +| # | Scenario | Steps | Expected | +|---|----------|-------|----------| +| TC-WEB-5.16.4 | My Pets tab row — horizontal scroll, no visible scrollbar | Sign in as customer → My Pets. Set viewport to 390px. If 3+ pets are seeded, the pet-selector row overflows. | Pet selector row scrolls horizontally; native scrollbar is **not** visible (`scrollbar-width: none` / `scrollbar-hide` applied). | +| TC-WEB-5.16.5 | My Pets section tab row — no visible scrollbar | On the same My Pets view, observe the tabs row (Basic Info / Medical / Grooming / History). | Tabs row scrolls horizontally when needed; native scrollbar is not visible. | +| TC-WEB-5.16.6 | Billing/Payments tab row — no wrap, no visible scrollbar | Sign in as customer → Billing/Payments at 390px. | Tab row (Invoices / Payment Methods / Packages) does **not** wrap to a second line; scrolls horizontally if needed; native scrollbar not visible. | +| TC-WEB-5.16.7 | Desktop — no visual regression | Open My Pets and Billing/Payments at ≥1024px. | No layout change; tab rows display identically to before the fix. | + ### 5.17 Error & Empty States | # | Scenario | Steps | Expected | diff --git a/src/App.tsx b/src/App.tsx index d6a8057..bf98a37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,6 +45,12 @@ function LoginPage() { if (result?.error) { setError(result.error.message ?? "Sign-in failed"); setIsLoading(false); + return; + } + // Better Auth returns the IdP authorize URL in data.url with redirect:true rather than + // issuing an HTTP 30x — the client must follow it (GRO-2572). + if (result?.data?.url) { + window.location.href = result.data.url; } }; diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index edddc4f..c0d898d 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, within, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { App } from "../App"; @@ -232,6 +233,7 @@ describe("Dev login selector", () => { }); it("does not redirect when a dev user is already selected", async () => { + localStorage.setItem("dev-user", JSON.stringify({ type: "staff", id: "s1", name: "Sarah" })); global.fetch = vi.fn((url: string) => { @@ -269,3 +271,61 @@ describe("Dev login selector", () => { ).toBeInTheDocument(); }); }); + +describe("GRO-2572 — SSO button follows redirect URL", () => { + it("navigates to data.url when signIn.social returns a redirect", async () => { + // Mock signIn.social to return the redirect payload Better Auth sends + vi.mock("../lib/auth-client.js", () => ({ + useSession: () => ({ data: null, isPending: false }), + signIn: { + social: vi.fn().mockResolvedValue({ + data: { redirect: true, url: "https://auth.farh.net/application/o/authorize/?test=1" }, + error: null, + }), + }, + signOut: vi.fn(), + changePassword: vi.fn(), + })); + + const assignMock = vi.fn(); + Object.defineProperty(window, "location", { + value: { ...window.location, href: "", origin: "https://uat.groombook.dev" }, + writable: true, + }); + Object.defineProperty(window.location, "href", { + set: assignMock, + get: () => "", + }); + + global.fetch = vi.fn((url: string) => { + if (url === "/api/dev/config") { + return Promise.resolve({ ok: true, json: async () => ({ authDisabled: false }) } as Response); + } + if (url === "/api/auth/get-session") { + return Promise.resolve({ ok: true, json: async () => null } as unknown as Response); + } + if (url === "/api/setup/status") { + return Promise.resolve({ ok: true, json: async () => ({ needsSetup: false }) } as Response); + } + if (url === "/api/auth/providers") { + return Promise.resolve({ ok: true, json: async () => ({ providers: ["authentik"] }) } as Response); + } + return Promise.resolve({ ok: true, json: async () => [] } as Response); + }) as unknown as typeof fetch; + + render( + + + + ); + + const ssoButton = await screen.findByRole("button", { name: /sign in with sso/i }); + await userEvent.click(ssoButton); + + await waitFor(() => { + expect(assignMock).toHaveBeenCalledWith( + "https://auth.farh.net/application/o/authorize/?test=1" + ); + }); + }); +}); diff --git a/src/index.css b/src/index.css index 32b3b5e..0ccb242 100644 --- a/src/index.css +++ b/src/index.css @@ -78,6 +78,15 @@ input:focus, select:focus, textarea:focus { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); } +/* ─── Scrollbar hide utility ─── */ +.scrollbar-hide { + scrollbar-width: none; + -ms-overflow-style: none; +} +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + /* ─── Scrollbar polish ─── */ ::-webkit-scrollbar { width: 6px; diff --git a/src/portal/sections/BillingPayments.tsx b/src/portal/sections/BillingPayments.tsx index e4d2902..be6610c 100644 --- a/src/portal/sections/BillingPayments.tsx +++ b/src/portal/sections/BillingPayments.tsx @@ -130,7 +130,7 @@ function BillingPaymentsInner({ sessionId, readOnly }: BillingPaymentsProps) { )} -
+
{([ { id: "invoices" as const, label: "Invoices", icon: DollarSign }, { id: "payment" as const, label: "Payment Methods", icon: CreditCard }, diff --git a/src/portal/sections/PetProfiles.tsx b/src/portal/sections/PetProfiles.tsx index 7fe0cf2..12e462f 100644 --- a/src/portal/sections/PetProfiles.tsx +++ b/src/portal/sections/PetProfiles.tsx @@ -145,7 +145,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) { return (
{/* Pet Selector */} -
+
{pets.map(p => (