feat(server): self-hosted worker (#10085)

This commit is contained in:
DarkSky
2025-02-12 08:01:57 +08:00
committed by GitHub
parent 19f0eb1931
commit 88a3a2d13b
23 changed files with 817 additions and 20 deletions

View File

@@ -0,0 +1,78 @@
# Snapshot report for `src/__tests__/worker.e2e.ts`
The actual snapshot is saved in `worker.e2e.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should proxy image
> Snapshot 1
{}
> Snapshot 2
{
code: 'Bad Request',
message: 'Missing "url" parameter',
name: 'BAD_REQUEST',
status: 400,
type: 'BAD_REQUEST',
}
> Snapshot 3
{
code: 'Bad Request',
message: 'Invalid header',
name: 'BAD_REQUEST',
status: 400,
type: 'BAD_REQUEST',
}
> Snapshot 4
Buffer @Uint8Array [
66616b65 20696d61 6765
]
## should preview link
> Snapshot 1
{}
> Snapshot 2
{
code: 'Bad Request',
message: 'Invalid URL',
name: 'BAD_REQUEST',
status: 400,
type: 'BAD_REQUEST',
}
> Snapshot 3
{
code: 'Bad Request',
message: 'Invalid URL',
name: 'BAD_REQUEST',
status: 400,
type: 'BAD_REQUEST',
}
> Snapshot 4
{
description: 'Test Description',
favicons: [
'http://localhost:3010/api/worker/image-proxy?url=https%3A%2F%2Fexample.com%2Ffavicon.ico',
],
images: [
'http://localhost:3010/api/worker/image-proxy?url=https%3A%2F%2Fexample.com%2Fimage.png',
],
title: 'Test Title',
url: 'http://example.com/page',
videos: [],
}

View File

@@ -95,7 +95,7 @@ export class TestingApp extends ApplyType<INestApplication>() {
}
request(
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
method: 'options' | 'get' | 'post' | 'put' | 'delete' | 'patch',
path: string
): supertest.Test {
return supertest(this.getHttpServer())
@@ -106,6 +106,10 @@ export class TestingApp extends ApplyType<INestApplication>() {
]);
}
OPTIONS(path: string): supertest.Test {
return this.request('options', path);
}
GET(path: string): supertest.Test {
return this.request('get', path);
}

View File

@@ -0,0 +1,174 @@
import type { ExecutionContext, TestFn } from 'ava';
import ava from 'ava';
import Sinon from 'sinon';
import type { Response } from 'supertest';
import { WorkerModule } from '../plugins/worker';
import { createTestingApp, TestingApp } from './utils';
type TestContext = {
app: TestingApp;
};
const test = ava as TestFn<TestContext>;
test.before(async t => {
const app = await createTestingApp({
imports: [WorkerModule],
});
t.context.app = app;
});
test.after.always(async t => {
await t.context.app.close();
});
const assertAndSnapshotRaw = async (
t: ExecutionContext<TestContext>,
route: string,
message: string,
options?: {
status?: number;
origin?: string;
method?: 'GET' | 'OPTIONS' | 'POST';
body?: any;
checker?: (res: Response) => any;
}
) => {
const {
status = 200,
origin = 'http://localhost',
method = 'GET',
checker = () => {},
} = options || {};
const { app } = t.context;
const res = app[method](route)
.set('Origin', origin)
.send(options?.body)
.expect(status)
.expect(checker);
t.notThrowsAsync(res, message);
t.snapshot((await res).body);
};
test('should proxy image', async t => {
const assertAndSnapshot = assertAndSnapshotRaw.bind(null, t);
await assertAndSnapshot(
'/api/worker/image-proxy',
'should return proper CORS headers on OPTIONS request',
{
status: 204,
method: 'OPTIONS',
checker: (res: Response) => {
if (!res.headers['access-control-allow-methods']) {
throw new Error('Missing CORS headers');
}
},
}
);
{
await assertAndSnapshot(
'/api/worker/image-proxy',
'should return 400 if "url" query parameter is missing',
{ status: 400 }
);
}
{
await assertAndSnapshot(
'/api/worker/image-proxy?url=http://example.com/image.png',
'should return 400 for invalid origin header',
{ status: 400, origin: 'http://invalid.com' }
);
}
{
const fakeBuffer = Buffer.from('fake image');
const fakeResponse = {
ok: true,
headers: {
get: (header: string) => {
if (header.toLowerCase() === 'content-type') return 'image/png';
if (header.toLowerCase() === 'content-disposition') return 'inline';
return null;
},
},
arrayBuffer: async () => fakeBuffer,
} as any;
const fetchSpy = Sinon.stub(global, 'fetch').resolves(fakeResponse);
await assertAndSnapshot(
'/api/worker/image-proxy?url=http://example.com/image.png',
'should return image buffer'
);
fetchSpy.restore();
}
});
test('should preview link', async t => {
const assertAndSnapshot = assertAndSnapshotRaw.bind(null, t);
await assertAndSnapshot(
'/api/worker/link-preview',
'should return proper CORS headers on OPTIONS request',
{
status: 204,
method: 'OPTIONS',
checker: (res: Response) => {
if (!res.headers['access-control-allow-methods']) {
throw new Error('Missing CORS headers');
}
},
}
);
await assertAndSnapshot(
'/api/worker/link-preview',
'should return 400 if request body is invalid',
{ status: 400, method: 'POST' }
);
await assertAndSnapshot(
'/api/worker/link-preview',
'should return 400 if provided URL is from the same origin',
{ status: 400, method: 'POST', body: { url: 'http://localhost/somepage' } }
);
{
const fakeHTML = new Response(`
<html>
<head>
<meta property="og:title" content="Test Title" />
<meta property="og:description" content="Test Description" />
<meta property="og:image" content="http://example.com/image.png" />
</head>
<body>
<title>Fallback Title</title>
</body>
</html>
`);
Object.defineProperty(fakeHTML, 'url', {
value: 'http://example.com/page',
});
const fetchSpy = Sinon.stub(global, 'fetch').resolves(fakeHTML);
await assertAndSnapshot(
'/api/worker/link-preview',
'should process a valid external URL and return link preview data',
{
status: 200,
method: 'POST',
body: { url: 'http://external.com/page' },
}
);
fetchSpy.restore();
}
});