mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-08 04:57:36 +00:00
config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config
gitconfig: name/email from chezmoi data, gh credential helper by PATH lookup (portable to darwin). claude: no credentials, history, projects, sessions, or plugin caches. ssh config as private_ (0600). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c691e8dc5
commit
20891f3910
476 files changed
+59086
-7
No files matched your search
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* tests/3 — Deploy-to-clean-account smoke test.
|
||||
*
|
||||
* Asserts that a clean `wrangler deploy` of this Worker into an empty account
|
||||
* completes in under 4 seconds (Spin's MVP target). This test is gated on
|
||||
* CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID being set in the environment;
|
||||
* skipped in normal CI runs.
|
||||
*
|
||||
* Run with:
|
||||
* CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... \
|
||||
* npx vitest run test/deploy.test.ts
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { execSync, spawnSync } from 'node:child_process';
|
||||
|
||||
const HAS_CREDS = Boolean(process.env.CLOUDFLARE_API_TOKEN && process.env.CLOUDFLARE_ACCOUNT_ID);
|
||||
const TARGET_MS = 4000;
|
||||
|
||||
const describeIfCreds = HAS_CREDS ? describe : describe.skip;
|
||||
|
||||
describeIfCreds('deploy-to-clean-account speed', () => {
|
||||
const workerName = `turnstile-siteverify-deploytest-${Date.now()}`;
|
||||
|
||||
it(`completes wrangler deploy in under ${TARGET_MS}ms`, () => {
|
||||
const start = Date.now();
|
||||
|
||||
const result = spawnSync(
|
||||
'npx',
|
||||
['wrangler', 'deploy', '--name', workerName, '--dry-run', '--outdir', '/tmp/spin-deploy-test'],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env },
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
);
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`wrangler deploy --dry-run failed:\n${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(elapsed).toBeLessThan(TARGET_MS);
|
||||
}, 30_000);
|
||||
|
||||
it('cleans up the test Worker', () => {
|
||||
// Idempotent — if the dry-run never produced a real Worker, this is a no-op.
|
||||
execSync(`npx wrangler delete --name ${workerName} 2>&1 || true`, { stdio: 'ignore' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deploy artifacts present', () => {
|
||||
it('wrangler.toml has the expected name', () => {
|
||||
const fs = require('node:fs');
|
||||
const text = fs.readFileSync('wrangler.toml', 'utf-8');
|
||||
expect(text).toMatch(/^name\s*=\s*"turnstile-siteverify"/m);
|
||||
});
|
||||
|
||||
it('wrangler.toml configures the post-deploy assets directory', () => {
|
||||
const fs = require('node:fs');
|
||||
const text = fs.readFileSync('wrangler.toml', 'utf-8');
|
||||
expect(text).toMatch(/\[assets\]/);
|
||||
expect(text).toMatch(/directory\s*=\s*"\.\/public"/);
|
||||
});
|
||||
|
||||
it('public/post-deploy.html exists', () => {
|
||||
const fs = require('node:fs');
|
||||
expect(fs.existsSync('public/post-deploy.html')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Integration tests that hit Cloudflare's real siteverify endpoint using the
|
||||
* documented test sitekeys + test secrets.
|
||||
*
|
||||
* Test keys reference:
|
||||
* https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
*
|
||||
* Test secrets:
|
||||
* 1x0000000000000000000000000000000AA — always passes
|
||||
* 2x0000000000000000000000000000000AA — always fails
|
||||
* 3x0000000000000000000000000000000AA — already-spent token (timeout-or-duplicate)
|
||||
*
|
||||
* Test sitekeys:
|
||||
* 1x00000000000000000000AA — visible, always passes
|
||||
* 2x00000000000000000000AB — visible, always blocks
|
||||
* 1x00000000000000000000BB — invisible, always passes
|
||||
* 2x00000000000000000000BB — invisible, always blocks
|
||||
* 3x00000000000000000000FF — forces an interactive challenge
|
||||
*
|
||||
* These tests make real network calls. Run with `npm run test:integration`.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import worker from '../src/index.js';
|
||||
import type { Env } from '../src/types.js';
|
||||
|
||||
const PASSING_SECRET = '1x0000000000000000000000000000000AA';
|
||||
const FAILING_SECRET = '2x0000000000000000000000000000000AA';
|
||||
const SPENT_TOKEN_SECRET = '3x0000000000000000000000000000000AA';
|
||||
|
||||
// The widget produces some opaque token string. For test secrets the actual
|
||||
// content doesn't matter; siteverify decides based on the secret.
|
||||
const FAKE_TOKEN = 'XXXX.DUMMY.TOKEN.XXXX';
|
||||
|
||||
function envWith(secret: string): Env {
|
||||
return {
|
||||
TURNSTILE_SECRET_KEY: secret,
|
||||
ALLOWED_ORIGIN: '*',
|
||||
};
|
||||
}
|
||||
|
||||
describe('integration: real siteverify with test secrets', () => {
|
||||
it('passing-secret returns success=true', async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: FAKE_TOKEN }),
|
||||
}),
|
||||
envWith(PASSING_SECRET),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean; _worker?: { duration_ms: number } };
|
||||
expect(body.success).toBe(true);
|
||||
expect(body._worker?.duration_ms).toBeGreaterThan(0);
|
||||
}, 15_000);
|
||||
|
||||
it('failing-secret returns success=false', async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: FAKE_TOKEN }),
|
||||
}),
|
||||
envWith(FAILING_SECRET),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[] };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes'].length).toBeGreaterThan(0);
|
||||
}, 15_000);
|
||||
|
||||
it('spent-token secret returns timeout-or-duplicate', async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: FAKE_TOKEN }),
|
||||
}),
|
||||
envWith(SPENT_TOKEN_SECRET),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[] };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes']).toContain('timeout-or-duplicate');
|
||||
}, 15_000);
|
||||
});
|
||||
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* Unit tests for the Worker. These don't hit Cloudflare's real siteverify —
|
||||
* see integration.test.ts for that.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import worker from '../src/index.js';
|
||||
import type { Env } from '../src/types.js';
|
||||
|
||||
const TEST_ENV: Env = {
|
||||
TURNSTILE_SECRET_KEY: 'test-secret-key',
|
||||
ALLOWED_ORIGIN: '*',
|
||||
};
|
||||
|
||||
const SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
|
||||
function mockFetchOnce(response: { status?: number; body: unknown }): void {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async () => {
|
||||
return new Response(JSON.stringify(response.body), {
|
||||
status: response.status ?? 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('CORS', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('responds to OPTIONS preflight with CORS headers', async () => {
|
||||
const res = await worker.fetch(new Request('https://w/', { method: 'OPTIONS' }), TEST_ENV);
|
||||
expect(res.status).toBe(204);
|
||||
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
|
||||
expect(res.headers.get('Access-Control-Allow-Methods')).toContain('POST');
|
||||
});
|
||||
|
||||
it('echoes ALLOWED_ORIGIN when set explicitly', async () => {
|
||||
const env = { ...TEST_ENV, ALLOWED_ORIGIN: 'https://example.com' };
|
||||
const res = await worker.fetch(new Request('https://w/', { method: 'OPTIONS' }), env);
|
||||
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('https://example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health endpoints', () => {
|
||||
it('GET / returns {ok: true, version}', async () => {
|
||||
const res = await worker.fetch(new Request('https://w/'), TEST_ENV);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { ok: boolean; version: string; service: string };
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(body.service).toBe('turnstile-siteverify');
|
||||
});
|
||||
|
||||
it('GET /health returns the same shape', async () => {
|
||||
const res = await worker.fetch(new Request('https://w/health'), TEST_ENV);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { ok: boolean; version: string };
|
||||
expect(body.ok).toBe(true);
|
||||
expect(typeof body.version).toBe('string');
|
||||
});
|
||||
|
||||
it('GET /healthz still returns health (legacy alias)', async () => {
|
||||
const res = await worker.fetch(new Request('https://w/healthz'), TEST_ENV);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { ok: boolean };
|
||||
expect(body.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST / (siteverify proxy) — JSON body', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('forwards a JSON token to siteverify and returns success', async () => {
|
||||
mockFetchOnce({
|
||||
body: {
|
||||
success: true,
|
||||
hostname: 'example.com',
|
||||
challenge_ts: '2026-05-22T12:00:00Z',
|
||||
'error-codes': [],
|
||||
},
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean; _worker?: { worker_version: string } };
|
||||
expect(body.success).toBe(true);
|
||||
expect(body._worker?.worker_version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
|
||||
// Verify it called the right upstream
|
||||
expect(globalThis.fetch).toHaveBeenCalledOnce();
|
||||
expect((globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0]?.[0]).toBe(SITEVERIFY_URL);
|
||||
});
|
||||
|
||||
it('passes through siteverify failure with success=false', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: false, 'error-codes': ['invalid-input-response'] },
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'bad' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[] };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes']).toContain('invalid-input-response');
|
||||
});
|
||||
|
||||
it('returns 400 when token is missing', async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[] };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes']).toContain('missing-token');
|
||||
});
|
||||
|
||||
it('returns 400 on malformed JSON', async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{not json',
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('accepts cf-turnstile-response as alias for token', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, 'error-codes': [] },
|
||||
});
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ 'cf-turnstile-response': 'xyz' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST / (siteverify proxy) — form body', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('accepts urlencoded body', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, 'error-codes': [] },
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'token=abc',
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean };
|
||||
expect(body.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts cf-turnstile-response from form body', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, 'error-codes': [] },
|
||||
});
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'cf-turnstile-response=xyz',
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content-Type handling', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('returns 415 for unsupported content type', async () => {
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: 'token=abc',
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
expect(res.status).toBe(415);
|
||||
const body = (await res.json()) as { 'error-codes': string[] };
|
||||
expect(body['error-codes']).toContain('invalid-content-type');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Path routing', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('accepts POST /siteverify as alias of POST /', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, 'error-codes': [] },
|
||||
});
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('returns 404 for unknown paths', async () => {
|
||||
const res = await worker.fetch(new Request('https://w/random'), TEST_ENV);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CF-Connecting-IP fallback', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('uses CF-Connecting-IP if customer did not pass remoteip', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async (_url, init) => {
|
||||
const form = init?.body as FormData;
|
||||
expect(form.get('remoteip')).toBe('1.2.3.4');
|
||||
return new Response(JSON.stringify({ success: true, 'error-codes': [] }), { status: 200 });
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'CF-Connecting-IP': '1.2.3.4',
|
||||
},
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(fetchSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('prefers explicit remoteip over CF-Connecting-IP', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async (_url, init) => {
|
||||
const form = init?.body as FormData;
|
||||
expect(form.get('remoteip')).toBe('9.9.9.9');
|
||||
return new Response(JSON.stringify({ success: true, 'error-codes': [] }), { status: 200 });
|
||||
});
|
||||
|
||||
await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'CF-Connecting-IP': '1.2.3.4',
|
||||
},
|
||||
body: JSON.stringify({ token: 'abc', remoteip: '9.9.9.9' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hostname-mismatch defense', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('rejects success response when hostname does not match EXPECTED_HOSTNAME', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, hostname: 'attacker.com', 'error-codes': [] },
|
||||
});
|
||||
const env: Env = { ...TEST_ENV, EXPECTED_HOSTNAME: 'mysite.com' };
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
env,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[] };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes']).toContain('hostname-mismatch');
|
||||
});
|
||||
|
||||
it('accepts success response when hostname matches EXPECTED_HOSTNAME', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, hostname: 'mysite.com', 'error-codes': [] },
|
||||
});
|
||||
const env: Env = { ...TEST_ENV, EXPECTED_HOSTNAME: 'mysite.com' };
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
env,
|
||||
);
|
||||
const body = (await res.json()) as { success: boolean };
|
||||
expect(body.success).toBe(true);
|
||||
});
|
||||
|
||||
it('does not apply hostname check when EXPECTED_HOSTNAME is unset', async () => {
|
||||
mockFetchOnce({
|
||||
body: { success: true, hostname: 'anywhere.com', 'error-codes': [] },
|
||||
});
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
TEST_ENV,
|
||||
);
|
||||
const body = (await res.json()) as { success: boolean };
|
||||
expect(body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Missing secret', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('returns 500 with missing-input-secret when TURNSTILE_SECRET_KEY is empty', async () => {
|
||||
const env = { ...TEST_ENV, TURNSTILE_SECRET_KEY: '' };
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
env,
|
||||
);
|
||||
expect(res.status).toBe(500);
|
||||
const body = (await res.json()) as { 'error-codes': string[] };
|
||||
expect(body['error-codes']).toContain('missing-input-secret');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* tests/4 — Validation surface.
|
||||
*
|
||||
* Tests the endpoints that Turnstile Spin's wizard relies on during its
|
||||
* post-deploy validation step:
|
||||
*
|
||||
* - GET /health returns {ok: true, version}
|
||||
* - GET / returns the same shape
|
||||
* - POST / with dummy returns a structured error (success: false, error-codes,
|
||||
* _worker metadata) rather than a bare 500
|
||||
* - hostname mismatch when EXPECTED_HOSTNAME is set
|
||||
*
|
||||
* These match the assertions in the wizard's Step 7. If any of these fail,
|
||||
* the wizard's validation step will fail, and the customer will see a
|
||||
* misleading "setup complete" message followed by a broken integration.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import worker, { WORKER_VERSION } from '../src/index.js';
|
||||
import type { Env } from '../src/types.js';
|
||||
|
||||
const ENV: Env = {
|
||||
TURNSTILE_SECRET_KEY: 'test-secret-key',
|
||||
ALLOWED_ORIGIN: '*',
|
||||
};
|
||||
|
||||
describe('validation surface — /health', () => {
|
||||
it('returns ok:true and a semver version', async () => {
|
||||
const res = await worker.fetch(new Request('https://w/health'), ENV);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { ok: boolean; version: string };
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(body.version).toBe(WORKER_VERSION);
|
||||
});
|
||||
|
||||
it('GET / returns the same shape as /health', async () => {
|
||||
const [a, b] = await Promise.all([
|
||||
worker.fetch(new Request('https://w/'), ENV),
|
||||
worker.fetch(new Request('https://w/health'), ENV),
|
||||
]);
|
||||
const ja = (await a.json()) as { ok: boolean; version: string };
|
||||
const jb = (await b.json()) as { ok: boolean; version: string };
|
||||
expect(ja.ok).toBe(jb.ok);
|
||||
expect(ja.version).toBe(jb.version);
|
||||
});
|
||||
|
||||
it('responds in well under the wizard timeout (1s)', async () => {
|
||||
const start = Date.now();
|
||||
await worker.fetch(new Request('https://w/health'), ENV);
|
||||
expect(Date.now() - start).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation surface — dummy siteverify returns structured error', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('returns success:false + error-codes + _worker for a dummy token', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, 'error-codes': ['invalid-input-response'] }),
|
||||
{ status: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'XXXX.DUMMY.TOKEN.XXXX' }),
|
||||
}),
|
||||
ENV,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const body = (await res.json()) as {
|
||||
success: boolean;
|
||||
'error-codes': string[];
|
||||
_worker?: { duration_ms: number; worker_version: string };
|
||||
};
|
||||
|
||||
// Wizard's required assertions:
|
||||
expect(body.success).toBe(false);
|
||||
expect(Array.isArray(body['error-codes'])).toBe(true);
|
||||
expect(body['error-codes'].length).toBeGreaterThan(0);
|
||||
expect(body._worker).toBeDefined();
|
||||
expect(typeof body._worker?.duration_ms).toBe('number');
|
||||
expect(body._worker?.worker_version).toBe(WORKER_VERSION);
|
||||
});
|
||||
|
||||
it('returns structured error on Worker-level failure (no bare 500)', async () => {
|
||||
// Force the upstream to be unreachable.
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async () => {
|
||||
throw new TypeError('network unreachable');
|
||||
});
|
||||
// Disable the retry loop by also failing the second attempt.
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async () => {
|
||||
throw new TypeError('network unreachable');
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'whatever' }),
|
||||
}),
|
||||
ENV,
|
||||
);
|
||||
|
||||
// Worker-level failure still returns a *parseable* error body.
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[]; _worker?: unknown };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes'][0]).toBe('upstream-unreachable');
|
||||
expect(body._worker).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation surface — hostname mismatch', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('rejects success when EXPECTED_HOSTNAME does not match', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, hostname: 'attacker.example.com', 'error-codes': [] }),
|
||||
{ status: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
const env: Env = { ...ENV, EXPECTED_HOSTNAME: 'www.example.com' };
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
env,
|
||||
);
|
||||
|
||||
const body = (await res.json()) as { success: boolean; 'error-codes': string[] };
|
||||
expect(body.success).toBe(false);
|
||||
expect(body['error-codes']).toContain('hostname-mismatch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation surface — telemetry marker passthrough', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('passes data-action through to siteverify and surfaces it in the response', async () => {
|
||||
// Upstream echoes the action field. Spin marker should survive.
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
hostname: 'example.com',
|
||||
action: 'turnstile-spin-v1',
|
||||
'error-codes': [],
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
const res = await worker.fetch(
|
||||
new Request('https://w/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'abc' }),
|
||||
}),
|
||||
ENV,
|
||||
);
|
||||
|
||||
const body = (await res.json()) as { success: boolean; action?: string };
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.action).toBe('turnstile-spin-v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation surface — post-deploy form is reachable', () => {
|
||||
it('GET /post-deploy returns 404 when ASSETS binding is missing (test env)', async () => {
|
||||
// In the unit-test env we don't have the ASSETS binding wired, so the
|
||||
// handler falls through. The real Worker has the binding from wrangler.toml.
|
||||
const res = await worker.fetch(new Request('https://w/post-deploy'), ENV);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('GET /post-deploy serves the HTML file when ASSETS is bound', async () => {
|
||||
const html = '<!doctype html><title>Post-deploy</title>';
|
||||
const mockAssets: Fetcher = {
|
||||
fetch: async () =>
|
||||
new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }),
|
||||
} as unknown as Fetcher;
|
||||
|
||||
const envWithAssets: Env = { ...ENV, ASSETS: mockAssets };
|
||||
const res = await worker.fetch(new Request('https://w/post-deploy'), envWithAssets);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toContain('Post-deploy');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user