Skip to content

Commit b8a2563

Browse files
committed
Use Caddy to rewrite origin
1 parent d07f2b6 commit b8a2563

File tree

6 files changed

+59
-93
lines changed

6 files changed

+59
-93
lines changed

docker/dev-gateway/Caddyfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
handle /ghost/api/* {
2626
reverse_proxy {env.GHOST_BACKEND} {
2727
header_up Host {host}
28+
header_up Origin http://localhost:2368
2829
header_up X-Real-IP {remote_host}
2930
header_up X-Forwarded-For {remote_host}
3031

@@ -145,6 +146,7 @@
145146
handle {
146147
reverse_proxy {env.GHOST_BACKEND} {
147148
header_up Host {host}
149+
header_up Origin http://localhost:2368
148150
header_up X-Real-IP {remote_host}
149151
header_up X-Forwarded-For {remote_host}
150152

@@ -162,6 +164,7 @@
162164
rewrite * {http.request.orig_uri.path}
163165
reverse_proxy {env.GHOST_BACKEND} {
164166
header_up Host {host}
167+
header_up Origin http://localhost:2368
165168
header_up X-Forwarded-Proto https
166169
}
167170
}

e2e/helpers/pages/admin/settings/sections/staff-section.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,6 @@ export class StaffSection extends BasePage {
6363
// Submit the invitation
6464
await this.inviteModal.getByRole('button', {name: 'Send invitation'}).click();
6565

66-
// Wait for success toast
67-
await this.page.getByTestId('toast-success').waitFor({state: 'visible'});
68-
6966
// Wait for modal to close
7067
await this.inviteModal.waitFor({state: 'hidden'});
7168
}

e2e/helpers/pages/admin/signup-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ export class SignupPage extends AdminPage {
4848
}
4949
}
5050

51+
Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,30 @@
1-
import {Browser, BrowserContext} from '@playwright/test';
2-
import * as path from 'path';
31
import * as fs from 'fs';
4-
5-
/**
6-
* Base URL used by Playwright for all test contexts. Requests to this hostname
7-
* are routed to the actual Ghost backend instance (localhost:{port}) via route interception.
8-
*/
9-
export const PLAYWRIGHT_BASE_URL = 'http://ghost.test:2368';
2+
import * as path from 'path';
3+
import {Browser, BrowserContext} from '@playwright/test';
104

115
const AUTH_STATE_DIR = path.join(process.cwd(), 'e2e', 'data', 'state', 'auth');
126

