Skip to content

Commit 176682d

Browse files
committed
Test that slashed v2 pages redirect and update the pinned contract
1 parent c506cd2 commit 176682d

3 files changed

Lines changed: 150 additions & 1 deletion

File tree

‎apps/cloud/src/edge/marketing.test.ts‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
marketingProxyRequest,
99
parseV2Edge,
1010
v2EdgeResponse,
11+
v2PageForSlashedPath,
1112
type V2EdgeEnv,
1213
type V2Service,
1314
} from "./marketing";
@@ -145,6 +146,18 @@ describe("isV2Path", () => {
145146
}
146147
});
147148

149+
describe("v2PageForSlashedPath", () => {
150+
it("canonicalizes only v2's exact pages", () => {
151+
expect(v2PageForSlashedPath("/pricing/")).toBe("/pricing");
152+
expect(v2PageForSlashedPath("/google-workspace/")).toBe("/google-workspace");
153+
expect(v2PageForSlashedPath("/pricing")).toBeNull();
154+
expect(v2PageForSlashedPath("/pricing.md/")).toBeNull();
155+
expect(v2PageForSlashedPath("/blog/")).toBeNull();
156+
expect(v2PageForSlashedPath("/terms/")).toBeNull();
157+
expect(v2PageForSlashedPath("/")).toBeNull();
158+
});
159+
});
160+
148161
describe("isSignUpPath", () => {
149162
it("claims /sign-up and /signup only", () => {
150163
expect(isSignUpPath("/sign-up")).toBe(true);
@@ -689,6 +702,66 @@ describe("v2EdgeResponse marketing", () => {
689702
return { received, service };
690703
};
691704

705+
// A signed-out visitor at `/pricing/` would otherwise reach v1's sign-in
706+
// gate, which redirects to login.
707+
it("redirects v2's slashed exact pages to the page, query kept, without forwarding", async () => {
708+
const { received, service } = recordingService();
709+
710+
for (const [target, location] of [
711+
["/pricing/", "/pricing"],
712+
["/pricing/?ref=hn", "/pricing?ref=hn"],
713+
["/home/", "/home"],
714+
["/about-executor/", "/about-executor"],
715+
["/google-workspace/", "/google-workspace"],
716+
] as const) {
717+
for (const method of ["GET", "HEAD"]) {
718+
const response = await v2EdgeResponse(
719+
new Request(`https://executor.sh${target}`, {
720+
method,
721+
headers: { cookie: "wos-session=sealed" },
722+
}),
723+
settings(service),
724+
);
725+
expect(response?.status, `${method} ${target}`).toBe(308);
726+
expect(response?.headers.get("location"), `${method} ${target}`).toBe(
727+
`https://executor.sh${location}`,
728+
);
729+
}
730+
}
731+
expect(received).toHaveLength(0);
732+
});
733+
734+
it("forwards the slashed form of a page v2 owns below, such as /blog/", async () => {
735+
const { received, service } = recordingService();
736+
737+
const response = await v2EdgeResponse(
738+
new Request("https://executor.sh/docs/"),
739+
settings(service),
740+
);
741+
742+
expect(await response?.text()).toBe("v2");
743+
expect(received[0]?.url).toBe("https://executor.sh/docs/");
744+
});
745+
746+
it("keeps deeper paths, other methods and v1's own slashed pages with v1", () => {
747+
const { received, service } = recordingService();
748+
749+
for (const [method, target] of [
750+
["GET", "/pricing/extra"],
751+
["GET", "/home/extra/"],
752+
["GET", "/pricing//"],
753+
["POST", "/pricing/"],
754+
["GET", "/terms/"],
755+
["GET", "/demo/"],
756+
] as const) {
757+
expect(
758+
v2EdgeResponse(new Request(`https://executor.sh${target}`, { method }), settings(service)),
759+
`${method} ${target}`,
760+
).toBeNull();
761+
}
762+
expect(received).toHaveLength(0);
763+
});
764+
692765
it("sends the signed-out homepage to v2 with the URL unchanged", async () => {
693766
const { received, service } = recordingService();
694767

‎apps/cloud/src/edge/production-config.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const EdgeContract = Schema.Struct({
7777
cases: Schema.Array(
7878
Schema.Struct({ method: Schema.String, target: Schema.String, forwards: Schema.Boolean }),
7979
),
80+
slashRedirects: Schema.Struct({
81+
pages: Schema.Array(Schema.String),
82+
cases: Schema.Array(Schema.Struct({ target: Schema.String, location: Schema.String })),
83+
}),
8084
});
8185
const contract = Schema.decodeUnknownSync(Schema.fromJsonString(EdgeContract))(
8286
readFileSync(fileURLToPath(new URL("./v2-edge-contract.json", import.meta.url)), "utf8"),
@@ -134,6 +138,27 @@ describe("v2's edge contract", () => {
134138
expect(signUps.map((c) => c.target).toSorted()).toEqual(["/sign-up", "/signup"]);
135139
});
136140

141+
// A slashed exact page is not forwarded: the edge redirects it to the page,
142+
// which is. Every page v2 lists has an example.
143+
it("covers every slashed page v2 lists", () => {
144+
expect(
145+
contract.slashRedirects.cases
146+
.map((c) => new URL(c.target, contract.origin).pathname)
147+
.toSorted(),
148+
).toEqual(expect.arrayContaining(contract.slashRedirects.pages.map((page) => `${page}/`)));
149+
});
150+
151+
for (const { target, location } of contract.slashRedirects.cases) {
152+
for (const method of ["GET", "HEAD"]) {
153+
it(`redirects ${method} ${target} to ${location} without forwarding`, async () => {
154+
const { calls, response } = await runCase(method, target);
155+
expect(calls).toHaveLength(0);
156+
expect(response?.status).toBe(308);
157+
expect(response?.headers.get("location")).toBe(`${contract.origin}${location}`);
158+
});
159+
}
160+
}
161+
137162
for (const { method, target } of signUps) {
138163
it(`redirects ${method} ${target} to v2's sign-up page without forwarding`, async () => {
139164
const { calls, response } = await runCase(method, target);

‎apps/cloud/src/edge/v2-edge-contract.json‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@
116116
"target": "/blog",
117117
"forwards": true
118118
},
119+
{
120+
"method": "GET",
121+
"target": "/blog/",
122+
"forwards": true
123+
},
119124
{
120125
"method": "GET",
121126
"target": "/blog/a-post",
@@ -126,6 +131,11 @@
126131
"target": "/docs",
127132
"forwards": true
128133
},
134+
{
135+
"method": "GET",
136+
"target": "/docs/",
137+
"forwards": true
138+
},
129139
{
130140
"method": "GET",
131141
"target": "/docs/quickstart",
@@ -316,6 +326,21 @@
316326
"target": "/home/extra",
317327
"forwards": false
318328
},
329+
{
330+
"method": "GET",
331+
"target": "/about-executor/extra",
332+
"forwards": false
333+
},
334+
{
335+
"method": "GET",
336+
"target": "/google-workspace/extra",
337+
"forwards": false
338+
},
339+
{
340+
"method": "POST",
341+
"target": "/pricing/",
342+
"forwards": false
343+
},
319344
{
320345
"method": "GET",
321346
"target": "/login",
@@ -371,5 +396,31 @@
371396
"target": "/api/webhooks/workos",
372397
"forwards": false
373398
}
374-
]
399+
],
400+
"slashRedirects": {
401+
"rule": "GET or HEAD <page>/ answers 308 to <page>, query kept, for each of pages",
402+
"pages": ["/home", "/pricing", "/about-executor", "/google-workspace"],
403+
"cases": [
404+
{
405+
"target": "/pricing/",
406+
"location": "/pricing"
407+
},
408+
{
409+
"target": "/pricing/?ref=hn",
410+
"location": "/pricing?ref=hn"
411+
},
412+
{
413+
"target": "/home/",
414+
"location": "/home"
415+
},
416+
{
417+
"target": "/about-executor/",
418+
"location": "/about-executor"
419+
},
420+
{
421+
"target": "/google-workspace/",
422+
"location": "/google-workspace"
423+
}
424+
]
425+
}
375426
}

0 commit comments

Comments
 (0)