Sharkey/packages/backend/test/e2e/fetch-resource.ts

461 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js';
import type { SimpleGetResponse } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
// Request Accept
const ONLY_AP = 'application/activity+json';
const PREFER_AP = 'application/activity+json, */*';
const PREFER_HTML = 'text/html, */*';
const UNSPECIFIED = '*/*';
// Response Content-Type
const AP = 'application/activity+json; charset=utf-8';
const HTML = 'text/html; charset=utf-8';
const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => {
let app: INestApplicationContext;
let alice: any;
let aliceUploadedFile: any;
let alicesPost: any;
let alicePage: any;
let alicePlay: any;
let aliceClip: any;
let aliceGalleryPost: any;
let aliceChannel: any;
type Request = {
path: string,
accept?: string,
cookie?: string,
};
const ok = async (param: Request & {
type?: string,
}):Promise<SimpleGetResponse> => {
const { path, accept, cookie, type } = param;
const res = await simpleGet(path, accept, cookie);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, type ?? HTML);
return res;
};
const notOk = async (param: Request & {
status?: number,
code?: string,
}): Promise<SimpleGetResponse> => {
const { path, accept, cookie, status, code } = param;
const res = await simpleGet(path, accept, cookie);
assert.notStrictEqual(res.status, 200);
if (status != null) {
assert.strictEqual(res.status, status);
}
if (code != null) {
assert.strictEqual(res.body.error.code, code);
}
return res;
};
const notFound = async (param: Request): Promise<SimpleGetResponse> => {
return await notOk({
...param,
status: 404,
});
};
const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => {
return res.body.window.document.querySelector('meta[' + superkey + '="' + key + '"]')?.content;
};
beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' });
aliceUploadedFile = await uploadFile(alice);
alicesPost = await post(alice, {
text: 'test',
});
alicePage = await page(alice, {});
alicePlay = await play(alice, {});
aliceClip = await clip(alice, {});
aliceGalleryPost = await galleryPost(alice, {
fileIds: [aliceUploadedFile.body.id],
});
aliceChannel = await channel(alice, {});
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe.each([
{ path: '/', type: HTML },
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
// fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
{ path: '/api-doc', type: 'text/html; charset=UTF-8' },
{ path: '/api.json', type: JSON_UTF8 },
{ path: '/api-console', type: HTML },
{ path: '/_info_card_', type: HTML },
{ path: '/bios', type: HTML },
{ path: '/cli', type: HTML },
{ path: '/flush', type: HTML },
{ path: '/robots.txt', type: 'text/plain; charset=UTF-8' },
{ path: '/favicon.ico', type: 'image/vnd.microsoft.icon' },
{ path: '/opensearch.xml', type: 'application/opensearchdescription+xml' },
{ path: '/apple-touch-icon.png', type: 'image/png' },
{ path: '/twemoji/2764.svg', type: 'image/svg+xml' },
{ path: '/twemoji/2764-fe0f-200d-1f525.svg', type: 'image/svg+xml' },
{ path: '/twemoji-badge/2764.png', type: 'image/png' },
{ path: '/twemoji-badge/2764-fe0f-200d-1f525.png', type: 'image/png' },
{ path: '/fluent-emoji/2764.png', type: 'image/png' },
{ path: '/fluent-emoji/2764-fe0f-200d-1f525.png', type: 'image/png' },
])('$path', (p) => {
test('GETできる', async () => await ok({ ...p }));
// 注意: Webページが200で取得できても、実際のHTMLが正しく表示できるとは限らない
// 例えば、 /@xxx/pages/yyy に存在しないIDを渡した場合、HTTPレスポンスではエラーを区別できない
// こういったアサーションはフロントエンドE2EやAPI Endpointのテストで担保する。
});
describe.each([
{ path: '/twemoji/2764.png' },
{ path: '/twemoji/2764-fe0f-200d-1f525.png' },
{ path: '/twemoji-badge/2764.svg' },
{ path: '/twemoji-badge/2764-fe0f-200d-1f525.svg' },
{ path: '/fluent-emoji/2764.svg' },
{ path: '/fluent-emoji/2764-fe0f-200d-1f525.svg' },
])('$path', ({ path }) => {
test('GETできない', async () => await notFound({ path }));
});
describe.each([
{ ext: 'rss', type: 'application/rss+xml; charset=utf-8' },
{ ext: 'atom', type: 'application/atom+xml; charset=utf-8' },
{ ext: 'json', type: 'application/json; charset=utf-8' },
])('/@:username.$ext', ({ ext, type }) => {
const path = (username: string): string => `/@${username}.${ext}`;
test('GETできる', async () => await ok({
path: path(alice.username),
type,
}));
test('GETできない', async () => await notOk({
path: path('nonexisting'),
status: 404,
}));
});
describe.each([{ path: '/api/foo' }])('$path', ({ path }) => {
test('GETできない', async () => await notOk({
path,
status: 404,
code: 'UNKNOWN_API_ENDPOINT',
}));
});
describe.each([{ path: '/queue' }])('$path', ({ path }) => {
test('adminでなければGETできない', async () => await notOk({
path,
status: 500, // FIXME? 403ではない。
}));
test('adminならGETできる', async () => await ok({
path,
cookie: cookie(alice),
}));
});
describe.each([{ path: '/streaming' }])('$path', ({ path }) => {
test('GETできない', async () => await notOk({
path,
status: 503,
}));
});
describe('/@:username', () => {
const path = (username: string): string => `/@${username}`;
describe.each([
{ accept: PREFER_HTML },
{ accept: UNSPECIFIED },
])('(Acceptヘッダ: $accept)', ({ accept }) => {
test('HTMLとしてGETできる', async () => {
const res = await ok({
path: path(alice.username),
accept,
type: HTML,
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
// TODO ogタグの検証
// TODO profile.noCrawleの検証
// TODO twitter:creatorの検証
// TODO <link rel="me" ...>の検証
});
test('HTMLとしてGETできる(IDでも)', async () => await ok({
path: path('xxxxxxxxxx'),
type: HTML,
}));
});
describe.each([
{ accept: ONLY_AP },
{ accept: PREFER_AP },
])('(Acceptヘッダ: $accept)', ({ accept }) => {
test('ActivityPubとしてGETできる', async () => {
const res = await ok({
path: path(alice.username),
accept,
type: AP,
});
assert.strictEqual(res.body.type, 'Person');
});
test('IDのときActivityPubとしてGETできない', async () => await notFound({
path: path('xxxxxxxxxx'),
accept,
}));
});
});
describe.each([
// 実際のハンドルはフロントエンド(index.vue)で行われる
{ sub: 'home' },
{ sub: 'notes' },
{ sub: 'activity' },
{ sub: 'achievements' },
{ sub: 'reactions' },
{ sub: 'clips' },
{ sub: 'pages' },
{ sub: 'gallery' },
])('/@:username/$sub', ({ sub }) => {
const path = (username: string): string => `/@${username}/${sub}`;
test('HTMLとしてGETできる', async () => {
const res = await ok({
path: path(alice.username),
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
});
});
describe('/@:user/pages/:page', () => {
const path = (username: string, pagename: string): string => `/@${username}/pages/${pagename}`;
test('HTMLとしてGETできる', async () => {
const res = await ok({
path: path(alice.username, alicePage.name),
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
assert.strictEqual(metaTag(res, 'misskey:page-id'), alicePage.id);
// TODO ogタグの検証
// TODO profile.noCrawleの検証
// TODO twitter:creatorの検証
});
test('GETできる(IDでも)', async () => await ok({
path: path(alice.username, 'xxxxxxxxxx'),
}));
});
describe('/users/:id', () => {
const path = (id: string): string => `/users/${id}`;
describe.each([
{ accept: PREFER_HTML },
{ accept: UNSPECIFIED },
])('(Acceptヘッダ: $accept)', ({ accept }) => {
test('/@:usernameにリダイレクトする', async () => {
const res = await simpleGet(path(alice.id), accept);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
});
test('GETできない', async () => await notFound({
path: path('xxxxxxxx'),
}));
});
describe.each([
{ accept: ONLY_AP },
{ accept: PREFER_AP },
])('(Acceptヘッダ: $accept)', ({ accept }) => {
test('ActivityPubとしてGETできる', async () => {
const res = await ok({
path: path(alice.id),
accept,
type: AP,
});
assert.strictEqual(res.body.type, 'Person');
});
test('IDのときActivityPubとしてGETできない', async () => await notOk({
path: path('xxxxxxxx'),
accept,
status: 404,
}));
});
});
describe('/users/inbox', () => {
test('GETできる(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({
path: '/inbox',
}));
// test.todo('POSTできる');
});
describe('/users/:id/inbox', () => {
const path = (id: string): string => `/users/${id}/inbox`;
test('GETできる(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({
path: path(alice.id),
}));
// test.todo('POSTできる');
});
describe('/users/:id/outbox', () => {
const path = (id: string): string => `/users/${id}/outbox`;
test('GETできる', async () => {
const res = await ok({
path: path(alice.id),
type: AP,
});
assert.strictEqual(res.body.type, 'OrderedCollection');
});
});
describe('/notes/:id', () => {
const path = (noteId: string): string => `/notes/${noteId}`;
describe.each([
{ accept: PREFER_HTML },
{ accept: UNSPECIFIED },
])('(Acceptヘッダ: $accept)', ({ accept }) => {
test('HTMLとしてGETできる', async () => {
const res = await ok({
path: path(alicesPost.id),
accept,
type: HTML,
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
assert.strictEqual(metaTag(res, 'misskey:note-id'), alicesPost.id);
// TODO ogタグの検証
// TODO profile.noCrawleの検証
// TODO twitter:creatorの検証
});
test('HTMLとしてGETできる(IDでも)', async () => await ok({
path: path('xxxxxxxxxx'),
}));
});
describe.each([
{ accept: ONLY_AP },
{ accept: PREFER_AP },
])('(Acceptヘッダ: $accept)', ({ accept }) => {
test('ActivityPubとしてGETできる', async () => {
const res = await ok({
path: path(alicesPost.id),
accept,
type: AP,
});
assert.strictEqual(res.body.type, 'Note');
});
test('IDのときActivityPubとしてGETできない', async () => await notFound({
path: path('xxxxxxxxxx'),
accept,
}));
});
});
describe('/play/:id', () => {
const path = (playid: string): string => `/play/${playid}`;
test('GETできる', async () => {
const res = await ok({
path: path(alicePlay.id),
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
assert.strictEqual(metaTag(res, 'misskey:flash-id'), alicePlay.id);
// TODO ogタグの検証
// TODO profile.noCrawleの検証
// TODO twitter:creatorの検証
});
test('GETできる(IDでも)', async () => await ok({
path: path('xxxxxxxxxx'),
}));
});
describe('/clips/:clip', () => {
const path = (clip: string): string => `/clips/${clip}`;
test('GETできる', async () => {
const res = await ok({
path: path(aliceClip.id),
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
assert.strictEqual(metaTag(res, 'misskey:clip-id'), aliceClip.id);
// TODO ogタグの検証
// TODO profile.noCrawleの検証
});
test('GETできる(IDでも)', async () => await ok({
path: path('xxxxxxxxxx'),
}));
});
describe('/gallery/:post', () => {
const path = (post: string): string => `/gallery/${post}`;
test('GETできる', async () => {
const res = await ok({
path: path(aliceGalleryPost.id),
});
assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username);
assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id);
// FIXME: misskey:gallery-post-idみたいなmetaタグの設定がない
// TODO profile.noCrawleの検証
// TODO twitter:creatorの検証
});
test('GETできる(IDでも)', async () => await ok({
path: path('xxxxxxxxxx'),
}));
});
describe('/channels/:channel', () => {
const path = (channel: string): string => `/channels/${channel}`;
test('GETできる', async () => {
const res = await ok({
path: path(aliceChannel.id),
});
// FIXME: misskey関連のmetaタグの設定がない
// TODO ogタグの検証
});
test('GETできる(IDでも)', async () => await ok({
path: path('xxxxxxxxxx'),
}));
});
});