Skip to content

Commit eb0627a

Browse files
committed
Add E2E Fallback Tests Workflow and Update Environment Configurations
- Introduced a new GitHub Actions workflow for end-to-end fallback tests, ensuring the SDK properly exercises fallback logic when the primary backend is down. - Updated environment configuration files across multiple applications to include fallback API URLs. - Enhanced the backend's package.json to support fallback logic in development mode. - Added client-side components and pages for testing fallback scenarios in the demo application. - Improved the StackClientInterface to handle fallback URLs and implement sticky fallback behavior. These changes enhance the testing framework and improve the SDK's resilience in handling backend failures.
1 parent ee150a9 commit eb0627a

15 files changed

Lines changed: 753 additions & 20 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# TODO: keep in sync with e2e-tests.yaml — this is a near-copy with the backend
2+
# started on the fallback port (8110) only, so the SDK exercises fallback logic.
3+
name: Runs E2E Fallback Tests
4+
5+
on:
6+
push:
7+
branches:
8+
- main
9+
- dev
10+
pull_request:
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
15+
16+
jobs:
17+
build:
18+
name: E2E Fallback Tests (Node ${{ matrix.node-version }})
19+
runs-on: ubicloud-standard-8
20+
env:
21+
NODE_ENV: test
22+
STACK_ENABLE_HARDCODED_PASSKEY_CHALLENGE_FOR_TESTING: yes
23+
STACK_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:8128/stackframe"
24+
STACK_EXTERNAL_DB_SYNC_MAX_DURATION_MS: "20000"
25+
STACK_EXTERNAL_DB_SYNC_DIRECT: "false"
26+
27+
strategy:
28+
matrix:
29+
node-version: [22.x]
30+
31+
steps:
32+
- uses: actions/checkout@v6
33+
34+
- name: Setup Node.js ${{ matrix.node-version }}
35+
uses: actions/setup-node@v6
36+
with:
37+
node-version: ${{ matrix.node-version }}
38+
39+
- name: Setup pnpm
40+
uses: pnpm/action-setup@v4
41+
42+
- name: Start Docker Compose in background
43+
uses: JarvusInnovations/background-action@v1.0.7
44+
with:
45+
run: docker compose -f docker/dependencies/docker.compose.yaml up --pull always -d &
46+
wait-on: /dev/null
47+
tail: true
48+
wait-for: 3s
49+
log-output-if: true
50+
51+
- name: Install dependencies
52+
run: pnpm install --frozen-lockfile
53+
54+
- name: Create .env.test.local files
55+
run: |
56+
cp apps/backend/.env.development apps/backend/.env.test.local
57+
cp apps/dashboard/.env.development apps/dashboard/.env.test.local
58+
cp apps/e2e/.env.development apps/e2e/.env.test.local
59+
cp docs/.env.development docs/.env.test.local
60+
cp examples/cjs-test/.env.development examples/cjs-test/.env.test.local
61+
cp examples/demo/.env.development examples/demo/.env.test.local
62+
cp examples/docs-examples/.env.development examples/docs-examples/.env.test.local
63+
cp examples/e-commerce/.env.development examples/e-commerce/.env.test.local
64+
cp examples/middleware/.env.development examples/middleware/.env.test.local
65+
cp examples/supabase/.env.development examples/supabase/.env.test.local
66+
cp examples/convex/.env.development examples/convex/.env.test.local
67+
68+
- name: Configure fallback backend URL
69+
run: |
70+
echo "NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:8110" >> apps/backend/.env.test.local
71+
echo "NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:8110" >> apps/dashboard/.env.test.local
72+
echo "NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:8110" >> apps/e2e/.env.test.local
73+
echo "NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:8110" >> examples/demo/.env.test.local
74+
echo "STACK_BACKEND_BASE_URL=http://localhost:8110" >> apps/e2e/.env.test.local
75+
76+
- name: Build
77+
run: pnpm build
78+
79+
- name: Wait on Postgres
80+
run: pnpm run wait-until-postgres-is-ready:pg_isready
81+
82+
- name: Wait on Inbucket
83+
run: pnpx wait-on tcp:localhost:8129
84+
85+
- name: Wait on Svix
86+
run: pnpx wait-on tcp:localhost:8113
87+
88+
- name: Wait on QStash
89+
run: pnpx wait-on tcp:localhost:8125
90+
91+
- name: Wait on ClickHouse
92+
run: pnpx wait-on http://localhost:8136/ping
93+
94+
- name: Initialize database
95+
run: pnpm run db:init
96+
97+
# Start backend ONLY on fallback port 8110 — primary port 8102 is intentionally left down
98+
# so the SDK exercises its fallback logic for every request.
99+
- name: Start stack-backend on fallback port (8110)
100+
uses: JarvusInnovations/background-action@v1.0.7
101+
with:
102+
run: pnpm -C apps/backend run with-env:test next start --port 8110 --log-order=stream &
103+
wait-on: |
104+
http://localhost:8110
105+
tail: true
106+
wait-for: 30s
107+
log-output-if: true
108+
109+
- name: Start stack-dashboard in background
110+
uses: JarvusInnovations/background-action@v1.0.7
111+
with:
112+
run: pnpm run start:dashboard --log-order=stream &
113+
wait-on: |
114+
http://localhost:8101
115+
tail: true
116+
wait-for: 30s
117+
log-output-if: true
118+
119+
- name: Start mock-oauth-server in background
120+
uses: JarvusInnovations/background-action@v1.0.7
121+
with:
122+
run: pnpm run start:mock-oauth-server --log-order=stream &
123+
wait-on: |
124+
http://localhost:8110
125+
tail: true
126+
wait-for: 30s
127+
log-output-if: true
128+
129+
- name: Start run-email-queue in background
130+
uses: JarvusInnovations/background-action@v1.0.7
131+
with:
132+
run: pnpm -C apps/backend run run-email-queue --log-order=stream &
133+
wait-on: |
134+
http://localhost:8110
135+
tail: true
136+
wait-for: 30s
137+
log-output-if: true
138+
139+
- name: Start run-cron-jobs in background
140+
uses: JarvusInnovations/background-action@v1.0.7
141+
with:
142+
run: pnpm -C apps/backend run run-cron-jobs:test --log-order=stream &
143+
wait-on: |
144+
http://localhost:8110
145+
tail: true
146+
wait-for: 30s
147+
log-output-if: true
148+
149+
- name: Wait 10 seconds
150+
run: sleep 10
151+
152+
- name: Verify primary port 8102 is NOT running
153+
run: |
154+
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8102/health 2>/dev/null | grep -q "200"; then
155+
echo "ERROR: Primary backend on port 8102 should NOT be running for fallback tests"
156+
exit 1
157+
fi
158+
echo "Confirmed: primary port 8102 is down, fallback tests will exercise SDK fallback logic"
159+
160+
- name: Run tests
161+
run: pnpm test run
162+
163+
- name: Verify data integrity
164+
run: pnpm run verify-data-integrity --no-bail
165+
166+
- name: Print Docker Compose logs
167+
if: always()
168+
run: docker compose -f docker/dependencies/docker.compose.yaml logs

