@essential-apps/shopify-test-runner 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/normalize.d.ts +61 -0
- package/dist/contracts/normalize.d.ts.map +1 -0
- package/dist/contracts/normalize.js +99 -0
- package/dist/contracts/normalize.js.map +1 -0
- package/dist/contracts/normalizeHtml.d.ts +37 -0
- package/dist/contracts/normalizeHtml.d.ts.map +1 -0
- package/dist/contracts/normalizeHtml.js +89 -0
- package/dist/contracts/normalizeHtml.js.map +1 -0
- package/dist/edge/cert.d.ts +44 -0
- package/dist/edge/cert.d.ts.map +1 -0
- package/dist/edge/cert.js +117 -0
- package/dist/edge/cert.js.map +1 -0
- package/dist/edge/edgeProxy.d.ts +43 -0
- package/dist/edge/edgeProxy.d.ts.map +1 -0
- package/dist/edge/edgeProxy.js +297 -0
- package/dist/edge/edgeProxy.js.map +1 -0
- package/dist/edge/nodeShim.d.ts +2 -0
- package/dist/edge/nodeShim.d.ts.map +1 -0
- package/dist/edge/nodeShim.js +217 -0
- package/dist/edge/nodeShim.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/buildSourceBundle.d.ts +56 -0
- package/dist/lib/buildSourceBundle.d.ts.map +1 -0
- package/dist/lib/buildSourceBundle.js +153 -0
- package/dist/lib/buildSourceBundle.js.map +1 -0
- package/dist/lib/freePort.d.ts +5 -0
- package/dist/lib/freePort.d.ts.map +1 -0
- package/dist/lib/freePort.js +33 -0
- package/dist/lib/freePort.js.map +1 -0
- package/dist/lib/functionBuild.d.ts +99 -0
- package/dist/lib/functionBuild.d.ts.map +1 -0
- package/dist/lib/functionBuild.js +413 -0
- package/dist/lib/functionBuild.js.map +1 -0
- package/dist/lib/neonWsProxy.d.ts +41 -0
- package/dist/lib/neonWsProxy.d.ts.map +1 -0
- package/dist/lib/neonWsProxy.js +101 -0
- package/dist/lib/neonWsProxy.js.map +1 -0
- package/dist/lib/sourceZipUpload.d.ts +45 -0
- package/dist/lib/sourceZipUpload.d.ts.map +1 -0
- package/dist/lib/sourceZipUpload.js +129 -0
- package/dist/lib/sourceZipUpload.js.map +1 -0
- package/dist/lib/stealthLaunch.d.ts +35 -0
- package/dist/lib/stealthLaunch.d.ts.map +1 -0
- package/dist/lib/stealthLaunch.js +46 -0
- package/dist/lib/stealthLaunch.js.map +1 -0
- package/dist/lib/storeAutomation.d.ts +22 -0
- package/dist/lib/storeAutomation.d.ts.map +1 -0
- package/dist/lib/storeAutomation.js +85 -0
- package/dist/lib/storeAutomation.js.map +1 -0
- package/dist/playwright/baseConfig.d.ts +62 -0
- package/dist/playwright/baseConfig.d.ts.map +1 -0
- package/dist/playwright/baseConfig.js +68 -0
- package/dist/playwright/baseConfig.js.map +1 -0
- package/dist/playwright/globalSetup.d.ts +2 -0
- package/dist/playwright/globalSetup.d.ts.map +1 -0
- package/dist/playwright/globalSetup.js +139 -0
- package/dist/playwright/globalSetup.js.map +1 -0
- package/dist/playwright/index.d.ts +9 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +9 -0
- package/dist/playwright/index.js.map +1 -0
- package/dist/probes/fonts.d.ts +4 -0
- package/dist/probes/fonts.d.ts.map +1 -0
- package/dist/probes/fonts.js +255 -0
- package/dist/probes/fonts.js.map +1 -0
- package/dist/probes/mirror.d.ts +4 -0
- package/dist/probes/mirror.d.ts.map +1 -0
- package/dist/probes/mirror.js +260 -0
- package/dist/probes/mirror.js.map +1 -0
- package/dist/probes/runProbe.d.ts +3 -0
- package/dist/probes/runProbe.d.ts.map +1 -0
- package/dist/probes/runProbe.js +219 -0
- package/dist/probes/runProbe.js.map +1 -0
- package/dist/probes/types.d.ts +72 -0
- package/dist/probes/types.d.ts.map +1 -0
- package/dist/probes/types.js +2 -0
- package/dist/probes/types.js.map +1 -0
- package/dist/scripts/_probeSourceUrl.d.ts +3 -0
- package/dist/scripts/_probeSourceUrl.d.ts.map +1 -0
- package/dist/scripts/_probeSourceUrl.js +119 -0
- package/dist/scripts/_probeSourceUrl.js.map +1 -0
- package/dist/scripts/addStore.d.ts +3 -0
- package/dist/scripts/addStore.d.ts.map +1 -0
- package/dist/scripts/addStore.js +46 -0
- package/dist/scripts/addStore.js.map +1 -0
- package/dist/scripts/buildDockerImage.d.ts +3 -0
- package/dist/scripts/buildDockerImage.d.ts.map +1 -0
- package/dist/scripts/buildDockerImage.js +60 -0
- package/dist/scripts/buildDockerImage.js.map +1 -0
- package/dist/scripts/captureAuth.d.ts +3 -0
- package/dist/scripts/captureAuth.d.ts.map +1 -0
- package/dist/scripts/captureAuth.js +124 -0
- package/dist/scripts/captureAuth.js.map +1 -0
- package/dist/scripts/captureContracts.d.ts +3 -0
- package/dist/scripts/captureContracts.d.ts.map +1 -0
- package/dist/scripts/captureContracts.js +517 -0
- package/dist/scripts/captureContracts.js.map +1 -0
- package/dist/scripts/captureRestContracts.d.ts +3 -0
- package/dist/scripts/captureRestContracts.d.ts.map +1 -0
- package/dist/scripts/captureRestContracts.js +245 -0
- package/dist/scripts/captureRestContracts.js.map +1 -0
- package/dist/scripts/checkOperationCoverage.d.ts +3 -0
- package/dist/scripts/checkOperationCoverage.d.ts.map +1 -0
- package/dist/scripts/checkOperationCoverage.js +302 -0
- package/dist/scripts/checkOperationCoverage.js.map +1 -0
- package/dist/scripts/cleanupStores.d.ts +3 -0
- package/dist/scripts/cleanupStores.d.ts.map +1 -0
- package/dist/scripts/cleanupStores.js +77 -0
- package/dist/scripts/cleanupStores.js.map +1 -0
- package/dist/scripts/createStores.d.ts +3 -0
- package/dist/scripts/createStores.d.ts.map +1 -0
- package/dist/scripts/createStores.js +66 -0
- package/dist/scripts/createStores.js.map +1 -0
- package/dist/scripts/deployAppVersion.d.ts +3 -0
- package/dist/scripts/deployAppVersion.d.ts.map +1 -0
- package/dist/scripts/deployAppVersion.js +591 -0
- package/dist/scripts/deployAppVersion.js.map +1 -0
- package/dist/scripts/devE2eBackend.d.ts +3 -0
- package/dist/scripts/devE2eBackend.d.ts.map +1 -0
- package/dist/scripts/devE2eBackend.js +117 -0
- package/dist/scripts/devE2eBackend.js.map +1 -0
- package/dist/scripts/devOnlineBackend.d.ts +3 -0
- package/dist/scripts/devOnlineBackend.d.ts.map +1 -0
- package/dist/scripts/devOnlineBackend.js +117 -0
- package/dist/scripts/devOnlineBackend.js.map +1 -0
- package/dist/scripts/installApp.d.ts +3 -0
- package/dist/scripts/installApp.d.ts.map +1 -0
- package/dist/scripts/installApp.js +163 -0
- package/dist/scripts/installApp.js.map +1 -0
- package/dist/scripts/listStores.d.ts +3 -0
- package/dist/scripts/listStores.d.ts.map +1 -0
- package/dist/scripts/listStores.js +18 -0
- package/dist/scripts/listStores.js.map +1 -0
- package/dist/scripts/runDocker.d.ts +3 -0
- package/dist/scripts/runDocker.d.ts.map +1 -0
- package/dist/scripts/runDocker.js +88 -0
- package/dist/scripts/runDocker.js.map +1 -0
- package/dist/scripts/runDockerAuth.d.ts +3 -0
- package/dist/scripts/runDockerAuth.d.ts.map +1 -0
- package/dist/scripts/runDockerAuth.js +108 -0
- package/dist/scripts/runDockerAuth.js.map +1 -0
- package/dist/scripts/runDockerOffline.d.ts +3 -0
- package/dist/scripts/runDockerOffline.d.ts.map +1 -0
- package/dist/scripts/runDockerOffline.js +129 -0
- package/dist/scripts/runDockerOffline.js.map +1 -0
- package/dist/scripts/runDockerOfflineExplore.d.ts +3 -0
- package/dist/scripts/runDockerOfflineExplore.d.ts.map +1 -0
- package/dist/scripts/runDockerOfflineExplore.js +116 -0
- package/dist/scripts/runDockerOfflineExplore.js.map +1 -0
- package/dist/scripts/runIsolatedDockerOffline.d.ts +3 -0
- package/dist/scripts/runIsolatedDockerOffline.d.ts.map +1 -0
- package/dist/scripts/runIsolatedDockerOffline.js +351 -0
- package/dist/scripts/runIsolatedDockerOffline.js.map +1 -0
- package/dist/scripts/runOffline.d.ts +3 -0
- package/dist/scripts/runOffline.d.ts.map +1 -0
- package/dist/scripts/runOffline.js +521 -0
- package/dist/scripts/runOffline.js.map +1 -0
- package/dist/scripts/runOfflineE2e.d.ts +3 -0
- package/dist/scripts/runOfflineE2e.d.ts.map +1 -0
- package/dist/scripts/runOfflineE2e.js +408 -0
- package/dist/scripts/runOfflineE2e.js.map +1 -0
- package/dist/scripts/runOfflineFullTests.d.ts +3 -0
- package/dist/scripts/runOfflineFullTests.d.ts.map +1 -0
- package/dist/scripts/runOfflineFullTests.js +1456 -0
- package/dist/scripts/runOfflineFullTests.js.map +1 -0
- package/dist/scripts/runSupermachine.d.ts +3 -0
- package/dist/scripts/runSupermachine.d.ts.map +1 -0
- package/dist/scripts/runSupermachine.js +474 -0
- package/dist/scripts/runSupermachine.js.map +1 -0
- package/dist/scripts/runSupermachineAuth.d.ts +3 -0
- package/dist/scripts/runSupermachineAuth.d.ts.map +1 -0
- package/dist/scripts/runSupermachineAuth.js +454 -0
- package/dist/scripts/runSupermachineAuth.js.map +1 -0
- package/dist/scripts/runTests.d.ts +3 -0
- package/dist/scripts/runTests.d.ts.map +1 -0
- package/dist/scripts/runTests.js +278 -0
- package/dist/scripts/runTests.js.map +1 -0
- package/dist/scripts/runVm.d.ts +3 -0
- package/dist/scripts/runVm.d.ts.map +1 -0
- package/dist/scripts/runVm.js +524 -0
- package/dist/scripts/runVm.js.map +1 -0
- package/dist/scripts/runVmAuth.d.ts +3 -0
- package/dist/scripts/runVmAuth.d.ts.map +1 -0
- package/dist/scripts/runVmAuth.js +475 -0
- package/dist/scripts/runVmAuth.js.map +1 -0
- package/dist/scripts/runVmScript.d.ts +3 -0
- package/dist/scripts/runVmScript.d.ts.map +1 -0
- package/dist/scripts/runVmScript.js +242 -0
- package/dist/scripts/runVmScript.js.map +1 -0
- package/dist/scripts/setupTestDb.d.ts +3 -0
- package/dist/scripts/setupTestDb.d.ts.map +1 -0
- package/dist/scripts/setupTestDb.js +61 -0
- package/dist/scripts/setupTestDb.js.map +1 -0
- package/dist/scripts/verifyContracts.d.ts +3 -0
- package/dist/scripts/verifyContracts.d.ts.map +1 -0
- package/dist/scripts/verifyContracts.js +258 -0
- package/dist/scripts/verifyContracts.js.map +1 -0
- package/dist/scripts/verifyRestContracts.d.ts +3 -0
- package/dist/scripts/verifyRestContracts.d.ts.map +1 -0
- package/dist/scripts/verifyRestContracts.js +237 -0
- package/dist/scripts/verifyRestContracts.js.map +1 -0
- package/dist/vite/offlineConfig.d.ts +34 -0
- package/dist/vite/offlineConfig.d.ts.map +1 -0
- package/dist/vite/offlineConfig.js +61 -0
- package/dist/vite/offlineConfig.js.map +1 -0
- package/dist/vite/onlineConfig.d.ts +42 -0
- package/dist/vite/onlineConfig.d.ts.map +1 -0
- package/dist/vite/onlineConfig.js +56 -0
- package/dist/vite/onlineConfig.js.map +1 -0
- package/docker/Dockerfile +67 -0
- package/docker/Dockerfile.vm +137 -0
- package/docker/README.md +50 -0
- package/docker/entrypoint.sh +198 -0
- package/package.json +85 -0
- package/src/contracts/normalize.ts +96 -0
- package/src/contracts/normalizeHtml.ts +98 -0
- package/src/edge/ca.cnf +14 -0
- package/src/edge/ca.crt +22 -0
- package/src/edge/ca.key +28 -0
- package/src/edge/cert.ts +117 -0
- package/src/edge/edgeProxy.ts +390 -0
- package/src/edge/server.cnf +28 -0
- package/src/edge/server.crt +26 -0
- package/src/edge/server.key +28 -0
- package/src/index.ts +67 -0
- package/src/lib/buildSourceBundle.ts +197 -0
- package/src/lib/freePort.ts +33 -0
- package/src/lib/functionBuild.ts +490 -0
- package/src/lib/neonWsProxy.ts +124 -0
- package/src/lib/sourceZipUpload.ts +168 -0
- package/src/lib/stealthLaunch.ts +57 -0
- package/src/lib/storeAutomation.ts +110 -0
- package/src/playwright/baseConfig.ts +120 -0
- package/src/playwright/globalSetup.ts +179 -0
- package/src/playwright/index.ts +11 -0
- package/src/probes/fonts.ts +279 -0
- package/src/probes/mirror.ts +283 -0
- package/src/probes/runProbe.ts +257 -0
- package/src/probes/types.ts +73 -0
- package/src/scripts/addStore.ts +59 -0
- package/src/scripts/buildDockerImage.ts +66 -0
- package/src/scripts/captureAuth.ts +145 -0
- package/src/scripts/captureContracts.ts +675 -0
- package/src/scripts/captureRestContracts.ts +319 -0
- package/src/scripts/checkOperationCoverage.ts +365 -0
- package/src/scripts/cleanupStores.ts +91 -0
- package/src/scripts/createStores.ts +77 -0
- package/src/scripts/deployAppVersion.ts +692 -0
- package/src/scripts/devOnlineBackend.ts +141 -0
- package/src/scripts/installApp.ts +188 -0
- package/src/scripts/listStores.ts +19 -0
- package/src/scripts/runDockerAuth.ts +120 -0
- package/src/scripts/runOffline.ts +577 -0
- package/src/scripts/runOfflineFullTests.ts +1634 -0
- package/src/scripts/runTests.ts +306 -0
- package/src/scripts/runVm.ts +562 -0
- package/src/scripts/runVmAuth.ts +541 -0
- package/src/scripts/runVmScript.ts +282 -0
- package/src/scripts/setupTestDb.ts +71 -0
- package/src/scripts/verifyContracts.ts +310 -0
- package/src/scripts/verifyRestContracts.ts +275 -0
- package/src/vite/onlineConfig.ts +60 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Self-contained test image targeting an HVF arm64 microVM
|
|
2
|
+
# (via supermachine HVF) — sibling of `Dockerfile` (which targets libkrun-via-
|
|
3
|
+
# Rosetta amd64). The two are deliberately separate files rather
|
|
4
|
+
# than a single multi-arch Dockerfile because the diff between them
|
|
5
|
+
# is meaningful, not just an ARCH var:
|
|
6
|
+
#
|
|
7
|
+
# - libkrun (`Dockerfile`): amd64, bind-mounted workspace,
|
|
8
|
+
# Google Chrome stable amd64 for
|
|
9
|
+
# Cloudflare Turnstile.
|
|
10
|
+
# - vm (this file): arm64 native (Apple HVF), workspace
|
|
11
|
+
# mounted via virtio-fs DAX once that
|
|
12
|
+
# lands (until then, baked-in or
|
|
13
|
+
# vm.writeFile'd), arm64 Chromium
|
|
14
|
+
# bundled with Playwright — passes
|
|
15
|
+
# Turnstile too (verified against
|
|
16
|
+
# nowsecure.nl, the public canary).
|
|
17
|
+
#
|
|
18
|
+
# Built via either `container build --arch arm64 -t essential-apps/
|
|
19
|
+
# shopify-test-vm:latest -f Dockerfile.vm .` (Apple
|
|
20
|
+
# `container`) or `docker buildx build --platform linux/arm64 ...`
|
|
21
|
+
# once the VM runtime adds local-OCI-archive `Image.build` support.
|
|
22
|
+
# Then consumed by the VM-side runner as:
|
|
23
|
+
#
|
|
24
|
+
# const image = await Image.build({
|
|
25
|
+
# ref: 'essential-apps/shopify-test-vm:latest',
|
|
26
|
+
# source: 'local-oci-archive', // future API
|
|
27
|
+
# memoryMib: 4096, // for safeFetch's patchright
|
|
28
|
+
# // pre-warm; 2 GiB OOM-killed it.
|
|
29
|
+
# vcpus: 2,
|
|
30
|
+
# });
|
|
31
|
+
#
|
|
32
|
+
# Until local-OCI-archive lands, push to ghcr.io as a workaround.
|
|
33
|
+
#
|
|
34
|
+
# Workspace shape inside the guest (matches the libkrun side so
|
|
35
|
+
# probe code is identical):
|
|
36
|
+
# /workspace ← virtio-fs mount from host (or
|
|
37
|
+
# baked snapshot if mount missing)
|
|
38
|
+
# /var/lib/postgresql/data ← per-VM Postgres data dir (NOT
|
|
39
|
+
# persisted; VMs are
|
|
40
|
+
# ephemeral, fresh per acquire)
|
|
41
|
+
FROM mcr.microsoft.com/playwright:v1.59.1-jammy
|
|
42
|
+
|
|
43
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
44
|
+
|
|
45
|
+
# Base toolchain. Same set as the amd64 image MINUS:
|
|
46
|
+
# - Google Chrome stable (no arm64 build; not needed because
|
|
47
|
+
# Playwright's bundled arm64 Chromium passes Turnstile)
|
|
48
|
+
#
|
|
49
|
+
# Kept:
|
|
50
|
+
# - postgresql-14 + client (mock backend for offline-full tests)
|
|
51
|
+
# - openssh-client (localhost.run SSH tunnel fallback when
|
|
52
|
+
# cloudflared 429s — verified working in VMs)
|
|
53
|
+
# - cloudflared (primary webhook tunnel; arm64 .deb below)
|
|
54
|
+
# - net-tools, procps (debugging + process inspection)
|
|
55
|
+
# - libnss3-tools (Playwright's NSS for client cert handling)
|
|
56
|
+
# - x11vnc — auth-capture flow (runVmAuth.ts) starts it
|
|
57
|
+
# post-acquire to expose Xvfb :99 to host via vm.exposeTcp,
|
|
58
|
+
# so a human can interactively log in to Shopify Partners.
|
|
59
|
+
#
|
|
60
|
+
# Xvfb ships in the Playwright base image (it's how MS expects you
|
|
61
|
+
# to run headful flows under their image); no extra install needed.
|
|
62
|
+
#
|
|
63
|
+
# build-essential (make + g++): consuming apps declare native deps (e.g.
|
|
64
|
+
# bufferutil + utf-8-validate — DIRECT deps, not optional) that have no
|
|
65
|
+
# prebuilt for the guest's Node on arm64, so the warmup `npm install`
|
|
66
|
+
# compiles them via node-gyp, which needs make + a C++ toolchain. Without
|
|
67
|
+
# it the warmup fails ("not found: make") and a fresh bake never boots.
|
|
68
|
+
# (The compile is serialized via MAKEFLAGS=-j1 in the warmup to avoid OOM.)
|
|
69
|
+
RUN apt-get update -qq \
|
|
70
|
+
&& apt-get install -y -qq \
|
|
71
|
+
build-essential \
|
|
72
|
+
postgresql-14 \
|
|
73
|
+
postgresql-client-14 \
|
|
74
|
+
openssh-client \
|
|
75
|
+
sudo \
|
|
76
|
+
procps \
|
|
77
|
+
net-tools \
|
|
78
|
+
wget \
|
|
79
|
+
gnupg \
|
|
80
|
+
ca-certificates \
|
|
81
|
+
libnss3-tools \
|
|
82
|
+
x11vnc \
|
|
83
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
84
|
+
|
|
85
|
+
# cloudflared — arm64 .deb (vs amd64 in the sibling Dockerfile).
|
|
86
|
+
# Used by webhook-envelope probes to expose the in-VM Hono receiver
|
|
87
|
+
# on a public *.trycloudflare.com URL so real Shopify can POST
|
|
88
|
+
# webhooks during conformance live capture.
|
|
89
|
+
#
|
|
90
|
+
# Note: when running INSIDE the VM, cloudflared still spawns
|
|
91
|
+
# fine because it's an outbound connection (to Cloudflare's edge)
|
|
92
|
+
# — the VM runtime's vsock-based TSI doesn't block outbound
|
|
93
|
+
# AF_INET on real interfaces. The in-VM 127.0.0.1 loopback for
|
|
94
|
+
# guest-local listeners is the bit that has the accept-queue
|
|
95
|
+
# split (known limitation, deferred); cloudflared's connect to the
|
|
96
|
+
# receiver via 127.0.0.1 WOULD hit that, so we either:
|
|
97
|
+
# (a) run cloudflared on host + use vm.exposeTcp(N, receiverPort)
|
|
98
|
+
# so cloudflared sees `host:N` and forwards from there, OR
|
|
99
|
+
# (b) bake cloudflared into the VM and accept that it'll work
|
|
100
|
+
# once the TSI accept-queue split is fixed upstream.
|
|
101
|
+
# We bake it here (option b) so the image is self-sufficient; the
|
|
102
|
+
# host-side runner (`vmProbe.ts`, TBD) picks the topology.
|
|
103
|
+
RUN wget -q -O /tmp/cloudflared.deb \
|
|
104
|
+
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb \
|
|
105
|
+
&& apt-get update -qq \
|
|
106
|
+
&& apt-get install -y -qq /tmp/cloudflared.deb \
|
|
107
|
+
&& rm -f /tmp/cloudflared.deb \
|
|
108
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
109
|
+
|
|
110
|
+
# Native-addon build toolchain. Consuming apps may pull npm deps with
|
|
111
|
+
# native bindings that lack a linux-arm64 / current-Node prebuild (e.g.
|
|
112
|
+
# utf-8-validate, bufferutil — WebSocket accelerators). The in-VM
|
|
113
|
+
# `npm install` (offline warmup) then falls back to compiling them with
|
|
114
|
+
# node-gyp, which needs make + a C/C++ compiler + python3. Without these
|
|
115
|
+
# the build fails and aborts the whole bake. Added as a dedicated late
|
|
116
|
+
# layer so rebuilds stay cache-friendly (the base/Chrome/cloudflared
|
|
117
|
+
# layers above don't re-run).
|
|
118
|
+
RUN apt-get update -qq \
|
|
119
|
+
&& apt-get install -y -qq \
|
|
120
|
+
build-essential \
|
|
121
|
+
python3 \
|
|
122
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
123
|
+
|
|
124
|
+
ENV PGDATA=/var/lib/postgresql/data
|
|
125
|
+
ENV PG_BIN=/usr/lib/postgresql/14/bin
|
|
126
|
+
|
|
127
|
+
RUN mkdir -p "$PGDATA" \
|
|
128
|
+
&& chown -R postgres:postgres "$PGDATA" \
|
|
129
|
+
&& chmod 700 "$PGDATA"
|
|
130
|
+
|
|
131
|
+
# VM quirk: `cmd` is set by the host-side runner at
|
|
132
|
+
# Image.build time, not by Dockerfile CMD/ENTRYPOINT. Whatever we
|
|
133
|
+
# put here is overridden. Leaving CMD as `bash` so the image is
|
|
134
|
+
# still useful for ad-hoc `docker run` / `container run` debugging.
|
|
135
|
+
WORKDIR /workspace
|
|
136
|
+
|
|
137
|
+
CMD ["bash"]
|
package/docker/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `essential-apps/shopify-test` container image
|
|
2
|
+
|
|
3
|
+
Canonical Linux Chrome + Postgres + VNC image used by every layer of
|
|
4
|
+
the test stack. The tag mirrors the npm scope (`@essential-apps/shopify-test`)
|
|
5
|
+
so anyone inspecting `container images` sees the same name they
|
|
6
|
+
`npm install`.
|
|
7
|
+
|
|
8
|
+
Consumers (current):
|
|
9
|
+
|
|
10
|
+
- **Conformance Partner-session capture** — `conformance:auth` boots
|
|
11
|
+
this image and exposes VNC so an operator can log in once. Bootstrap
|
|
12
|
+
(`conformance:bootstrap`) reuses it headlessly to mint per-store
|
|
13
|
+
admin tokens.
|
|
14
|
+
- **Online-test fallback** — `dev:online` / online specs were historically
|
|
15
|
+
run from inside this image so real Google Chrome amd64 could clear
|
|
16
|
+
Cloudflare Turnstile on `admin.shopify.com`. The `runDocker*.ts`
|
|
17
|
+
drivers were removed in the May 2026 rename; the image lingers as
|
|
18
|
+
a fallback only — the vm is the default, but the vm runtime
|
|
19
|
+
ships only arm64 chromium and patchright doesn't clear Turnstile
|
|
20
|
+
(see `packages/conformance/docs/AUTH.md` — escalation pending).
|
|
21
|
+
|
|
22
|
+
## Build
|
|
23
|
+
|
|
24
|
+
From the shopify-test workspace root:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run image:build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Tags the image as `essential-apps/shopify-test:latest`. Override
|
|
31
|
+
with `SHOPIFY_TEST_IMAGE_TAG=...` env var. Build is amd64
|
|
32
|
+
(Rosetta-emulated on Apple Silicon) because Google only ships
|
|
33
|
+
Chrome stable for Linux amd64.
|
|
34
|
+
|
|
35
|
+
## Why amd64, not arm64
|
|
36
|
+
|
|
37
|
+
Bundled Playwright chromium hits Cloudflare's Turnstile challenge
|
|
38
|
+
on Linux even with patchright stealth patches. Real Google Chrome
|
|
39
|
+
(stable) renders Turnstile the same way it does on macOS host
|
|
40
|
+
Chrome, where the challenge auto-clicks cleanly. Google doesn't
|
|
41
|
+
ship an arm64 Linux Chrome, so the image is amd64-only.
|
|
42
|
+
|
|
43
|
+
## Migrating existing consumers
|
|
44
|
+
|
|
45
|
+
Apps that built their own per-app image (e.g. `essential-upsell-online:latest`)
|
|
46
|
+
can either:
|
|
47
|
+
|
|
48
|
+
1. **Retag** — `container image tag essential-apps/shopify-test:latest essential-upsell-online:latest` — zero changes to their scripts.
|
|
49
|
+
2. **Point at the shared image** — update `TEST_IMAGE` (or equivalent env var)
|
|
50
|
+
in their `.env.test` to `essential-apps/shopify-test:latest`.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Container entrypoint: initialize/start Postgres, then exec the user's
|
|
3
|
+
# command. Postgres data lives at $PGDATA (bind-mounted from the host
|
|
4
|
+
# at runtime, so it survives container restarts).
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Initialize cluster on first run
|
|
8
|
+
if [ ! -s "$PGDATA/PG_VERSION" ]; then
|
|
9
|
+
echo "[entrypoint] Initializing fresh Postgres cluster at $PGDATA"
|
|
10
|
+
chown -R postgres:postgres "$PGDATA"
|
|
11
|
+
sudo -u postgres "$PG_BIN/initdb" \
|
|
12
|
+
-D "$PGDATA" \
|
|
13
|
+
--auth-local=trust \
|
|
14
|
+
--auth-host=trust \
|
|
15
|
+
--no-locale \
|
|
16
|
+
--encoding=UTF8 \
|
|
17
|
+
>/dev/null
|
|
18
|
+
# Allow connections from any IP within the container's loopback range.
|
|
19
|
+
echo "host all all 127.0.0.1/32 trust" >> "$PGDATA/pg_hba.conf"
|
|
20
|
+
echo "host all all ::1/128 trust" >> "$PGDATA/pg_hba.conf"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
echo "[entrypoint] Starting Postgres on 127.0.0.1:5432"
|
|
24
|
+
sudo -u postgres "$PG_BIN/pg_ctl" \
|
|
25
|
+
-D "$PGDATA" \
|
|
26
|
+
-l "$PGDATA/postgres.log" \
|
|
27
|
+
-o "-h 127.0.0.1 -p 5432 -k /tmp" \
|
|
28
|
+
start
|
|
29
|
+
|
|
30
|
+
# Wait for pg ready
|
|
31
|
+
for _ in $(seq 1 30); do
|
|
32
|
+
if "$PG_BIN/pg_isready" -h 127.0.0.1 -p 5432 -q; then break; fi
|
|
33
|
+
sleep 0.5
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# Create a superuser role matching the container's running user (root by
|
|
37
|
+
# default) so createdb/psql work without explicit -U postgres flags. The
|
|
38
|
+
# OR-true tolerates the role already existing on warm starts.
|
|
39
|
+
# Using -h /tmp because pg started with -k /tmp (custom socket dir).
|
|
40
|
+
CURRENT_USER=$(id -un)
|
|
41
|
+
sudo -u postgres "$PG_BIN/psql" -h /tmp -d postgres -c \
|
|
42
|
+
"CREATE ROLE \"$CURRENT_USER\" SUPERUSER LOGIN" 2>&1 | grep -v "already exists" || true
|
|
43
|
+
|
|
44
|
+
# Stop pg cleanly when the container exits
|
|
45
|
+
trap 'sudo -u postgres "$PG_BIN/pg_ctl" -D "$PGDATA" stop -m fast >/dev/null 2>&1 || true' EXIT
|
|
46
|
+
|
|
47
|
+
# Mark to test runner that we're running inside the container so the
|
|
48
|
+
# storePool fixture uses vanilla @playwright/test (matches the bundled
|
|
49
|
+
# chromium in this image) instead of patchright (which wants a newer
|
|
50
|
+
# Playwright image version).
|
|
51
|
+
export TEST_IN_CONTAINER=true
|
|
52
|
+
|
|
53
|
+
# ── Offline-mode wiring: /etc/hosts + system CA trust ───────────────
|
|
54
|
+
#
|
|
55
|
+
# When the OFFLINE runner is invoked inside this container, it boots a
|
|
56
|
+
# tiny HTTPS reverse-proxy ("edge") on :443 that fronts the per-process
|
|
57
|
+
# mock Storefront / Admin / Storefront-API / Admin-Shell servers. We
|
|
58
|
+
# need:
|
|
59
|
+
#
|
|
60
|
+
# 1. DNS: Shopify hostnames must resolve to 127.0.0.1 so the browser
|
|
61
|
+
# AND Node both reach the edge instead of real Shopify on the
|
|
62
|
+
# internet. /etc/hosts works for both — it's read by glibc's
|
|
63
|
+
# getaddrinfo, which is what `dns.lookup` (and therefore Node's
|
|
64
|
+
# HTTP stack) calls, and Chrome reads it on Linux via its host
|
|
65
|
+
# resolver.
|
|
66
|
+
#
|
|
67
|
+
# 2. TLS: the edge presents a self-signed cert with broad SANs
|
|
68
|
+
# (*.myshopify.com, *.shopify.com, etc.) shipped at
|
|
69
|
+
# `node_modules/@essential-apps/shopify-test-runner/src/edge/cert.pem`.
|
|
70
|
+
# We install it as a system CA so Chrome AND Node trust it
|
|
71
|
+
# natively — no --ignore-certificate-errors, no
|
|
72
|
+
# NODE_TLS_REJECT_UNAUTHORIZED=0.
|
|
73
|
+
#
|
|
74
|
+
# We do this in the entrypoint (rather than at image-build time)
|
|
75
|
+
# because (a) /etc/hosts is mounted by the container runtime at
|
|
76
|
+
# start, not baked into the image; and (b) the cert lives in the
|
|
77
|
+
# bind-mounted node_modules, not in the image. Both restrictions push
|
|
78
|
+
# this work to runtime.
|
|
79
|
+
#
|
|
80
|
+
# Idempotent: re-running the entrypoint (warm container restart) is
|
|
81
|
+
# safe — the hosts entries dedupe, and update-ca-certificates is
|
|
82
|
+
# a no-op if the cert is already trusted.
|
|
83
|
+
# Conformance (and any future "talk to real Shopify" mode) skips
|
|
84
|
+
# offline wiring entirely — it needs real DNS for *.shopify.com and
|
|
85
|
+
# the real Shopify TLS cert chain. Set SKIP_OFFLINE_HOST_HIJACK=1 in
|
|
86
|
+
# `container run --env ...` from such callers; the rest of the
|
|
87
|
+
# entrypoint (Postgres, x11vnc) still runs.
|
|
88
|
+
EDGE_CERT_SRC="/workspace/node_modules/@essential-apps/shopify-test-runner/src/edge/ca.crt"
|
|
89
|
+
if [ "${SKIP_OFFLINE_HOST_HIJACK:-0}" = "1" ]; then
|
|
90
|
+
echo "[entrypoint] SKIP_OFFLINE_HOST_HIJACK=1 — leaving /etc/hosts and CA store untouched."
|
|
91
|
+
elif [ -f "$EDGE_CERT_SRC" ]; then
|
|
92
|
+
# /etc/hosts entries — preserve any existing content (e.g. container
|
|
93
|
+
# runtime's auto-injected hostname line). Need BOTH IPv4 (127.0.0.1)
|
|
94
|
+
# and IPv6 (::1) entries: glibc's getaddrinfo consults /etc/hosts for
|
|
95
|
+
# both address families, but if only an IPv4 entry exists for a host
|
|
96
|
+
# that ALSO has IPv6 DNS records (real test-shop.myshopify.com has
|
|
97
|
+
# AAAA records), the IPv6 lookup falls through to DNS and resolves
|
|
98
|
+
# to real Shopify. Chrome prefers IPv6 when available and would
|
|
99
|
+
# connect there, bypassing our /etc/hosts redirect entirely.
|
|
100
|
+
for HOST in test-shop.myshopify.com admin.shopify.com cdn.shopify.com fonts.shopifycdn.com; do
|
|
101
|
+
if ! grep -q "^127\.0\.0\.1[[:space:]]\+$HOST\$" /etc/hosts 2>/dev/null; then
|
|
102
|
+
echo "127.0.0.1 $HOST" >> /etc/hosts
|
|
103
|
+
fi
|
|
104
|
+
if ! grep -q "^::1[[:space:]]\+$HOST\$" /etc/hosts 2>/dev/null; then
|
|
105
|
+
echo "::1 $HOST" >> /etc/hosts
|
|
106
|
+
fi
|
|
107
|
+
done
|
|
108
|
+
# Trust the edge's CA cert. /usr/local/share/ca-certificates/*.crt
|
|
109
|
+
# is the canonical location; update-ca-certificates appends to
|
|
110
|
+
# /etc/ssl/certs/ca-certificates.crt (which Node + glibc OpenSSL +
|
|
111
|
+
# Chrome's bundled NSS all read).
|
|
112
|
+
cp -f "$EDGE_CERT_SRC" /usr/local/share/ca-certificates/edge-ca.crt
|
|
113
|
+
update-ca-certificates >/dev/null 2>&1 || true
|
|
114
|
+
# Tell Node explicitly too — NODE_EXTRA_CA_CERTS is the
|
|
115
|
+
# supported way to extend Node's TLS trust without touching
|
|
116
|
+
# built-in defaults.
|
|
117
|
+
export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/edge-ca.crt
|
|
118
|
+
echo "[entrypoint] Offline mode: /etc/hosts + system CA configured for *.myshopify.com / *.shopify.com → 127.0.0.1"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Prepend the consuming app's node_modules/.bin so locally-installed
|
|
122
|
+
# CLIs (e.g. `shopify`, used by the storefront mock to run discount
|
|
123
|
+
# function WASM via `@shopify/cli`) resolve from PATH without
|
|
124
|
+
# requiring an `npx` wrapper. npm scripts already do this; child
|
|
125
|
+
# processes spawned from in-process mocks don't go through npm and
|
|
126
|
+
# inherit only PATH.
|
|
127
|
+
if [ -d /workspace/node_modules/.bin ]; then
|
|
128
|
+
export PATH="/workspace/node_modules/.bin:$PATH"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Newer Shopify CLI versions (3.90+) refuse to run `shopify app
|
|
132
|
+
# function run` if no `shopify.app.toml` exists in the app root.
|
|
133
|
+
# Consuming apps typically check in variant configs only
|
|
134
|
+
# (`shopify.app.dev.toml`, `shopify.app.production.toml`, etc.) and
|
|
135
|
+
# select one via `shopify app config use` — that picks one and
|
|
136
|
+
# symlinks it as `shopify.app.toml`. Container starts can't run
|
|
137
|
+
# interactive `config use`, so do the symlink here: pick a sensible
|
|
138
|
+
# default (prefer an online-named variant if present, else any
|
|
139
|
+
# variant). The actual config CONTENT doesn't matter for `function
|
|
140
|
+
# run` — it just needs to find the file to satisfy its preflight.
|
|
141
|
+
if [ -d /workspace ] && [ ! -e /workspace/shopify.app.toml ]; then
|
|
142
|
+
cd /workspace
|
|
143
|
+
CANDIDATE=""
|
|
144
|
+
for pattern in shopify.app.online*.toml shopify.app.dev*.toml shopify.app.*.toml; do
|
|
145
|
+
for f in $pattern; do
|
|
146
|
+
if [ -f "$f" ]; then CANDIDATE="$f"; break; fi
|
|
147
|
+
done
|
|
148
|
+
[ -n "$CANDIDATE" ] && break
|
|
149
|
+
done
|
|
150
|
+
if [ -n "$CANDIDATE" ]; then
|
|
151
|
+
ln -s "$CANDIDATE" shopify.app.toml
|
|
152
|
+
echo "[entrypoint] Symlinked $CANDIDATE → shopify.app.toml (for Shopify CLI compatibility)"
|
|
153
|
+
fi
|
|
154
|
+
cd /
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Start Xvfb as a background daemon so headed Chromium has a virtual
|
|
158
|
+
# display. This avoids xvfb-run's process-wrapping behavior (which has
|
|
159
|
+
# been observed to hang under Apple `container`). We just need DISPLAY
|
|
160
|
+
# to point at a live X server.
|
|
161
|
+
if [ -z "${DISPLAY:-}" ] && command -v Xvfb >/dev/null 2>&1; then
|
|
162
|
+
# 1600x1000 matches modern laptop screens better than 1400x900 and
|
|
163
|
+
# gives explore-mode Chrome enough room for DevTools-open layout
|
|
164
|
+
# without clipping. Tests use Playwright's own viewport setting
|
|
165
|
+
# (1400x900) so this only affects what's visible via VNC.
|
|
166
|
+
Xvfb :99 -screen 0 1600x1000x24 -nolisten tcp &
|
|
167
|
+
export DISPLAY=:99
|
|
168
|
+
# Tiny wait for the X server to be ready before any X client connects.
|
|
169
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
170
|
+
[ -S /tmp/.X11-unix/X99 ] && break
|
|
171
|
+
sleep 0.1
|
|
172
|
+
done
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Optional: expose the Xvfb display via VNC for the one-time interactive
|
|
176
|
+
# auth-capture flow. The wrapper (scripts/test/runDockerAuth.ts) starts
|
|
177
|
+
# the container with TEST_ONLINE_VNC=1 and --publish 5900:5900, then opens
|
|
178
|
+
# macOS Screen Sharing pointing at vnc://localhost:5900. After auth is
|
|
179
|
+
# captured, normal test runs don't set this and the VNC server doesn't
|
|
180
|
+
# start — Xvfb stays purely virtual.
|
|
181
|
+
if [ "${TEST_ONLINE_VNC:-}" = "1" ] && command -v x11vnc >/dev/null 2>&1; then
|
|
182
|
+
# macOS Screen Sharing.app prompts for a password even against a
|
|
183
|
+
# -nopw VNC server, so set a static token. Port is only published
|
|
184
|
+
# on host loopback (--publish 127.0.0.1:5900:5900), so this is just
|
|
185
|
+
# to satisfy Screen Sharing's UI, not real security.
|
|
186
|
+
VNC_PASSWORD="${TEST_ONLINE_VNC_PASSWORD:-test}"
|
|
187
|
+
mkdir -p /root/.vnc
|
|
188
|
+
x11vnc -storepasswd "$VNC_PASSWORD" /root/.vnc/passwd >/dev/null 2>&1
|
|
189
|
+
echo "[entrypoint] Starting x11vnc on :5900 (display :99); password: $VNC_PASSWORD"
|
|
190
|
+
# x11vnc must bind 0.0.0.0 inside the container or the host-side
|
|
191
|
+
# port forward can't reach it. -shared/-forever: allow disconnect &
|
|
192
|
+
# reconnect.
|
|
193
|
+
x11vnc -display :99 -forever -shared -rfbauth /root/.vnc/passwd \
|
|
194
|
+
-rfbport 5900 -bg -quiet \
|
|
195
|
+
>/var/log/x11vnc.log 2>&1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
exec "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@essential-apps/shopify-test-runner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Orchestration scripts (container, auth capture, install) and Playwright config preset for Essential Apps' Shopify test suites. Internal use only.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./playwright": {
|
|
14
|
+
"types": "./dist/playwright/index.d.ts",
|
|
15
|
+
"import": "./dist/playwright/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./contracts/normalize": {
|
|
18
|
+
"types": "./dist/contracts/normalize.d.ts",
|
|
19
|
+
"import": "./dist/contracts/normalize.js"
|
|
20
|
+
},
|
|
21
|
+
"./contracts/normalize-html": {
|
|
22
|
+
"types": "./dist/contracts/normalizeHtml.d.ts",
|
|
23
|
+
"import": "./dist/contracts/normalizeHtml.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"shopify-test-run-online": "./dist/scripts/runVm.js",
|
|
28
|
+
"shopify-test-run-offline": "./dist/scripts/runOffline.js",
|
|
29
|
+
"shopify-test-run-tests": "./dist/scripts/runTests.js",
|
|
30
|
+
"shopify-test-capture-online-auth": "./dist/scripts/runVmAuth.js",
|
|
31
|
+
"shopify-test-capture-online-auth-libkrun": "./dist/scripts/runDockerAuth.js",
|
|
32
|
+
"shopify-test-capture-auth": "./dist/scripts/captureAuth.js",
|
|
33
|
+
"shopify-test-deploy-app-version": "./dist/scripts/deployAppVersion.js",
|
|
34
|
+
"shopify-test-install-app": "./dist/scripts/installApp.js",
|
|
35
|
+
"shopify-test-add-store": "./dist/scripts/addStore.js",
|
|
36
|
+
"shopify-test-list-stores": "./dist/scripts/listStores.js",
|
|
37
|
+
"shopify-test-create-stores": "./dist/scripts/createStores.js",
|
|
38
|
+
"shopify-test-cleanup-stores": "./dist/scripts/cleanupStores.js",
|
|
39
|
+
"shopify-test-setup-db": "./dist/scripts/setupTestDb.js",
|
|
40
|
+
"shopify-test-dev-backend": "./dist/scripts/devOnlineBackend.js",
|
|
41
|
+
"shopify-test-run-offline-full-tests": "./dist/scripts/runOfflineFullTests.js",
|
|
42
|
+
"shopify-test-probe": "./dist/probes/runProbe.js",
|
|
43
|
+
"shopify-test-build-image": "./dist/scripts/buildDockerImage.js",
|
|
44
|
+
"shopify-test-check-operation-coverage": "./dist/scripts/checkOperationCoverage.js",
|
|
45
|
+
"shopify-test-capture-contracts": "./dist/scripts/captureContracts.js",
|
|
46
|
+
"shopify-test-verify-contracts": "./dist/scripts/verifyContracts.js",
|
|
47
|
+
"shopify-test-capture-rest-contracts": "./dist/scripts/captureRestContracts.js",
|
|
48
|
+
"shopify-test-verify-rest-contracts": "./dist/scripts/verifyRestContracts.js"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"docker",
|
|
53
|
+
"src"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc -p tsconfig.json",
|
|
57
|
+
"clean": "rm -rf dist"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@essential-apps/shopify-test-core": "^1.0.0",
|
|
61
|
+
"@essential-apps/shopify-test-mock-admin": "^1.0.0",
|
|
62
|
+
"@essential-apps/shopify-test-shopify-api": "^1.0.0",
|
|
63
|
+
"@essential-apps/shopify-test-storefront": "^1.0.0",
|
|
64
|
+
"@essential-apps/shopify-test-themes": "^1.0.0",
|
|
65
|
+
"@playwright/test": "^1.49.0",
|
|
66
|
+
"@types/node": "^20.19.40",
|
|
67
|
+
"@types/tar-stream": "^3.1.4",
|
|
68
|
+
"esbuild": "^0.25.0",
|
|
69
|
+
"graphql": "^16.8.1",
|
|
70
|
+
"jszip": "^3.10.1",
|
|
71
|
+
"patchright": "^1.59.4",
|
|
72
|
+
"tar-stream": "^3.2.0",
|
|
73
|
+
"undici": "^7.0.0",
|
|
74
|
+
"ws": "^8.18.0",
|
|
75
|
+
"@supermachine/core": "^0.7.67"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@types/ws": "^8.5.0",
|
|
79
|
+
"vite": "^5.4.0"
|
|
80
|
+
},
|
|
81
|
+
"license": "MIT",
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response normalisation for operation contracts.
|
|
3
|
+
*
|
|
4
|
+
* The contract-conformance system (see docs/CONTRACTS.md) compares
|
|
5
|
+
* GraphQL responses captured from two different sources — the
|
|
6
|
+
* offline mock and live Shopify — and asserts they're "the same".
|
|
7
|
+
* But "the same" can mean two different things:
|
|
8
|
+
*
|
|
9
|
+
* - **Shape-equal**: same keys, same value types, same array
|
|
10
|
+
* lengths. Ignores actual values that Shopify generates per
|
|
11
|
+
* request (IDs, timestamps, CDN-cache-busted image URLs).
|
|
12
|
+
* - **Value-equal**: byte-for-byte JSON equality.
|
|
13
|
+
*
|
|
14
|
+
* Live verification REQUIRES shape-equal — real Shopify hands out
|
|
15
|
+
* different numeric IDs and cache-bust hashes on every store. If
|
|
16
|
+
* we compare raw values, every live diff is noise.
|
|
17
|
+
*
|
|
18
|
+
* This module implements shape-equal by walking a response tree
|
|
19
|
+
* and replacing volatile substrings with stable tokens. Two
|
|
20
|
+
* responses that differ ONLY in IDs/timestamps/CDN-URLs become
|
|
21
|
+
* byte-equal after normalisation; real structural drift survives.
|
|
22
|
+
*
|
|
23
|
+
* Replacement rules (each preserves the surrounding context so the
|
|
24
|
+
* normalised value remains parseable):
|
|
25
|
+
*
|
|
26
|
+
* gid://shopify/<Type>/<digits> → gid://shopify/<Type>/<ID>
|
|
27
|
+
* gid://shopify/<Type>/<digits>?<rest> → gid://shopify/<Type>/<ID>?<rest>
|
|
28
|
+
* 2026-05-14T12:34:56Z → <DATETIME>
|
|
29
|
+
* 2026-05-14T12:34:56.789Z → <DATETIME>
|
|
30
|
+
* 2026-05-14 → <DATE>
|
|
31
|
+
* https://cdn.shopify.com/... → <CDN_URL>
|
|
32
|
+
* https://<shop>.myshopify.com/cdn/... → <CDN_URL>
|
|
33
|
+
* base64-ish cursor (eyJ... starting) → <CURSOR>
|
|
34
|
+
*
|
|
35
|
+
* The replacement is INTENTIONALLY GENEROUS. We'd rather over-
|
|
36
|
+
* normalise (treat two distinct values as equal) than under (treat
|
|
37
|
+
* two equivalent values as different and produce noisy diffs).
|
|
38
|
+
* Genuine drift (a field that's now missing, a type that changed)
|
|
39
|
+
* survives because we only rewrite well-formed volatile patterns.
|
|
40
|
+
*
|
|
41
|
+
* For offline-only verification (Layer 3), normalisation is a
|
|
42
|
+
* no-op — the offline mock returns deterministic values, so raw
|
|
43
|
+
* equality already works. For live verification (Layer 4),
|
|
44
|
+
* normalisation is essential.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const ID_GID_RE = /^(gid:\/\/shopify\/[A-Za-z]+)\/(\d+)(\?.*)?$/;
|
|
48
|
+
const ISO_DATE_TIME_RE =
|
|
49
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
50
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
51
|
+
const CDN_URL_RE =
|
|
52
|
+
/^https?:\/\/(?:cdn\.shopify\.com|[^./]+\.myshopify\.com\/cdn\/)/;
|
|
53
|
+
// Base64-ish cursors are tough to detect without false-positives. A
|
|
54
|
+
// loose heuristic: starts with `eyJ` (the base64 prefix of `{"...`)
|
|
55
|
+
// and is at least 40 chars. Real Shopify cursors are JWT-ish.
|
|
56
|
+
const CURSOR_RE = /^eyJ[A-Za-z0-9_+/=-]{37,}$/;
|
|
57
|
+
|
|
58
|
+
export function normaliseString(s: string): string {
|
|
59
|
+
const gid = s.match(ID_GID_RE);
|
|
60
|
+
if (gid) {
|
|
61
|
+
const [, typePrefix, , suffix] = gid;
|
|
62
|
+
return `${typePrefix}/<ID>${suffix ?? ''}`;
|
|
63
|
+
}
|
|
64
|
+
if (ISO_DATE_TIME_RE.test(s)) return '<DATETIME>';
|
|
65
|
+
if (ISO_DATE_RE.test(s)) return '<DATE>';
|
|
66
|
+
if (CDN_URL_RE.test(s)) return '<CDN_URL>';
|
|
67
|
+
if (CURSOR_RE.test(s)) return '<CURSOR>';
|
|
68
|
+
return s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Walk a JSON-able value and replace volatile substrings with
|
|
73
|
+
* stable tokens. Returns a deep-cloned tree — never mutates the
|
|
74
|
+
* input. Objects and arrays recurse; scalars (string / number /
|
|
75
|
+
* boolean / null) get inspected directly.
|
|
76
|
+
*
|
|
77
|
+
* Number IDs (e.g. `legacyResourceId: 12345`) — we DON'T normalise
|
|
78
|
+
* these. They're explicit values the consuming app may compare on.
|
|
79
|
+
* If a contract relies on a specific numeric ID, the live and
|
|
80
|
+
* offline values won't match and the diff fires (correctly — it's
|
|
81
|
+
* a fixture-mismatch issue, fix via fixtures.json or by re-
|
|
82
|
+
* capturing against a known seeded live store).
|
|
83
|
+
*/
|
|
84
|
+
export function normaliseResponse(value: unknown): unknown {
|
|
85
|
+
if (value === null || value === undefined) return value;
|
|
86
|
+
if (typeof value === 'string') return normaliseString(value);
|
|
87
|
+
if (Array.isArray(value)) return value.map(normaliseResponse);
|
|
88
|
+
if (typeof value === 'object') {
|
|
89
|
+
const out: Record<string, unknown> = {};
|
|
90
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
91
|
+
out[k] = normaliseResponse(v);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML normalisation for Liquid output contracts.
|
|
3
|
+
*
|
|
4
|
+
* Liquid renders produce HTML with three kinds of volatile content
|
|
5
|
+
* that would otherwise diff-spam every capture against live:
|
|
6
|
+
*
|
|
7
|
+
* 1. Generated identifiers — UUIDs in data-* attributes,
|
|
8
|
+
* auto-incremented `id="..."` values, cursor-y opaque tokens.
|
|
9
|
+
* 2. Asset URLs with cache-busters — `?v=1234567890` on every
|
|
10
|
+
* CDN-served image / CSS.
|
|
11
|
+
* 3. Whitespace + attribute order — both sides may render the
|
|
12
|
+
* same logical HTML with different formatting.
|
|
13
|
+
*
|
|
14
|
+
* The normaliser collapses these to stable tokens, preserving the
|
|
15
|
+
* structural skeleton that conformance actually cares about. Two
|
|
16
|
+
* outputs that differ ONLY in these volatile dimensions become
|
|
17
|
+
* byte-equal after normalisation; genuine structural drift
|
|
18
|
+
* (a missing element, a renamed attribute) survives.
|
|
19
|
+
*
|
|
20
|
+
* Implementation: regex passes on the raw HTML string. We could
|
|
21
|
+
* use a real HTML parser (parse5 / node-html-parser) for attribute
|
|
22
|
+
* sorting, but that's a substantial dep for marginal gain — both
|
|
23
|
+
* the offline mock and live Shopify emit attributes in source-
|
|
24
|
+
* declaration order, so post-render orders match anyway. Switch
|
|
25
|
+
* to a parser later if a real ordering mismatch shows up.
|
|
26
|
+
*
|
|
27
|
+
* Keep the patterns in `normaliseHtmlString` aligned with whatever
|
|
28
|
+
* volatile values surface during live captures — drift caught here
|
|
29
|
+
* (false positive) means add a rule; drift missed (false negative)
|
|
30
|
+
* means tighten an existing one.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** UUID v4-ish — case-insensitive, hyphenated. Used for funnel / variant / extension IDs. */
|
|
34
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
35
|
+
|
|
36
|
+
/** Long base-N numeric IDs — Shopify uses these everywhere (product IDs, variant IDs). */
|
|
37
|
+
const NUMERIC_ID_8PLUS_RE = /\b\d{8,}\b/g;
|
|
38
|
+
|
|
39
|
+
/** Asset cache-buster `?v=12345` (with surrounding URL chars preserved). */
|
|
40
|
+
const CACHE_BUSTER_RE = /\?v=\d+/g;
|
|
41
|
+
|
|
42
|
+
/** Token used by Shopify analytics — `__st.*` payloads in inline script tags. */
|
|
43
|
+
const ANALYTICS_TOKEN_RE = /"(s|t|a|p|c|e)t":\s*\d+/g;
|
|
44
|
+
|
|
45
|
+
/** Long base64-ish blob (40+ chars) — cursors, signed URLs, etc. */
|
|
46
|
+
const LONG_BASE64_RE = /\b[A-Za-z0-9+/]{40,}={0,2}\b/g;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Replace the value of any `data-…="…"` attribute whose key looks
|
|
50
|
+
* "id-like" with a token. Catches `data-funnel-id="abc-..."`,
|
|
51
|
+
* `data-product-id="123"`, etc. without us having to enumerate every
|
|
52
|
+
* possible attribute name. Conservative — only matches keys ending
|
|
53
|
+
* in `-id` to avoid stomping on legitimate data attributes like
|
|
54
|
+
* `data-trigger-type`.
|
|
55
|
+
*/
|
|
56
|
+
const DATA_ID_ATTR_RE = /(data-[\w-]*-id)="[^"]+"/gi;
|
|
57
|
+
|
|
58
|
+
/** `id="..."` value — any HTML id attribute. */
|
|
59
|
+
const ID_ATTR_RE = /\bid="([^"]+)"/g;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Collapse runs of whitespace BETWEEN HTML tags. Preserves
|
|
63
|
+
* whitespace WITHIN text content (which can be semantically
|
|
64
|
+
* meaningful, e.g. between words in a sentence).
|
|
65
|
+
*/
|
|
66
|
+
function collapseInterTagWhitespace(html: string): string {
|
|
67
|
+
return html.replace(/>\s+</g, '><');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Trim leading/trailing whitespace and collapse all-whitespace
|
|
72
|
+
* runs to single spaces inside text nodes that span attribute
|
|
73
|
+
* boundaries. Safe: applied after the inter-tag pass.
|
|
74
|
+
*/
|
|
75
|
+
function collapseTextWhitespace(html: string): string {
|
|
76
|
+
return html.replace(/\s{2,}/g, ' ').trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Single entry point. Apply each normalisation in a deterministic
|
|
81
|
+
* order — later passes assume earlier ones already ran.
|
|
82
|
+
*/
|
|
83
|
+
export function normaliseHtmlString(html: string): string {
|
|
84
|
+
let out = html;
|
|
85
|
+
// Order matters — narrower patterns first, so they consume the
|
|
86
|
+
// input before generic ones (`NUMERIC_ID_8PLUS_RE`) eat the
|
|
87
|
+
// numeric tail of a cache-buster or UUID.
|
|
88
|
+
out = out.replace(CACHE_BUSTER_RE, '?v=<CACHE_BUSTER>');
|
|
89
|
+
out = out.replace(UUID_RE, '<UUID>');
|
|
90
|
+
out = out.replace(DATA_ID_ATTR_RE, '$1="<ID>"');
|
|
91
|
+
out = out.replace(ID_ATTR_RE, 'id="<ID>"');
|
|
92
|
+
out = out.replace(LONG_BASE64_RE, '<BASE64>');
|
|
93
|
+
out = out.replace(ANALYTICS_TOKEN_RE, (_m, k) => `"${k}t":<TS>`);
|
|
94
|
+
out = out.replace(NUMERIC_ID_8PLUS_RE, '<NUMERIC_ID>');
|
|
95
|
+
out = collapseInterTagWhitespace(out);
|
|
96
|
+
out = collapseTextWhitespace(out);
|
|
97
|
+
return out;
|
|
98
|
+
}
|