chore(deps): update dependency vitest to v4 (#49)

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
renovate[bot]
2026-05-14 19:44:44 +02:00
committed by GitHub
co-authored by Daniel Dietzler
parent 17a90269e1
commit 67b9ac4b19
19 changed files with 53328 additions and 962 deletions
+5 -4
View File
@@ -1,23 +1,24 @@
import { createExecutionContext, createScheduledController, env, SELF, waitOnExecutionContext } from 'cloudflare:test'; import { createExecutionContext, createScheduledController, waitOnExecutionContext } from 'cloudflare:test';
import { env, exports } from 'cloudflare:workers';
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { __resetFlushStateForTests } from './flush-state.js'; import { __resetFlushStateForTests } from './flush-state.js';
import worker from './index.js'; import worker from './index.js';
describe('HTTP handler', () => { describe('HTTP handler', () => {
it('returns 200 for /health', async () => { it('returns 200 for /health', async () => {
const response = await SELF.fetch('https://example.com/health'); const response = await exports.default.fetch('https://example.com/health');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as { status: string }; const body = (await response.json()) as { status: string };
expect(body.status).toBe('ok'); expect(body.status).toBe('ok');
}); });
it('returns 404 for unknown routes', async () => { it('returns 404 for unknown routes', async () => {
const response = await SELF.fetch('https://example.com/nope'); const response = await exports.default.fetch('https://example.com/nope');
expect(response.status).toBe(404); expect(response.status).toBe(404);
}); });
it('returns 404 for /collect — the manual trigger endpoint has been removed', async () => { it('returns 404 for /collect — the manual trigger endpoint has been removed', async () => {
const response = await SELF.fetch('https://example.com/collect'); const response = await exports.default.fetch('https://example.com/collect');
expect(response.status).toBe(404); expect(response.status).toBe(404);
}); });
}); });
@@ -1,4 +1,4 @@
import { env } from 'cloudflare:test'; import { env } from 'cloudflare:workers';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { CloudflareMetricsCollector } from './collector.js'; import { CloudflareMetricsCollector } from './collector.js';
import { ALL_DATASETS } from './datasets.js'; import { ALL_DATASETS } from './datasets.js';
+2 -3
View File
@@ -1,10 +1,9 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers", "vitest/globals"], "types": ["@cloudflare/vitest-pool-workers/types", "vitest/globals", "node"],
"baseUrl": ".",
"paths": { "paths": {
"@immich-services/cloudflare-metrics/*": ["*"] "@immich-services/cloudflare-metrics/*": ["./src/*"]
} }
}, },
"include": ["src/**/*", "worker-configuration.d.ts"] "include": ["src/**/*", "worker-configuration.d.ts"]
+15 -14
View File
@@ -1,20 +1,21 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';
export default defineWorkersConfig({ export default defineConfig({
test: { test: {
exclude: ['**/node_modules/**', 'src/integration.test.ts'], exclude: ['**/node_modules/**', 'src/integration.test.ts'],
poolOptions: { },
workers: { plugins: [
wrangler: { configPath: './wrangler.toml' }, cloudflareTest({
miniflare: { wrangler: { configPath: './wrangler.toml' },
// Inject a fake VictoriaMetrics token so the InfluxMetricsProvider miniflare: {
// takes the real fetch path in tests — otherwise it short-circuits // Inject a fake VictoriaMetrics token so the InfluxMetricsProvider
// in its "no token" branch and we can't observe flush behaviour. // takes the real fetch path in tests — otherwise it short-circuits
bindings: { // in its "no token" branch and we can't observe flush behaviour.
VMETRICS_API_TOKEN: 'test-token', bindings: {
}, VMETRICS_API_TOKEN: 'test-token',
}, },
}, },
}, }),
}, ],
}); });
@@ -1,18 +1,19 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';
export default defineWorkersConfig({ export default defineConfig({
test: { test: {
include: ['src/integration.test.ts'], include: ['src/integration.test.ts'],
poolOptions: { },
workers: { plugins: [
wrangler: { configPath: './wrangler.toml' }, cloudflareTest({
miniflare: { wrangler: { configPath: './wrangler.toml' },
bindings: { miniflare: {
CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN ?? '', bindings: {
CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID ?? '', CLOUDFLARE_API_TOKEN: process.env.CLOUDFLARE_API_TOKEN ?? '',
}, CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID ?? '',
}, },
}, },
}, }),
}, ],
}); });
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,10 +1,10 @@
import { SELF } from 'cloudflare:test'; import { exports } from 'cloudflare:workers';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
describe('GitHub Approval Check Worker', () => { describe('GitHub Approval Check Worker', () => {
describe('Health Check', () => { describe('Health Check', () => {
it('should return healthy status', async () => { it('should return healthy status', async () => {
const response = await SELF.fetch('https://example.com/health'); const response = await exports.default.fetch('https://example.com/health');
const data = (await response.json()) as any; const data = (await response.json()) as any;
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -14,7 +14,7 @@ describe('GitHub Approval Check Worker', () => {
describe('Webhook Endpoint', () => { describe('Webhook Endpoint', () => {
it('should reject requests without signature', async () => { it('should reject requests without signature', async () => {
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -27,7 +27,7 @@ describe('GitHub Approval Check Worker', () => {
}); });
it('should return 404 for unknown paths', async () => { it('should return 404 for unknown paths', async () => {
const response = await SELF.fetch('https://example.com/unknown'); const response = await exports.default.fetch('https://example.com/unknown');
expect(response.status).toBe(404); expect(response.status).toBe(404);
}); });
}); });
+2 -3
View File
@@ -1,10 +1,9 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": ["@cloudflare/workers-types", "vitest/globals"], "types": ["./worker-configuration.d.ts", "vitest/globals"],
"baseUrl": ".",
"paths": { "paths": {
"@immich-services/github-approval-check/*": ["*"] "@immich-services/github-approval-check/*": ["./src/*"]
} }
}, },
"include": ["src/**/*", "worker-configuration.d.ts"] "include": ["src/**/*", "worker-configuration.d.ts"]
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,9 +1,9 @@
import { SELF } from 'cloudflare:test'; import { exports } from 'cloudflare:workers';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
describe('Hello Worker', () => { describe('Hello Worker', () => {
it('should return hello message on root path', async () => { it('should return hello message on root path', async () => {
const response = await SELF.fetch('https://example.com/'); const response = await exports.default.fetch('https://example.com/');
const data = (await response.json()) as any; const data = (await response.json()) as any;
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -12,7 +12,7 @@ describe('Hello Worker', () => {
}); });
it('should return health status', async () => { it('should return health status', async () => {
const response = await SELF.fetch('https://example.com/health'); const response = await exports.default.fetch('https://example.com/health');
const data = (await response.json()) as any; const data = (await response.json()) as any;
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -20,7 +20,7 @@ describe('Hello Worker', () => {
}); });
it('should greet with name parameter', async () => { it('should greet with name parameter', async () => {
const response = await SELF.fetch('https://example.com/api/greet?name=Claude'); const response = await exports.default.fetch('https://example.com/api/greet?name=Claude');
const data = (await response.json()) as any; const data = (await response.json()) as any;
expect(response.status).toBe(200); expect(response.status).toBe(200);
@@ -28,7 +28,7 @@ describe('Hello Worker', () => {
}); });
it('should return 404 for unknown routes', async () => { it('should return 404 for unknown routes', async () => {
const response = await SELF.fetch('https://example.com/unknown'); const response = await exports.default.fetch('https://example.com/unknown');
const data = (await response.json()) as any; const data = (await response.json()) as any;
expect(response.status).toBe(404); expect(response.status).toBe(404);
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["./worker-configuration.d.ts", "vitest/globals"]
},
"include": ["src/**/*", "worker-configuration.d.ts"]
}
+11749 -6
View File
File diff suppressed because it is too large Load Diff
+58 -64
View File
@@ -1,5 +1,5 @@
import { env, fetchMock, SELF } from 'cloudflare:test'; import { env, exports } from 'cloudflare:workers';
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { MemoryCache } from './memory-cache.js'; import { MemoryCache } from './memory-cache.js';
import { revalidationState, versionCache } from './version-service.js'; import { revalidationState, versionCache } from './version-service.js';
import { compareSemVer, isGreaterThan, parseSemVer } from './version.js'; import { compareSemVer, isGreaterThan, parseSemVer } from './version.js';
@@ -214,7 +214,7 @@ describe('Version Worker', () => {
describe('OPTIONS preflight', () => { describe('OPTIONS preflight', () => {
it('returns correct CORS headers', async () => { it('returns correct CORS headers', async () => {
const response = await SELF.fetch('https://example.com/version', { method: 'OPTIONS' }); const response = await exports.default.fetch('https://example.com/version', { method: 'OPTIONS' });
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS'); expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, POST, OPTIONS');
@@ -225,7 +225,7 @@ describe('Version Worker', () => {
describe('GET /health', () => { describe('GET /health', () => {
it('returns healthy status', async () => { it('returns healthy status', async () => {
const response = await SELF.fetch('https://example.com/health'); const response = await exports.default.fetch('https://example.com/health');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = await response.json(); const body = await response.json();
expect(body).toEqual({ status: 'ok' }); expect(body).toEqual({ status: 'ok' });
@@ -234,7 +234,7 @@ describe('Version Worker', () => {
describe('GET /version', () => { describe('GET /version', () => {
it('returns the latest version', async () => { it('returns the latest version', async () => {
const response = await SELF.fetch('https://example.com/version'); const response = await exports.default.fetch('https://example.com/version');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.version).toBe('v1.120.0'); expect(body.version).toBe('v1.120.0');
@@ -244,7 +244,7 @@ describe('Version Worker', () => {
it('awaits D1 on cold start rather than deferring', async () => { it('awaits D1 on cold start rather than deferring', async () => {
// Cache is empty (invalidated in beforeEach), D1 has data // Cache is empty (invalidated in beforeEach), D1 has data
// The first request must return real data, not 404 or empty // The first request must return real data, not 404 or empty
const response = await SELF.fetch('https://example.com/version'); const response = await exports.default.fetch('https://example.com/version');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.version).toBe('v1.120.0'); expect(body.version).toBe('v1.120.0');
@@ -252,44 +252,44 @@ describe('Version Worker', () => {
// Now delete D1 data — second request should come from cache, proving // Now delete D1 data — second request should come from cache, proving
// the first request populated the cache synchronously // the first request populated the cache synchronously
await env.VERSION_DB.exec('DELETE FROM releases'); await env.VERSION_DB.exec('DELETE FROM releases');
const second = await SELF.fetch('https://example.com/version'); const second = await exports.default.fetch('https://example.com/version');
expect(second.status).toBe(200); expect(second.status).toBe(200);
const secondBody = (await second.json()) as any; const secondBody = (await second.json()) as any;
expect(secondBody.version).toBe('v1.120.0'); expect(secondBody.version).toBe('v1.120.0');
}); });
it('does not include Cache-Control header (memory cached, not CDN)', async () => { it('does not include Cache-Control header (memory cached, not CDN)', async () => {
const response = await SELF.fetch('https://example.com/version'); const response = await exports.default.fetch('https://example.com/version');
expect(response.headers.get('Cache-Control')).toBeNull(); expect(response.headers.get('Cache-Control')).toBeNull();
}); });
it('returns 404 when no releases exist', async () => { it('returns 404 when no releases exist', async () => {
await env.VERSION_DB.exec('DELETE FROM releases'); await env.VERSION_DB.exec('DELETE FROM releases');
const response = await SELF.fetch('https://example.com/version'); const response = await exports.default.fetch('https://example.com/version');
expect(response.status).toBe(404); expect(response.status).toBe(404);
}); });
it('serves from in-memory cache on second request', async () => { it('serves from in-memory cache on second request', async () => {
const first = await SELF.fetch('https://example.com/version'); const first = await exports.default.fetch('https://example.com/version');
expect(first.status).toBe(200); expect(first.status).toBe(200);
// Delete from D1 - second request should still work from cache // Delete from D1 - second request should still work from cache
await env.VERSION_DB.exec('DELETE FROM releases'); await env.VERSION_DB.exec('DELETE FROM releases');
const second = await SELF.fetch('https://example.com/version'); const second = await exports.default.fetch('https://example.com/version');
expect(second.status).toBe(200); expect(second.status).toBe(200);
const body = (await second.json()) as any; const body = (await second.json()) as any;
expect(body.version).toBe('v1.120.0'); expect(body.version).toBe('v1.120.0');
}); });
it('includes CORS header', async () => { it('includes CORS header', async () => {
const response = await SELF.fetch('https://example.com/version'); const response = await exports.default.fetch('https://example.com/version');
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
}); });
it('serves stale data while revalidating in the background', async () => { it('serves stale data while revalidating in the background', async () => {
// Prime the cache // Prime the cache
const first = await SELF.fetch('https://example.com/version'); const first = await exports.default.fetch('https://example.com/version');
expect(first.status).toBe(200); expect(first.status).toBe(200);
// Expire the cache by invalidating and setting with 0 TTL // Expire the cache by invalidating and setting with 0 TTL
@@ -319,7 +319,7 @@ describe('Version Worker', () => {
.run(); .run();
// This request should get stale v1.120.0 while triggering background refresh // This request should get stale v1.120.0 while triggering background refresh
const stale = await SELF.fetch('https://example.com/version'); const stale = await exports.default.fetch('https://example.com/version');
expect(stale.status).toBe(200); expect(stale.status).toBe(200);
const staleBody = (await stale.json()) as any; const staleBody = (await stale.json()) as any;
expect(staleBody.version).toBe('v1.120.0'); expect(staleBody.version).toBe('v1.120.0');
@@ -328,7 +328,7 @@ describe('Version Worker', () => {
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
// Next request should get the updated version from refreshed cache // Next request should get the updated version from refreshed cache
const fresh = await SELF.fetch('https://example.com/version'); const fresh = await exports.default.fetch('https://example.com/version');
expect(fresh.status).toBe(200); expect(fresh.status).toBe(200);
const freshBody = (await fresh.json()) as any; const freshBody = (await fresh.json()) as any;
expect(freshBody.version).toBe('v1.130.0'); expect(freshBody.version).toBe('v1.130.0');
@@ -343,8 +343,8 @@ describe('Version Worker', () => {
// Fire two concurrent requests while stale // Fire two concurrent requests while stale
const [r1, r2] = await Promise.all([ const [r1, r2] = await Promise.all([
SELF.fetch('https://example.com/version'), exports.default.fetch('https://example.com/version'),
SELF.fetch('https://example.com/version'), exports.default.fetch('https://example.com/version'),
]); ]);
expect(r1.status).toBe(200); expect(r1.status).toBe(200);
@@ -360,21 +360,21 @@ describe('Version Worker', () => {
describe('GET /changelog', () => { describe('GET /changelog', () => {
it('returns 400 when version is missing', async () => { it('returns 400 when version is missing', async () => {
const response = await SELF.fetch('https://example.com/changelog'); const response = await exports.default.fetch('https://example.com/changelog');
expect(response.status).toBe(400); expect(response.status).toBe(400);
const body = (await response.json()) as { error: string }; const body = (await response.json()) as { error: string };
expect(body.error).toContain('version'); expect(body.error).toContain('version');
}); });
it('returns 400 for invalid version format', async () => { it('returns 400 for invalid version format', async () => {
const response = await SELF.fetch('https://example.com/changelog?version=invalid'); const response = await exports.default.fetch('https://example.com/changelog?version=invalid');
expect(response.status).toBe(400); expect(response.status).toBe(400);
const body = (await response.json()) as { error: string }; const body = (await response.json()) as { error: string };
expect(body.error).toContain('Invalid version'); expect(body.error).toContain('Invalid version');
}); });
it('returns releases newer than the provided version', async () => { it('returns releases newer than the provided version', async () => {
const response = await SELF.fetch('https://example.com/changelog?version=v1.100.0'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.100.0');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.current).toBe('v1.100.0'); expect(body.current).toBe('v1.100.0');
@@ -385,14 +385,14 @@ describe('Version Worker', () => {
}); });
it('returns empty releases when on latest version', async () => { it('returns empty releases when on latest version', async () => {
const response = await SELF.fetch('https://example.com/changelog?version=v1.120.0'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.120.0');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.releases).toHaveLength(0); expect(body.releases).toHaveLength(0);
}); });
it('handles version without v prefix', async () => { it('handles version without v prefix', async () => {
const response = await SELF.fetch('https://example.com/changelog?version=1.100.0'); const response = await exports.default.fetch('https://example.com/changelog?version=1.100.0');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.current).toBe('1.100.0'); expect(body.current).toBe('1.100.0');
@@ -400,20 +400,20 @@ describe('Version Worker', () => {
}); });
it('returns all releases when version is very old', async () => { it('returns all releases when version is very old', async () => {
const response = await SELF.fetch('https://example.com/changelog?version=v1.0.0'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.0.0');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.releases).toHaveLength(3); expect(body.releases).toHaveLength(3);
}); });
it('includes Cache-Control header for CDN caching', async () => { it('includes Cache-Control header for CDN caching', async () => {
const response = await SELF.fetch('https://example.com/changelog?version=v1.100.0'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.100.0');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=86400'); expect(response.headers.get('Cache-Control')).toBe('public, max-age=86400');
}); });
it('returns empty results when D1 has no data', async () => { it('returns empty results when D1 has no data', async () => {
await env.VERSION_DB.exec('DELETE FROM releases'); await env.VERSION_DB.exec('DELETE FROM releases');
const response = await SELF.fetch('https://example.com/changelog?version=v1.0.0'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.0.0');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.releases).toHaveLength(0); expect(body.releases).toHaveLength(0);
@@ -425,7 +425,7 @@ describe('Version Worker', () => {
const webhookSecret = 'test-secret'; const webhookSecret = 'test-secret';
it('returns 401 without signature', async () => { it('returns 401 without signature', async () => {
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body: '{}', body: '{}',
}); });
@@ -433,7 +433,7 @@ describe('Version Worker', () => {
}); });
it('returns 401 with invalid signature', async () => { it('returns 401 with invalid signature', async () => {
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body: '{}', body: '{}',
headers: { 'X-Hub-Signature-256': 'sha256=invalid' }, headers: { 'X-Hub-Signature-256': 'sha256=invalid' },
@@ -442,14 +442,14 @@ describe('Version Worker', () => {
}); });
it('returns 405 for non-POST requests', async () => { it('returns 405 for non-POST requests', async () => {
const response = await SELF.fetch('https://example.com/webhook'); const response = await exports.default.fetch('https://example.com/webhook');
expect(response.status).toBe(405); expect(response.status).toBe(405);
}); });
it('ignores non-release events', async () => { it('ignores non-release events', async () => {
const body = JSON.stringify({ action: 'opened' }); const body = JSON.stringify({ action: 'opened' });
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -465,7 +465,7 @@ describe('Version Worker', () => {
it('ignores non-published release actions', async () => { it('ignores non-published release actions', async () => {
const body = JSON.stringify({ action: 'created', release: {} }); const body = JSON.stringify({ action: 'created', release: {} });
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -480,7 +480,7 @@ describe('Version Worker', () => {
it('upserts a published release and invalidates cache', async () => { it('upserts a published release and invalidates cache', async () => {
// Prime the cache // Prime the cache
await SELF.fetch('https://example.com/version'); await exports.default.fetch('https://example.com/version');
const releasePayload = { const releasePayload = {
action: 'published', action: 'published',
@@ -497,7 +497,7 @@ describe('Version Worker', () => {
const body = JSON.stringify(releasePayload); const body = JSON.stringify(releasePayload);
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -510,7 +510,7 @@ describe('Version Worker', () => {
expect(result.success).toBe(true); expect(result.success).toBe(true);
// Verify the new release is now the latest (cache was invalidated) // Verify the new release is now the latest (cache was invalidated)
const versionResponse = await SELF.fetch('https://example.com/version'); const versionResponse = await exports.default.fetch('https://example.com/version');
const versionBody = (await versionResponse.json()) as any; const versionBody = (await versionResponse.json()) as any;
expect(versionBody.version).toBe('v1.130.0'); expect(versionBody.version).toBe('v1.130.0');
}); });
@@ -531,7 +531,7 @@ describe('Version Worker', () => {
const body = JSON.stringify(releasePayload); const body = JSON.stringify(releasePayload);
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
await SELF.fetch('https://example.com/webhook', { await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -540,7 +540,7 @@ describe('Version Worker', () => {
}, },
}); });
const response = await SELF.fetch('https://example.com/changelog?version=v1.110.0'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.110.0');
const changelog = (await response.json()) as any; const changelog = (await response.json()) as any;
expect(changelog.releases[0].body).toBe('Updated release notes'); expect(changelog.releases[0].body).toBe('Updated release notes');
}); });
@@ -553,7 +553,7 @@ describe('Version Worker', () => {
const body = JSON.stringify(releasePayload); const body = JSON.stringify(releasePayload);
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -581,7 +581,7 @@ describe('Version Worker', () => {
const body = JSON.stringify(releasePayload); const body = JSON.stringify(releasePayload);
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -611,7 +611,7 @@ describe('Version Worker', () => {
const body = JSON.stringify(releasePayload); const body = JSON.stringify(releasePayload);
const signature = await createWebhookSignature(body, webhookSecret); const signature = await createWebhookSignature(body, webhookSecret);
const response = await SELF.fetch('https://example.com/webhook', { const response = await exports.default.fetch('https://example.com/webhook', {
method: 'POST', method: 'POST',
body, body,
headers: { headers: {
@@ -627,7 +627,7 @@ describe('Version Worker', () => {
describe('Unknown routes', () => { describe('Unknown routes', () => {
it('returns 404 for unknown paths', async () => { it('returns 404 for unknown paths', async () => {
const response = await SELF.fetch('https://example.com/unknown'); const response = await exports.default.fetch('https://example.com/unknown');
expect(response.status).toBe(404); expect(response.status).toBe(404);
}); });
}); });
@@ -642,28 +642,30 @@ describe('Cron sync', () => {
versionCache.invalidate(); versionCache.invalidate();
revalidationState.inFlight = false; revalidationState.inFlight = false;
await env.VERSION_DB.exec('DELETE FROM releases'); await env.VERSION_DB.exec('DELETE FROM releases');
fetchMock.activate(); vi.spyOn(globalThis, 'fetch').mockImplementation((input, init) => {
fetchMock.disableNetConnect(); const request = new Request(input, init);
const url = new URL(request.url);
// Allow metrics flush (dev URL) if (
const metricsOrigin = fetchMock.get('https://cf-workers.monitoring.dev.immich.cloud'); request.method === 'GET' &&
metricsOrigin.intercept({ path: '/write', method: 'POST' }).reply(200, 'OK').persist(); url.origin === 'https://api.github.com' &&
url.pathname === '/repos/immich-app/immich/releases' &&
url.searchParams.get('per_page') === '100' &&
url.searchParams.get('page') === '1'
) {
return Promise.resolve(Response.json(mockReleases));
}
return fetch(input, init);
});
}); });
afterEach(() => { afterEach(() => {
fetchMock.deactivate(); vi.unstubAllGlobals();
}); });
it('fetches from GitHub and populates D1', async () => { it('fetches from GitHub and populates D1', async () => {
const github = fetchMock.get('https://api.github.com'); const response = await exports.default.fetch('https://example.com/changelog?version=v1.100.0');
github
.intercept({
path: '/repos/immich-app/immich/releases',
query: { per_page: '100', page: '1' },
})
.reply(200, mockReleases);
const response = await SELF.fetch('https://example.com/changelog?version=v1.100.0');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
// D1 is empty, so no releases // D1 is empty, so no releases
@@ -672,25 +674,17 @@ describe('Cron sync', () => {
// Seed directly to simulate cron populating D1 // Seed directly to simulate cron populating D1
await seedReleases(); await seedReleases();
const response2 = await SELF.fetch('https://example.com/changelog?version=v1.100.0'); const response2 = await exports.default.fetch('https://example.com/changelog?version=v1.100.0');
const body2 = (await response2.json()) as any; const body2 = (await response2.json()) as any;
expect(body2.releases).toHaveLength(2); expect(body2.releases).toHaveLength(2);
expect(body2.latest.tag_name).toBe('v1.120.0'); expect(body2.latest.tag_name).toBe('v1.120.0');
}); });
it('returns latest version from GitHub when cache is empty', async () => { it('returns latest version from GitHub when cache is empty', async () => {
const github = fetchMock.get('https://api.github.com');
github
.intercept({
path: '/repos/immich-app/immich/releases',
query: { per_page: '100', page: '1' },
})
.reply(200, mockReleases);
// Seed D1 directly // Seed D1 directly
await seedReleases(); await seedReleases();
const response = await SELF.fetch('https://example.com/version'); const response = await exports.default.fetch('https://example.com/version');
expect(response.status).toBe(200); expect(response.status).toBe(200);
const body = (await response.json()) as any; const body = (await response.json()) as any;
expect(body.version).toBe('v1.120.0'); expect(body.version).toBe('v1.120.0');
+2 -3
View File
@@ -1,10 +1,9 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers", "vitest/globals"], "types": ["./worker-configuration.d.ts", "vitest/globals"],
"baseUrl": ".",
"paths": { "paths": {
"@immich-services/version/*": ["*"] "@immich-services/version/*": ["./src/*"]
} }
}, },
"include": ["src/**/*", "worker-configuration.d.ts"] "include": ["src/**/*", "worker-configuration.d.ts"]
+14623 -11
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -11,11 +11,10 @@
"format": "prettier --check .", "format": "prettier --check .",
"format:fix": "prettier --write .", "format:fix": "prettier --write .",
"test": "pnpm -r test", "test": "pnpm -r test",
"check": "tsc --noEmit && pnpm -r typecheck" "check": "pnpm -r check"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.8.71", "@cloudflare/vitest-pool-workers": "^0.16.5",
"@cloudflare/workers-types": "^4.20241127.0",
"@eslint/js": "^9.35.0", "@eslint/js": "^9.35.0",
"@types/node": "^24.12.2", "@types/node": "^24.12.2",
"@typescript-eslint/eslint-plugin": "^8.19.0", "@typescript-eslint/eslint-plugin": "^8.19.0",
@@ -29,7 +28,7 @@
"prettier-plugin-organize-imports": "^4.2.0", "prettier-plugin-organize-imports": "^4.2.0",
"typescript": "^6.0.0", "typescript": "^6.0.0",
"typescript-eslint": "^8.43.0", "typescript-eslint": "^8.43.0",
"vitest": "^3.0.0", "vitest": "^4.0.0",
"wrangler": "^4.35.0" "wrangler": "^4.35.0"
}, },
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748", "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
+427 -791
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"extends": "./tsconfig.base.json", "extends": "./tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers", "@types/node"], "types": ["@types/node"],
"composite": true, "composite": true,
"declaration": true, "declaration": true,
"declarationMap": true "declarationMap": true
+8 -9
View File
@@ -1,11 +1,10 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';
export default defineWorkersConfig({ export default defineConfig({
test: { plugins: [
poolOptions: { cloudflareTest({
workers: { wrangler: { configPath: './wrangler.toml' },
wrangler: { configPath: './wrangler.toml' }, }),
}, ],
},
},
}); });