mirror of
https://github.com/immich-app/services.git
synced 2026-09-30 13:23:10 +08:00
chore(deps): update dependency vitest to v4 (#49)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
co-authored by
Daniel Dietzler
parent
17a90269e1
commit
67b9ac4b19
@@ -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';
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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 ?? '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
+11781
-8
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
+14622
-19
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["./worker-configuration.d.ts", "vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "worker-configuration.d.ts"]
|
||||||
|
}
|
||||||
Vendored
+11749
-6
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -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",
|
||||||
|
|||||||
Generated
+427
-791
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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' },
|
}),
|
||||||
},
|
],
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user