Merge tag '2023.12.1' into merge-upstream
This commit is contained in:
@@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js';
|
||||
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
@@ -89,6 +89,11 @@ describe('API', () => {
|
||||
});
|
||||
|
||||
test('管理者専用のAPIのアクセス制限', async () => {
|
||||
const application = await createAppToken(alice, ['read:account']);
|
||||
const application2 = await createAppToken(alice, ['read:admin:index-stats']);
|
||||
const application3 = await createAppToken(bob, []);
|
||||
const application4 = await createAppToken(bob, ['read:admin:index-stats']);
|
||||
|
||||
// aliceは管理者、APIを使える
|
||||
await successfulApiCall({
|
||||
endpoint: '/admin/get-index-stats',
|
||||
@@ -128,6 +133,42 @@ describe('API', () => {
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
});
|
||||
|
||||
await successfulApiCall({
|
||||
endpoint: '/admin/get-index-stats',
|
||||
parameters: {},
|
||||
user: { token: application2 },
|
||||
});
|
||||
|
||||
await failedApiCall({
|
||||
endpoint: '/admin/get-index-stats',
|
||||
parameters: {},
|
||||
user: { token: application },
|
||||
}, {
|
||||
status: 403,
|
||||
code: 'PERMISSION_DENIED',
|
||||
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
|
||||
});
|
||||
|
||||
await failedApiCall({
|
||||
endpoint: '/admin/get-index-stats',
|
||||
parameters: {},
|
||||
user: { token: application3 },
|
||||
}, {
|
||||
status: 403,
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
|
||||
});
|
||||
|
||||
await failedApiCall({
|
||||
endpoint: '/admin/get-index-stats',
|
||||
parameters: {},
|
||||
user: { token: application4 },
|
||||
}, {
|
||||
status: 403,
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication header', () => {
|
||||
|
40
packages/backend/test/e2e/nodeinfo.ts
Normal file
40
packages/backend/test/e2e/nodeinfo.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { relativeFetch, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
describe('nodeinfo', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('nodeinfo 2.1', async () => {
|
||||
const res = await relativeFetch('nodeinfo/2.1');
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
|
||||
const nodeInfo = await res.json() as any;
|
||||
assert.strictEqual(nodeInfo.software.name, 'misskey');
|
||||
});
|
||||
|
||||
test('nodeinfo 2.0', async () => {
|
||||
const res = await relativeFetch('nodeinfo/2.0');
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
|
||||
const nodeInfo = await res.json() as any;
|
||||
assert.strictEqual(nodeInfo.software.name, 'misskey');
|
||||
});
|
||||
});
|
@@ -941,4 +941,24 @@ describe('OAuth', () => {
|
||||
const response = await fetch(new URL('/oauth/foo', host));
|
||||
assert.strictEqual(response.status, 404);
|
||||
});
|
||||
|
||||
describe('CORS', () => {
|
||||
test('Token endpoint should support CORS', async () => {
|
||||
const response = await fetch(new URL('/oauth/token', host), { method: 'POST' });
|
||||
assert.ok(!response.ok);
|
||||
assert.strictEqual(response.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
});
|
||||
|
||||
test('Authorize endpoint should not support CORS', async () => {
|
||||
const response = await fetch(new URL('/oauth/authorize', host), { method: 'GET' });
|
||||
assert.ok(!response.ok);
|
||||
assert.ok(!response.headers.has('Access-Control-Allow-Origin'));
|
||||
});
|
||||
|
||||
test('Decision endpoint should not support CORS', async () => {
|
||||
const response = await fetch(new URL('/oauth/decision', host), { method: 'POST' });
|
||||
assert.ok(!response.ok);
|
||||
assert.ok(!response.headers.has('Access-Control-Allow-Origin'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -6,8 +6,9 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { WebSocket } from 'ws';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
import { signup, api, post, startServer, initTestDb, waitFire } from '../utils.js';
|
||||
import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
@@ -560,6 +561,28 @@ describe('Streaming', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Authentication', async () => {
|
||||
const application = await createAppToken(ayano, []);
|
||||
const application2 = await createAppToken(ayano, ['read:account']);
|
||||
const socket = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${application}`);
|
||||
const established = await new Promise<boolean>((resolve, reject) => {
|
||||
socket.on('error', () => resolve(false));
|
||||
socket.on('unexpected-response', () => resolve(false));
|
||||
setTimeout(() => resolve(true), 3000);
|
||||
});
|
||||
|
||||
socket.close();
|
||||
assert.strictEqual(established, false);
|
||||
|
||||
const fired = await waitFire(
|
||||
{ token: application2 }, 'hybridTimeline',
|
||||
() => api('notes/create', { text: 'Hello, world!' }, ayano),
|
||||
msg => msg.type === 'note' && msg.body.userId === ayano.id,
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
// XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
|
||||
/*
|
||||
describe('Hashtag Timeline', () => {
|
||||
|
111
packages/backend/test/e2e/well-known.ts
Normal file
111
packages/backend/test/e2e/well-known.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { host, origin, relativeFetch, signup, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('.well-known', () => {
|
||||
let app: INestApplicationContext;
|
||||
let alice: misskey.entities.User;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
||||
alice = await signup({ username: 'alice' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('nodeinfo', async () => {
|
||||
const res = await relativeFetch('.well-known/nodeinfo');
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
|
||||
const nodeInfo = await res.json();
|
||||
assert.deepStrictEqual(nodeInfo, {
|
||||
links: [{
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||
href: `${origin}/nodeinfo/2.1`,
|
||||
}, {
|
||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
href: `${origin}/nodeinfo/2.0`,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
test('webfinger', async () => {
|
||||
const preflight = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`, {
|
||||
method: 'options',
|
||||
headers: {
|
||||
'Access-Control-Request-Method': 'GET',
|
||||
Origin: 'http://example.com',
|
||||
},
|
||||
});
|
||||
assert.ok(preflight.ok);
|
||||
assert.strictEqual(preflight.headers.get('Access-Control-Allow-Headers'), 'Accept');
|
||||
|
||||
const res = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`);
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
assert.strictEqual(res.headers.get('Access-Control-Expose-Headers'), 'Vary');
|
||||
assert.strictEqual(res.headers.get('Vary'), 'Accept');
|
||||
|
||||
const webfinger = await res.json();
|
||||
|
||||
assert.deepStrictEqual(webfinger, {
|
||||
subject: `acct:alice@${host}`,
|
||||
links: [{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${origin}/users/${alice.id}`,
|
||||
}, {
|
||||
rel: 'http://webfinger.net/rel/profile-page',
|
||||
type: 'text/html',
|
||||
href: `${origin}/@alice`,
|
||||
}, {
|
||||
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
||||
template: `${origin}/authorize-follow?acct={uri}`,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
test('host-meta', async () => {
|
||||
const res = await relativeFetch('.well-known/host-meta');
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
});
|
||||
|
||||
test('host-meta.json', async () => {
|
||||
const res = await relativeFetch('.well-known/host-meta.json');
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
|
||||
const hostMeta = await res.json();
|
||||
assert.deepStrictEqual(hostMeta, {
|
||||
links: [{
|
||||
rel: 'lrdd',
|
||||
type: 'application/jrd+json',
|
||||
template: `${origin}/.well-known/webfinger?resource={uri}`,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
test('oauth-authorization-server', async () => {
|
||||
const res = await relativeFetch('.well-known/oauth-authorization-server');
|
||||
assert.ok(res.ok);
|
||||
assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
|
||||
|
||||
const serverInfo = await res.json() as any;
|
||||
assert.strictEqual(serverInfo.issuer, origin);
|
||||
assert.strictEqual(serverInfo.authorization_endpoint, `${origin}/oauth/authorize`);
|
||||
assert.strictEqual(serverInfo.token_endpoint, `${origin}/oauth/token`);
|
||||
});
|
||||
});
|
@@ -6,6 +6,7 @@
|
||||
import * as assert from 'node:assert';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { isAbsolute, basename } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { inspect } from 'node:util';
|
||||
import WebSocket, { ClientOptions } from 'ws';
|
||||
import fetch, { File, RequestInit } from 'node-fetch';
|
||||
@@ -25,6 +26,8 @@ interface UserToken {
|
||||
|
||||
const config = loadConfig();
|
||||
export const port = config.port;
|
||||
export const origin = config.url;
|
||||
export const host = new URL(config.url).host;
|
||||
|
||||
export const cookie = (me: UserToken): string => {
|
||||
return `token=${me.token};`;
|
||||
@@ -126,6 +129,15 @@ export const post = async (user: UserToken, params?: misskey.Endpoints['notes/cr
|
||||
return res.body ? res.body.createdNote : null;
|
||||
};
|
||||
|
||||
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
|
||||
const res = await api('miauth/gen-token', {
|
||||
session: randomUUID(),
|
||||
permission: permissions,
|
||||
}, user);
|
||||
|
||||
return (res.body as misskey.entities.MiauthGenTokenResponse).token;
|
||||
};
|
||||
|
||||
// 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts
|
||||
export const hiddenNote = (note: any): any => {
|
||||
const temp = {
|
||||
|
Reference in New Issue
Block a user