Compare commits

...

2 Commits

Author SHA1 Message Date
Stockboy Steve 86f254e939 fix(GRO-2572): follow Better Auth redirect URL from signIn.social response
CI / Test (pull_request) Successful in 20s
CI / Lint & Typecheck (pull_request) Successful in 25s
CI / Build & Push Docker Image (pull_request) Successful in 47s
Better Auth's signIn.social() returns { data: { redirect: true, url } } rather
than issuing an HTTP 30x when using the fetch client. The LoginPage handler
was discarding data.url, so the SSO button appeared to do nothing (the button
disabled but the user never left /login).

Fix: after the social sign-in call, if result.data.url is present, navigate via
window.location.href. Also add an early return in the error branch so the two
paths don't bleed into each other.

Updated UAT_PLAYBOOK.md §5.4.1 TC-WEB-SSO-2 to require a fresh/incognito
context so a stale auth cookie can't mask the regression.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-26 12:05:38 +00:00
Flea Flicker 8d005942df fix(GRO-1026): re-apply GRO-730 scrollbar-hide to portal tab rows (#88)
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Image (push) Successful in 16s
fix(GRO-1026): re-apply GRO-730 scrollbar-hide to portal tab rows

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-26 08:57:46 +00:00
6 changed files with 88 additions and 4 deletions
+10 -1
View File
@@ -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 |
+6
View File
@@ -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;
}
};
+60
View File
@@ -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(
<MemoryRouter initialEntries={["/login"]}>
<App />
</MemoryRouter>
);
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"
);
});
});
});
+9
View File
@@ -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;
+1 -1
View File
@@ -130,7 +130,7 @@ function BillingPaymentsInner({ sessionId, readOnly }: BillingPaymentsProps) {
</div>
)}
<div className="flex gap-2 flex-wrap">
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
{([
{ id: "invoices" as const, label: "Invoices", icon: DollarSign },
{ id: "payment" as const, label: "Payment Methods", icon: CreditCard },
+2 -2
View File
@@ -145,7 +145,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
return (
<div className="space-y-6">
{/* Pet Selector */}
<div className="flex gap-3 overflow-x-auto pb-1">
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
{pets.map(p => (
<button
key={p.id}
@@ -191,7 +191,7 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
)}
{/* Tabs */}
<div className="flex gap-1 bg-white rounded-xl border border-stone-200 p-1 overflow-x-auto">
<div className="flex gap-1 bg-white rounded-xl border border-stone-200 p-1 overflow-x-auto scrollbar-hide">
{([
{ id: "info", label: "Basic Info", icon: PawPrint },
{ id: "medical", label: "Medical", icon: Heart },