apps/backend/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
2+
NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}10
23
NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01
34
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09
45
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=false

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"with-env:dev": "dotenv -c development --",
1212
"with-env:prod": "dotenv -c production --",
1313
"with-env:test": "dotenv -c test --",
14-
"dev": "concurrently -n \"dev,codegen,prisma-studio,email-queue,cron-jobs\" -k \"next dev --port ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 ${STACK_BACKEND_DEV_EXTRA_ARGS:-}\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\" \"pnpm run run-cron-jobs\"",
14+
"dev": "BACKEND_PORT=${STACK_DEV_FALLBACK_BACKEND:+${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}10} && BACKEND_PORT=${BACKEND_PORT:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02} && concurrently -n \"dev,codegen,prisma-studio,email-queue,cron-jobs\" -k \"next dev --port $BACKEND_PORT ${STACK_BACKEND_DEV_EXTRA_ARGS:-}\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\" \"pnpm run run-email-queue\" \"pnpm run run-cron-jobs\"",
1515
"dev:inspect": "STACK_BACKEND_DEV_EXTRA_ARGS=\"--inspect\" pnpm run dev",
1616
"dev:profile": "STACK_BACKEND_DEV_EXTRA_ARGS=\"--experimental-cpu-prof\" pnpm run dev",
1717
"build": "pnpm run codegen && next build",

apps/dashboard/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
2+
NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}10
23
NEXT_PUBLIC_STACK_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}04
34
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}09
45
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=false

