Skip to content

Commit 958ddc0

Browse files
committed
Add end-to-end smoke tests for back-office host
1 parent 42617df commit 958ddc0

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { expect, request } from "@playwright/test";
2+
import { test } from "@shared/e2e/fixtures/page-auth";
3+
import { getBackOfficeBaseUrl, getBaseUrl } from "@shared/e2e/utils/constants";
4+
import { createTestContext } from "@shared/e2e/utils/test-assertions";
5+
import { step } from "@shared/e2e/utils/test-step-wrapper";
6+
7+
const BACK_OFFICE_BASE_URL = getBackOfficeBaseUrl();
8+
const BASE_URL = getBaseUrl();
9+
10+
test.describe("@smoke", () => {
11+
/**
12+
* Verifies the local-dev back-office authentication round trip:
13+
* an unauthenticated browser request to a back-office API endpoint
14+
* triggers a redirect to the MockEasyAuth impersonation page; selecting an
15+
* identity sets the DevEasyAuth cookie; and a follow-up request to
16+
* `/api/back-office/me` returns the impersonated identity's claims. Also
17+
* verifies that the back-office host serves the BackOfficeWebApp SPA shell.
18+
* Production validation (real Easy Auth) is a manual deploy-time check, not
19+
* covered by this CI suite.
20+
*/
21+
test("should redirect unauthenticated user to mock easy auth and return identity claims after impersonation", async ({
22+
browser
23+
}) => {
24+
const browserContext = await browser.newContext({ baseURL: BACK_OFFICE_BASE_URL, ignoreHTTPSErrors: true });
25+
const page = await browserContext.newPage();
26+
createTestContext(page);
27+
28+
await step("Navigate to authenticated back-office endpoint & verify redirect to mock easy auth login")(async () => {
29+
await page.goto("/api/back-office/me");
30+
31+
await expect(page).toHaveURL(
32+
`${BACK_OFFICE_BASE_URL}/.auth/login/aad?post_login_redirect_uri=%2Fapi%2Fback-office%2Fme`
33+
);
34+
await expect(page.getByRole("heading", { name: "Mock Easy Auth - pick an identity" })).toBeVisible();
35+
await expect(page.getByRole("link", { name: "Admin User Groups: BackOfficeAdmins" })).toBeVisible();
36+
})();
37+
38+
await step("Pick the Admin identity & verify callback redirects back to the protected endpoint")(async () => {
39+
await page.getByRole("link", { name: "Admin User Groups: BackOfficeAdmins" }).click();
40+
41+
await expect(page).toHaveURL(`${BACK_OFFICE_BASE_URL}/api/back-office/me`);
42+
})();
43+
44+
await step("Call /api/back-office/me with DevEasyAuth cookie & verify identity payload")(async () => {
45+
const response = await page.request.get(`${BACK_OFFICE_BASE_URL}/api/back-office/me`, {
46+
headers: { Accept: "application/json" }
47+
});
48+
49+
expect(response.status()).toBe(200);
50+
const payload = await response.json();
51+
expect(payload.displayName).toBe("Admin User");
52+
expect(payload.groups).toContain("BackOfficeAdmins");
53+
})();
54+
55+
await step("Visit back-office root & verify the BackOfficeWebApp SPA shell is served")(async () => {
56+
const response = await page.request.get(`${BACK_OFFICE_BASE_URL}/`, {
57+
headers: { Accept: "text/html" }
58+
});
59+
60+
expect(response.status()).toBe(200);
61+
const body = await response.text();
62+
expect(body).toContain('id="back-office"');
63+
expect(body).toContain("<title>Back Office</title>");
64+
})();
65+
66+
await browserContext.close();
67+
});
68+
});
69+
70+
test.describe("@smoke", () => {
71+
/**
72+
* Verifies host-scoped isolation: back-office endpoints are only served on
73+
* the back-office host (RequireHost predicate produces 404 elsewhere), and
74+
* an authenticated account session does not authorize back-office requests.
75+
* BackOfficeIdentity is a separate authentication scheme and ignores the
76+
* account session cookie even if present. The user-facing host continues to
77+
* serve account endpoints with normal 401 behavior when unauthenticated.
78+
*/
79+
test("should isolate back-office endpoints to the back-office host and reject account-authenticated requests", async ({
80+
ownerPage
81+
}) => {
82+
createTestContext(ownerPage);
83+
84+
const accountStorageState = await ownerPage.context().storageState();
85+
const accountAuthenticatedContext = await request.newContext({
86+
storageState: accountStorageState,
87+
ignoreHTTPSErrors: true
88+
});
89+
const anonymousApiContext = await request.newContext({ ignoreHTTPSErrors: true });
90+
91+
await step("GET /api/back-office/me on user-facing host with account session & verify 404 from RequireHost")(
92+
async () => {
93+
const response = await accountAuthenticatedContext.get(`${BASE_URL}/api/back-office/me`, {
94+
headers: { Accept: "application/json" },
95+
maxRedirects: 0
96+
});
97+
98+
expect(response.status()).toBe(404);
99+
}
100+
)();
101+
102+
await step(
103+
"GET /api/back-office/me on back-office host with account session and JSON Accept & verify 401 (BackOfficeIdentity ignores account cookies, and subdomain scoping prevents cross-host attachment)"
104+
)(async () => {
105+
const response = await accountAuthenticatedContext.get(`${BACK_OFFICE_BASE_URL}/api/back-office/me`, {
106+
headers: { Accept: "application/json" },
107+
maxRedirects: 0
108+
});
109+
110+
expect(response.status()).toBe(401);
111+
})();
112+
113+
await step(
114+
"GET /api/back-office/me on back-office host with account session and HTML Accept & verify redirect to mock easy auth login"
115+
)(async () => {
116+
const response = await accountAuthenticatedContext.get(`${BACK_OFFICE_BASE_URL}/api/back-office/me`, {
117+
headers: { Accept: "text/html" },
118+
maxRedirects: 0
119+
});
120+
121+
expect(response.status()).toBe(302);
122+
expect(response.headers().location).toBe("/.auth/login/aad?post_login_redirect_uri=%2Fapi%2Fback-office%2Fme");
123+
})();
124+
125+
await step("GET /api/account/users on user-facing host with no cookie & verify 401 regression")(async () => {
126+
const response = await anonymousApiContext.get(`${BASE_URL}/api/account/users`, {
127+
headers: { Accept: "application/json" },
128+
maxRedirects: 0
129+
});
130+
131+
expect(response.status()).toBe(401);
132+
})();
133+
134+
await accountAuthenticatedContext.dispose();
135+
await anonymousApiContext.dispose();
136+
});
137+
});

application/shared-webapp/tests/e2e/utils/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function readBasePort(): number {
2828

2929
const BASE_PORT = readBasePort();
3030
const DEFAULT_BASE_URL = `https://app.dev.localhost:${BASE_PORT}`;
31+
const DEFAULT_BACK_OFFICE_BASE_URL = `https://back-office.dev.localhost:${BASE_PORT}`;
3132

3233
export const isWindows = process.platform === "win32";
3334
export const isLinux = process.platform === "linux";
@@ -39,6 +40,14 @@ export function getBaseUrl(): string {
3940
return process.env.PUBLIC_URL ?? DEFAULT_BASE_URL;
4041
}
4142

43+
/**
44+
* Get the back-office base URL for tests. Mirrors getBaseUrl(): both share the
45+
* same port so a single AppGateway instance handles both hosts in local dev.
46+
*/
47+
export function getBackOfficeBaseUrl(): string {
48+
return process.env.BACK_OFFICE_PUBLIC_URL ?? DEFAULT_BACK_OFFICE_BASE_URL;
49+
}
50+
4251
/**
4352
* Check if we're running against localhost
4453
*/

0 commit comments

Comments
 (0)