137
/**
14-
* Creates a browser context with route interception to map ghost.test:2368
15-
* to localhost:{port} while preserving the Origin header for Ghost's CSRF protection.
8+
* Creates a browser context using the backend URL directly. Caddy proxy handles
9+
* Origin header rewriting for CSRF protection, so no route interception needed.
10+
* Also creates a separate APIRequestContext that uses the actual backend URL for direct HTTP requests.
1611
*/
1712
export async function createContextWithRoute(
1813
browser: Browser,
1914
backendURL: string,
2015
options?: {role?: string}
2116
): Promise<BrowserContext> {
22-
const port = new URL(backendURL).port;
2317
const storageState = options?.role ? path.join(AUTH_STATE_DIR, `${options.role}.json`) : undefined;
2418

2519
if (storageState && !fs.existsSync(storageState)) {
2620
throw new Error(`Storage state file not found: ${storageState}. Run global setup first.`);
2721
}
2822

29-
const context = await browser.newContext({
30-
baseURL: PLAYWRIGHT_BASE_URL,
31-
storageState,
32-
extraHTTPHeaders: {
33-
Origin: PLAYWRIGHT_BASE_URL
34-
}
23+
// Context for page navigation - uses backend URL directly
24+
// Caddy proxy handles Origin header rewriting
25+
return await browser.newContext({
26+
baseURL: backendURL,
27+
storageState
3528
});
36-
37-
await context.route('**/*', async (route) => {
38-
const url = new URL(route.request().url());
39-
if (url.hostname === 'ghost.test') {
40-
url.hostname = 'localhost';
41-
url.port = port;
42-
const headers = route.request().headers();
43-
// Preserve Origin header to match what was used during authentication
44-
headers['Origin'] = PLAYWRIGHT_BASE_URL;
45-
await route.continue({url: url.toString(), headers});
46-
} else {
47-
await route.continue();
48-
}
49-
});
50-
51-
return context;
5229
}
5330

e2e/helpers/playwright/fixture.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ import {AnalyticsOverviewPage} from '@/admin-pages';
33
import {Browser, BrowserContext, Page, TestInfo, test as base} from '@playwright/test';
44
import {GhostInstance, getEnvironmentManager} from '@/helpers/environment';
55
import {SettingsService} from '@/helpers/services/settings/settings-service';
6-
import {createContextWithRoute} from '@/helpers/playwright/context-with-route';
76
import {User} from '@/data-factory';
8-
import * as fs from 'fs';
9-
import * as path from 'path';
7+
import {createContextWithRoute} from '@/helpers/playwright/context-with-route';
108

119
const debug = baseDebug('e2e:ghost-fixture');
1210

13-
const AUTH_STATE_DIR = path.join(process.cwd(), 'e2e', 'data', 'state', 'auth');
14-
const USERS_STATE_FILE = path.join(process.cwd(), 'e2e', 'data', 'state', 'users.json');
15-
1611
export interface GhostConfig {
1712
memberWelcomeEmailSendInstantly: string;
1813
memberWelcomeEmailTestInbox: string;
@@ -52,7 +47,6 @@ async function setupNewAuthenticatedPage(browser: Browser, backendURL: string, r
5247
});
5348

5449
const page = await context.newPage();
55-
debug('Authenticated page created using saved storageState with host aliasing');
5650

5751
return {page, context};
5852
}
@@ -100,18 +94,12 @@ export const test = base.extend<GhostInstanceFixture>({
10094
await use(ghostInstance.baseUrl);
10195
},
10296

103-
// Load owner user credentials from saved state (backward compatibility)
10497
ghostAccountOwner: async ({}, use) => {
105-
if (!fs.existsSync(USERS_STATE_FILE)) {
106-
throw new Error(`User credentials file not found: ${USERS_STATE_FILE}. Run global setup first.`);
107-
}
108-
109-
const credentials = JSON.parse(fs.readFileSync(USERS_STATE_FILE, 'utf-8'));
11098
const owner: User = {
111-
name: credentials.owner.name,
112-
email: credentials.owner.email,
113-
password: credentials.owner.password,
114-
blogTitle: credentials.owner.blogTitle
99+
name: 'Test Owner',
100+
email: 'owner@ghost.org',
101+
password: 'test@123@test',
102+
blogTitle: 'Test Blog'
115103
};
116104
await use(owner);
117105
},

e2e/tests/global.setup.ts

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
import {getEnvironmentManager} from '@/helpers/environment';
2-
import {test as setup, expect} from '@playwright/test';
1+
import * as path from 'path';
2+
import {AnalyticsOverviewPage} from '@/admin-pages';
3+
import {MailPit} from '@/helpers/services/email/mail-pit';
34
import {SettingsPage} from '@/helpers/pages';
4-
import {loginToGetAuthenticatedSession} from '@/helpers/playwright/flows/login';
5+
import {SignupPage} from '@/helpers/pages/admin/signup-page';
56
import {createContextWithRoute} from '@/helpers/playwright/context-with-route';
6-
import {MailPit} from '@/helpers/services/email/mail-pit';
7+
import {ensureDir} from '@/helpers/utils/ensure-dir';
8+
import {expect, test as setup} from '@playwright/test';
79
import {extractInvitationLink} from '@/helpers/services/email/utils';
8-
import {SignupPage} from '@/helpers/pages/admin/signup-page';
10+
import {getEnvironmentManager} from '@/helpers/environment';
11+
import {loginToGetAuthenticatedSession} from '@/helpers/playwright/flows/login';
912
import {setupUser} from '@/helpers/utils/setup-user';
10-
import * as path from 'path';
11-
import {ensureDir} from '@/helpers/utils/ensure-dir';
1213

1314
const AUTH_STATE_DIR = path.join(process.cwd(), 'e2e', 'data', 'state', 'auth');
1415
const PASSWORD = 'test@123@test';
1516

17+
setup.describe.configure({mode: 'serial'});
1618
// Setup environment first
17-
setup('environment setup', async () => {
19+
setup('setup environment', async () => {
1820
const manager = await getEnvironmentManager();
1921
const result = await manager.globalSetup();
2022

@@ -29,7 +31,7 @@ setup('environment setup', async () => {
2931
});
3032

3133
// Setup owner user
32-
setup('setup owner user', async ({browser}) => {
34+
setup('create owner user', async ({browser}) => {
3335
const backendURL = process.env.E2E_BASE_URL!;
3436
const ownerEmail = 'owner@ghost.org';
3537

@@ -50,32 +52,33 @@ setup('setup owner user', async ({browser}) => {
5052
await context.close();
5153
});
5254

53-
// Invite and onboard staff members using parameterized tests
5455
const staffRoles: Array<{role: 'administrator' | 'editor' | 'author' | 'contributor'; name: string; email: string}> = [
5556
{role: 'administrator', name: 'Test Administrator', email: 'administrator@ghost.org'},
5657
{role: 'editor', name: 'Test Editor', email: 'editor@ghost.org'},
5758
{role: 'author', name: 'Test Author', email: 'author@ghost.org'},
5859
{role: 'contributor', name: 'Test Contributor', email: 'contributor@ghost.org'}
5960
];
61+
setup(`invite staff users`, async ({browser}) => {
62+
const backendURL = process.env.E2E_BASE_URL!;
63+
64+
const context = await createContextWithRoute(browser, backendURL, {
65+
role: 'owner'
66+
});
6067

61-
for (const {role, name, email} of staffRoles) {
62-
setup(`invite ${role}`, async ({browser}) => {
63-
const backendURL = process.env.E2E_BASE_URL!;
64-
65-
const context = await createContextWithRoute(browser, backendURL, {
66-
role: 'owner'
67-
});
68-
69-
const page = await context.newPage();
70-
const settingsPage = new SettingsPage(page);
71-
await settingsPage.goto();
72-
await settingsPage.staffSection.goto();
68+
const page = await context.newPage();
69+
const settingsPage = new SettingsPage(page);
70+
await settingsPage.goto();
71+
await settingsPage.staffSection.goto();
7372

73+
for (const {role, email} of staffRoles) {
7474
await settingsPage.staffSection.inviteUser(email, role);
75-
await context.close();
76-
});
75+
}
76+
77+
await context.close();
78+
});
7779

78-
setup(`complete ${role} signup`, async ({browser}) => {
80+
for (const {role, name, email} of staffRoles) {
81+
setup(`create ${role} user`, async ({browser}) => {
7982
const backendURL = process.env.E2E_BASE_URL!;
8083
const emailClient = new MailPit();
8184

@@ -85,37 +88,34 @@ for (const {role, name, email} of staffRoles) {
8588
const emailMessage = await emailClient.getMessageDetailed(messages[0]);
8689
const invitationLink = extractInvitationLink(emailMessage.HTML || emailMessage.Text);
8790

88-
let signupUrl = invitationLink.startsWith('http')
91+
// Extract the path from the invitation link and use consistent baseURL
92+
const invitationUrl = new URL(invitationLink.startsWith('http')
8993
? invitationLink
90-
: `${backendURL}${invitationLink}`;
91-
signupUrl = signupUrl.replace(/\/ghost\/signup\//, '/ghost/#/signup/');
94+
: `${backendURL}${invitationLink}`);
95+
const signupPath = invitationUrl.pathname.replace(/\/ghost\/signup\//, '/ghost/#/signup/');
9296

9397
const context = await createContextWithRoute(browser, backendURL);
9498
const page = await context.newPage();
9599

96-
await page.goto(signupUrl);
100+
// Use relative path so it goes through our route interception
101+
await page.goto(signupPath);
97102
const signupPage = new SignupPage(page);
98103
await signupPage.nameField.waitFor({state: 'visible'});
99104

100-
await signupPage.completeSignup(name, PASSWORD);
101-
await context.close();
102-
});
103-
104-
setup(`authenticate ${role}`, async ({browser}) => {
105-
const backendURL = process.env.E2E_BASE_URL!;
106-
107-
const context = await createContextWithRoute(browser, backendURL);
108-
const page = await context.newPage();
105+
await signupPage.nameField.fill(name);
106+
await signupPage.emailField.fill(email);
107+
await signupPage.passwordField.fill(PASSWORD);
108+
await signupPage.submitButton.click();
109109

110-
await loginToGetAuthenticatedSession(page, email, PASSWORD);
110+
await page.waitForURL(/\/ghost\/#\/(analytics|posts|site)/);
111111

112112
await context.storageState({path: path.join(AUTH_STATE_DIR, `${role}.json`)});
113113
await context.close();
114114
});
115115
}
116116

117117
// Create database snapshot after all users are onboarded
118-
setup('create database snapshot', async () => {
118+
setup('save database snapshot', async () => {
119119
const manager = await getEnvironmentManager();
120120
if ('createSnapshot' in manager && typeof manager.createSnapshot === 'function') {
121121
await manager.createSnapshot();

0 commit comments

Comments
 (0)