examples/demo/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Contains the credentials for the internal project of Stack's default development environment setup.
22
# Do not use in a production environment, instead replace it with actual values gathered from https://app.stack-auth.com.
33
NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02
4+
NEXT_PUBLIC_STACK_FALLBACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}10
45
NEXT_PUBLIC_STACK_PROJECT_ID=internal
56
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
67
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"use client";
2+
3+
import { useStackApp, useUser } from "@stackframe/stack";
4+
import Link from "next/link";
5+
import { usePathname } from "next/navigation";
6+
import { useCallback, useEffect, useRef, useState } from "react";
7+
8+
type LogEntry = {
9+
time: number;
10+
msg: string;
11+
ok: boolean;
12+
};
13+
14+
export function FallbackTestClient() {
15+
const app = useStackApp();
16+
const user = useUser();
17+
const pathname = usePathname();
18+
const [log, setLog] = useState<LogEntry[]>([]);
19+
const [running, setRunning] = useState(false);
20+
const renderCount = useRef(0);
21+
renderCount.current++;
22+
23+
const addLog = useCallback((msg: string, ok: boolean) => {
24+
setLog(prev => [...prev, { time: Date.now(), msg, ok }]);
25+
}, []);
26+
27+
const runTests = useCallback(async () => {
28+
setLog([]);
29+
setRunning(true);
30+
31+
// Test 1: getProject
32+
{
33+
const start = Date.now();
34+
try {
35+
const project = await app.getProject();
36+
addLog(`getProject: ${project.id} (${Date.now() - start}ms)`, true);
37+
} catch (e: any) {
38+
addLog(`getProject FAILED: ${e.message?.slice(0, 80)} (${Date.now() - start}ms)`, false);
39+
}
40+
}
41+
42+
// Test 2: useUser
43+
addLog(`useUser: ${user ? user.primaryEmail ?? user.id : "(not signed in)"}`, true);
44+
45+
// Test 3: 5x getProject to show sticky latency
46+
{
47+
const times: number[] = [];
48+
for (let i = 0; i < 5; i++) {
49+
const start = Date.now();
50+
try {
51+
await app.getProject();
52+
times.push(Date.now() - start);
53+
} catch {
54+
times.push(-1);
55+
}
56+
}
57+
const avg = times.filter(t => t >= 0).reduce((a, b) => a + b, 0) / times.filter(t => t >= 0).length;
58+
addLog(`getProject x5: [${times.map(t => t >= 0 ? `${t}ms` : "FAIL").join(", ")}] avg=${Math.round(avg)}ms`, times.every(t => t >= 0));
59+
}
60+
61+
setRunning(false);
62+
}, [app, user, addLog]);
63+
64+
useEffect(() => {
65+
void runTests();
66+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
67+
68+
return (
69+
<div>
70+
<section style={{ marginBottom: 24, padding: 16, background: "#f0f8ff", borderRadius: 8 }}>
71+
<h2 style={{ marginTop: 0 }}>Client-side</h2>
72+
73+
<div style={{ marginBottom: 12, fontSize: 12, color: "#666" }}>
74+
<strong>Debug:</strong> renders={renderCount.current} | pathname={pathname}
75+
</div>
76+
77+
<div style={{ marginBottom: 16, fontFamily: "monospace", fontSize: 13, lineHeight: 1.8 }}>
78+
{log.map((entry, i) => (
79+
<div key={i} style={{ color: entry.ok ? "#2a7" : "#c33" }}>
80+
{entry.ok ? "OK" : "ERR"} {entry.msg}
81+
</div>
82+
))}
83+
{log.length === 0 && <div style={{ color: "#999" }}>Running...</div>}
84+
</div>
85+
86+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
87+
<button onClick={() => void runTests()} disabled={running} style={{ padding: "6px 14px", cursor: running ? "wait" : "pointer" }}>
88+
{running ? "Running..." : "Re-run"}
89+
</button>
90+
</div>
91+
</section>
92+
93+
<section style={{ padding: 16, background: "#fff8f0", borderRadius: 8 }}>
94+
<h2 style={{ marginTop: 0 }}>SPA Navigation Test</h2>
95+
<p style={{ fontSize: 13, color: "#666" }}>
96+
Click these links (client-side navigation) and come back.
97+
If sticky fallback persists, requests after navigating back should still be fast.
98+
</p>
99+
<div style={{ display: "flex", gap: 12 }}>
100+
<Link href="/" style={{ color: "#06c" }}>Home</Link>
101+
<Link href="/fallback-test" style={{ color: "#06c" }}>This page (reload)</Link>
102+
<Link href="/settings" style={{ color: "#06c" }}>Settings</Link>
103+
</div>
104+
</section>
105+
</div>
106+
);
107+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { stackServerApp } from "src/stack";
2+
import { FallbackTestClient } from "./client";
3+
4+
export default async function FallbackTestPage() {
5+
const serverStart = Date.now();
6+
const user = await stackServerApp.getUser();
7+
const project = await stackServerApp.getProject();
8+
const serverDuration = Date.now() - serverStart;
9+
10+
return (
11+
<div style={{ fontFamily: "monospace", padding: 32, maxWidth: 900 }}>
12+
<h1>SDK Fallback Test</h1>
13+
14+
<section style={{ marginBottom: 24, padding: 16, background: "#f5f5f5", borderRadius: 8 }}>
15+
<h2 style={{ marginTop: 0 }}>Server-side (RSC)</h2>
16+
<pre>{JSON.stringify({ projectId: project.id, projectName: project.displayName, user: user?.primaryEmail ?? user?.id ?? null, duration: `${serverDuration}ms` }, null, 2)}</pre>
17+
</section>
18+
19+
<FallbackTestClient />
20+
</div>
21+
);
22+
}

0 commit comments

Comments
 (0)