Skip to content

Commit 7186a99

Browse files
committed
test: setup for skew protection e2e tests and first test (server actions case)
1 parent 2e8d2a6 commit 7186a99

File tree

19 files changed

+522
-9
lines changed

19 files changed

+522
-9
lines changed

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,12 @@
6565
"@netlify/zip-it-and-ship-it": "^14.1.7",
6666
"@opentelemetry/api": "^1.8.0",
6767
"@playwright/test": "^1.43.1",
68+
"@types/adm-zip": "^0.5.7",
6869
"@types/node": "^20.12.7",
6970
"@types/picomatch": "^3.0.0",
7071
"@types/uuid": "^10.0.0",
7172
"@vercel/nft": "^0.30.0",
73+
"adm-zip": "^0.5.16",
7274
"cheerio": "^1.0.0-rc.12",
7375
"clean-package": "^2.2.0",
7476
"esbuild": "^0.25.0",

tests/e2e/skew-protection.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { expect } from '@playwright/test'
2+
import { execaCommand } from 'execa'
3+
import {
4+
createE2EFixture,
5+
createSite,
6+
deleteSite,
7+
getBuildFixtureVariantCommand,
8+
publishDeploy,
9+
} from '../utils/create-e2e-fixture.js'
10+
import { test as baseTest } from '../utils/playwright-helpers.js'
11+
12+
type ExtendedFixtures = {
13+
skewProtection: {
14+
siteId: string
15+
url: string
16+
deploy1: Awaited<ReturnType<typeof createE2EFixture>>
17+
deploy2: Awaited<ReturnType<typeof createE2EFixture>>
18+
}
19+
}
20+
21+
const test = baseTest.extend<
22+
{ prepareSkewProtectionScenario: <T>(callback: () => T) => Promise<T> },
23+
ExtendedFixtures
24+
>({
25+
prepareSkewProtectionScenario: async ({ skewProtection }, use) => {
26+
// first we will publish deploy1
27+
// then we call arbitrary callback to allow tests to load page using deploy1
28+
// and after that we will publish deploy2
29+
30+
const fixture = async <T>(callback: () => T) => {
31+
await publishDeploy(skewProtection.siteId, skewProtection.deploy1.deployID)
32+
// poll to ensure deploy was restored before continuing
33+
while (true) {
34+
const response = await fetch(`${skewProtection.url}/variant.txt`)
35+
const text = await response.text()
36+
if (text.startsWith('A')) {
37+
break
38+
}
39+
await new Promise((resolve) => setTimeout(resolve, 1000))
40+
}
41+
console.log('Deploy 1 published')
42+
43+
const result = await callback()
44+
await new Promise((resolve) => setTimeout(resolve, 10_000))
45+
await publishDeploy(skewProtection.siteId, skewProtection.deploy2.deployID)
46+
47+
// poll to ensure deploy was restored before continuing
48+
while (true) {
49+
const response = await fetch(`${skewProtection.url}/variant.txt`)
50+
const text = await response.text()
51+
if (text.startsWith('B')) {
52+
break
53+
}
54+
await new Promise((resolve) => setTimeout(resolve, 1000))
55+
}
56+
console.log('Deploy 2 published')
57+
58+
return result
59+
}
60+
61+
await use(fixture)
62+
},
63+
skewProtection: [
64+
async ({}, use) => {
65+
// await use({
66+
// url: 'https://next-skew-tests-1758183689147.netlify.app',
67+
// siteId: 'cccd0ac4-fecd-4240-9263-01df2e613dd0',
68+
// deploy1: {
69+
// deployID: '68cbc1121ed77e68c4f85a17',
70+
// url: 'https://68cbc1121ed77e68c4f85a17--cccd0ac4-fecd-4240-9263-01df2e613dd0.netlify.app',
71+
// },
72+
// deploy2: {
73+
// deployID: '68cbc11b52952577e923929a',
74+
// url: 'https://68cbc11b52952577e923929a--cccd0ac4-fecd-4240-9263-01df2e613dd0.netlify.app',
75+
// },
76+
// })
77+
78+
// return
79+
80+
const { siteId, url } = await createSite({
81+
name: `next-skew-tests-${Date.now()}`,
82+
})
83+
84+
let onBuildStart: () => void = () => {}
85+
const waitForBuildStart = new Promise<void>((resolve) => {
86+
onBuildStart = () => {
87+
resolve()
88+
}
89+
})
90+
91+
const deploy1Promise = createE2EFixture('skew-protection', {
92+
siteId,
93+
useBuildbot: true,
94+
onBuildStart,
95+
env: {
96+
NETLIFY_NEXT_PLUGIN_SKEW_PROTECTION: 'true',
97+
},
98+
})
99+
100+
// we don't have to wait for deploy1 to finish completely before starting deploy2, but we do have to wait a little bit
101+
// to at least when build is scheduled, as otherwise whole deploy might be skipped
102+
await waitForBuildStart
103+
104+
const deploy2Promise = createE2EFixture('skew-protection', {
105+
siteId,
106+
useBuildbot: true,
107+
env: {
108+
NETLIFY_NEXT_PLUGIN_SKEW_PROTECTION: 'true',
109+
},
110+
onPreDeploy: async (fixtureRoot) => {
111+
await execaCommand(
112+
`${getBuildFixtureVariantCommand('variant-b')} --apply-file-changes-only`,
113+
{
114+
cwd: fixtureRoot,
115+
},
116+
)
117+
},
118+
})
119+
120+
const [deploy1, deploy2] = await Promise.all([deploy1Promise, deploy2Promise])
121+
122+
const fixture = {
123+
url,
124+
siteId,
125+
deploy1,
126+
deploy2,
127+
128+
cleanup: async () => {
129+
if (process.env.E2E_PERSIST) {
130+
console.log(
131+
`💾 Fixture and deployed site have been persisted. To clean up automatically, run tests without the 'E2E_PERSIST' environment variable.`,
132+
)
133+
134+
return
135+
}
136+
137+
await deploy1.cleanup()
138+
await deploy2.cleanup()
139+
await deleteSite(siteId)
140+
},
141+
}
142+
143+
// for local iteration - this will print out snippet to allow to reuse previously deployed setup
144+
// paste this at the top of `skewProtection` fixture function and this will avoid having to wait for redeploys
145+
// keep in mind that if fixture itself require changes, you will have to redeploy
146+
console.log(`await use(${JSON.stringify(fixture, null, 2)})\n\nreturn`)
147+
await use(fixture)
148+
149+
await fixture.cleanup()
150+
},
151+
{
152+
scope: 'worker',
153+
},
154+
],
155+
})
156+
157+
test.describe('Skew Protection', () => {
158+
test.describe('App Router', () => {
159+
test('should scope server actions to initial deploy', async ({
160+
page,
161+
skewProtection,
162+
prepareSkewProtectionScenario,
163+
}) => {
164+
await prepareSkewProtectionScenario(() => page.goto(`${skewProtection.url}/app-router`))
165+
166+
await page.getByTestId('server-action-button').click()
167+
168+
// if skew protection does not work, this will be either "B" (currently published deploy)
169+
// or error about not finding server action - example of such error:
170+
// "Error: Server Action "00a130b1673301d79679b22abb06a62c3125376d79" was not found on the server.
171+
// Read more: https://nextjs.org/docs/messages/failed-to-find-server-action"
172+
await expect(page.getByTestId('server-action-result')).toHaveText(`"A"`)
173+
})
174+
})
175+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"use server";
2+
3+
export async function testAction() {
4+
return process.env.SKEW_VARIANT;
5+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { testAction } from './actions'
5+
6+
export default function Page() {
7+
const [actionResult, setActionResult] = useState(null)
8+
9+
return (
10+
<>
11+
<h1>Skew Protection Testing - App Router</h1>
12+
<p>
13+
Current variant: <span data-testid="current-variant">{process.env.SKEW_VARIANT}</span>
14+
</p>
15+
<h2>Server Actions</h2>
16+
<div>
17+
<button
18+
data-testid="server-action-button"
19+
onClick={async () => {
20+
setActionResult(null)
21+
try {
22+
const result = await testAction()
23+
setActionResult(result)
24+
console.log(result)
25+
} catch (err) {
26+
console.error(err)
27+
setActionResult('Error: ' + (err.message || err.toString()))
28+
}
29+
}}
30+
>
31+
Test server action
32+
</button>
33+
{actionResult && (
34+
<p>
35+
Action result: <span data-testid="server-action-result">{actionResult}</span>
36+
</p>
37+
)}
38+
</div>
39+
</>
40+
)
41+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const GET = async (req) => {
2+
return new Response(process.env.SKEW_VARIANT);
3+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Simple Next App',
3+
description: 'Description for Simple Next App',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default function Page() {
2+
return (
3+
<>
4+
<h1>Skew Protection Testing</h1>
5+
<nav>
6+
<ul>
7+
<li>
8+
<a href="/app-router">App Router</a>
9+
</li>
10+
</ul>
11+
</nav>
12+
</>
13+
)
14+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { remoteImage, variant } from './variant-config.mjs'
2+
3+
/** @type {import('next').NextConfig} */
4+
const nextConfig = {
5+
output: 'standalone',
6+
eslint: {
7+
ignoreDuringBuilds: true,
8+
},
9+
experimental: {
10+
// for next@<14.1.4
11+
useDeploymentId: true,
12+
// Optionally, use with Server Actions
13+
useDeploymentIdServerActions: true,
14+
},
15+
outputFileTracingRoot: import.meta.dirname,
16+
17+
compiler: {
18+
define: {
19+
'process.env.SKEW_VARIANT': JSON.stringify(variant),
20+
},
21+
},
22+
images: {
23+
remotePatterns: [
24+
{
25+
hostname: remoteImage === 'unsplash' ? 'images.unsplash.com' : '*.pixabay.com',
26+
},
27+
],
28+
},
29+
30+
redirects() {
31+
return [
32+
{
33+
source: '/next-config-redirect',
34+
destination: `/target/${variant.toLowerCase()}`,
35+
permanent: false,
36+
},
37+
]
38+
},
39+
rewrites() {
40+
return [
41+
{
42+
source: '/next-config-rewrite',
43+
destination: `/target/${variant.toLowerCase()}`,
44+
},
45+
]
46+
},
47+
}
48+
49+
export default nextConfig

0 commit comments

Comments
 (0)