Skip to content

Commit 4c5f6a5

Browse files
committed
Fix auth E2E: invitation accept fallback and branding mocks.
Accept invitations client-side after invite signup without failing when the server hook already accepted; fix org-config route mocks for tagline-only fetches and disambiguate login password field.
1 parent fd4107f commit 4c5f6a5

4 files changed

Lines changed: 54 additions & 25 deletions

File tree

‎components/auth/AcceptInvitationActions.tsx‎

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,40 @@ import { AuthMessage } from "@/components/auth/authConstants";
1010
import api from "@/lib/api";
1111
import authClient from "@/lib/authClient";
1212

13+
const SESSION_POLL_MS = 50;
14+
const SESSION_POLL_ATTEMPTS = 20;
15+
16+
async function waitForAuthSession() {
17+
for (let attempt = 0; attempt < SESSION_POLL_ATTEMPTS; attempt++) {
18+
const { data } = await authClient.getSession();
19+
if (data?.session) return true;
20+
await new Promise((resolve) => setTimeout(resolve, SESSION_POLL_MS));
21+
}
22+
return false;
23+
}
24+
25+
/** Accepts the invitation for the current session and sets the active org. */
26+
export async function acceptInvitationForSession(
27+
invitationId: string,
28+
): Promise<boolean> {
29+
try {
30+
if (!(await waitForAuthSession())) return false;
31+
const res = await api.invitations[":id"].accept.$post({
32+
param: { id: invitationId },
33+
});
34+
if (!res.ok) return false;
35+
const { organizationId } = (await res.json()) as {
36+
organizationId: string;
37+
};
38+
const { error: activeOrgError } = await authClient.organization.setActive({
39+
organizationId,
40+
});
41+
return !activeOrgError;
42+
} catch {
43+
return false;
44+
}
45+
}
46+
1347
/** Accepts the invitation as the signed-in invitee, then goes home. */
1448
export function AcceptInvitationButton({
1549
invitationId,
@@ -22,27 +56,7 @@ export function AcceptInvitationButton({
2256

2357
const accept = async () => {
2458
setMessage({ kind: "loading", text: AuthMessage.Loading });
25-
try {
26-
const res = await api.invitations[":id"].accept.$post({
27-
param: { id: invitationId },
28-
});
29-
if (!res.ok) {
30-
setMessage({ kind: "error", text: AuthMessage.AcceptFailed });
31-
return;
32-
}
33-
const { organizationId } = (await res.json()) as {
34-
organizationId: string;
35-
};
36-
const { error: activeOrgError } = await authClient.organization.setActive(
37-
{
38-
organizationId,
39-
},
40-
);
41-
if (activeOrgError) {
42-
setMessage({ kind: "error", text: AuthMessage.AcceptFailed });
43-
return;
44-
}
45-
} catch {
59+
if (!(await acceptInvitationForSession(invitationId))) {
4660
setMessage({ kind: "error", text: AuthMessage.AcceptFailed });
4761
return;
4862
}

‎components/auth/SignupForm.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
66
import AuthTextField, {
77
focusFirstInvalidField,
88
} from "@/components/auth/AuthTextField";
9+
import { acceptInvitationForSession } from "@/components/auth/AcceptInvitationActions";
910
import AuthPasswordField from "@/components/auth/AuthPasswordField";
1011
import AuthSubmitButton from "@/components/auth/AuthSubmitButton";
1112
import AuthFormMessage, {
@@ -124,6 +125,11 @@ export default function SignupForm({
124125
return;
125126
}
126127

128+
if (invitation) {
129+
setMessage({ kind: "loading", text: AuthMessage.Loading });
130+
await acceptInvitationForSession(invitation.id);
131+
}
132+
127133
setMessage({ kind: "success", text: AuthMessage.AccountCreated });
128134
};
129135

‎tests/e2e/login.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ test.describe("Login Page", () => {
3737

3838
await page.goto("/login");
3939
await page.getByPlaceholder("example@email.com").fill(user.email);
40-
await page.getByPlaceholder("Password").fill(user.password);
40+
await page.getByLabel("Password", { exact: true }).fill(user.password);
4141

4242
await page.getByRole("button", { name: "Login" }).click();
4343
await expect(page).toHaveURL(/\//);

‎tests/e2e/unauthenticated-branding.spec.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@ async function configureBranding(
2020
const url = new URL(route.request().url());
2121
expect(url.searchParams.get("organizationSlug")).toBe("servicestart");
2222
const keys = url.searchParams.getAll("keys");
23-
expect(keys).toContain(OrganizationConfigKey.LogoUrl);
23+
const normalizedLogo =
24+
logoUrl === null || logoUrl === undefined
25+
? logoUrl
26+
: logoUrl.trim() === ""
27+
? logoUrl
28+
: logoUrl.trim();
2429
const config: Record<string, string | null> = {
25-
[OrganizationConfigKey.LogoUrl]: logoUrl,
30+
[OrganizationConfigKey.LogoUrl]: normalizedLogo,
2631
[OrganizationConfigKey.Tagline]: tagline,
2732
[OrganizationConfigKey.PrimaryColor]: "#FD8033",
2833
[OrganizationConfigKey.SecondaryColor]: "#FB3552",
2934
};
3035
await route.fulfill({
31-
json: Object.fromEntries(keys.map((key) => [key, config[key]])),
36+
json: Object.fromEntries(keys.map((key) => [key, config[key] ?? null])),
3237
});
3338
});
3439
}
@@ -110,7 +115,11 @@ test.describe("Unauthenticated organization branding", () => {
110115
page,
111116
}) => {
112117
await configureBranding(page, "/bog.svg");
118+
const configResponse = page.waitForResponse((response) =>
119+
new URL(response.url()).pathname.endsWith("/organizationConfig"),
120+
);
113121
await page.goto(path);
122+
await configResponse;
114123
const tagline = page.getByTestId("organization-tagline");
115124
await expect(tagline).toHaveText("Our organization welcomes you");
116125
await expect(tagline).toBeVisible();

0 commit comments

Comments
 (0)