remove tests

Since I don't use them and they never really worked correctly anyway,
there is no real point in keeping them around. Also, removing them
allows to shed quite a bit of "weight" in (dev)dependencies.
This commit is contained in:
Johann150 2024-07-07 19:31:45 +02:00
parent 77a8eb6f54
commit c0364f236c
Signed by: Johann150
GPG key ID: 9EE6577A2A06F8F1
55 changed files with 73 additions and 6426 deletions

4
.gitignore vendored
View file

@ -18,10 +18,6 @@
node_modules node_modules
report.*.json report.*.json
# Cypress
cypress/screenshots
cypress/videos
# config # config
/.config/* /.config/*
!/.config/example.yml !/.config/example.yml

View file

@ -1,13 +0,0 @@
url: 'http://misskey.local'
port: 80
db:
host: postgres
port: 5432
db: test-misskey
user: postgres
pass: ''
redis:
host: redis
port: 6379

View file

@ -17,32 +17,4 @@ pipeline:
commands: commands:
- yarn install - yarn install
- git diff --exit-code yarn.lock - git diff --exit-code yarn.lock
- cp .woodpecker/misskey/test.yml .config
- yarn build - yarn build
mocha:
when:
branch: main
event: push
image: node:18.6.0
commands:
- yarn mocha
e2e:
when:
branch: main
event: push
image: cypress/included:10.3.0
commands:
- npm run start:test &
- sleep 30 # wait for server to start
- cypress run --browser chrome
# TODO: upload screenshots and video artifacts?
# would need some kind of storage though
services:
postgres:
image: postgres:13
environment:
- POSTGRES_DB=test-misskey
- POSTGRES_HOST_AUTH_METHOD=trust
redis:
image: redis:6

View file

@ -157,39 +157,6 @@ During development, it is useful to use the `npm run dev` command.
This command monitors the server-side and client-side source files and automatically builds them if they are modified. This command monitors the server-side and client-side source files and automatically builds them if they are modified.
In addition, it will also automatically start the Misskey server process. In addition, it will also automatically start the Misskey server process.
## Testing
- Test codes are located in [`/test`](/test).
### Run test
Create a config file.
```
cp test/test.yml .config/
```
Prepare DB/Redis for testing.
```
docker-compose -f test/docker-compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
Run all test.
```
npm run test
```
#### Run specify test
```
npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT="./test/tsconfig.json" npx mocha test/foo.ts --require ts-node/register
```
### e2e tests
TODO
## Continuous integration (CI)
Foundkey uses Woodpecker for executing automated tests and lints.
CI runs can be found at [ci.akkoma.dev](https://ci.akkoma.dev/FoundKeyGang/FoundKey)
Configuration files are located in `/.woodpecker/`.
## Vue ## Vue
Misskey uses Vue(v3) as its front-end framework. Misskey uses Vue(v3) as its front-end framework.
- Use TypeScript functionality. - Use TypeScript functionality.

View file

@ -1,12 +0,0 @@
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:61812',
},
})

View file

@ -1,149 +0,0 @@
describe('Before setup instance', () => {
beforeEach(() => {
cy.resetState();
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.visit('/');
});
it('setup instance', () => {
cy.visit('/');
cy.intercept('POST', '/api/admin/accounts/create').as('signup');
cy.get('[data-cy-admin-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click();
// なぜか動かない
//cy.wait('@signup').should('have.property', 'response.statusCode');
cy.wait('@signup');
});
});
describe('After setup instance', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.visit('/');
});
it('signup', () => {
cy.visit('/');
cy.intercept('POST', '/api/signup').as('signup');
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').click();
cy.wait('@signup');
});
});
describe('After user signup', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.visit('/');
});
it('signin', () => {
cy.visit('/');
cy.intercept('POST', '/api/signin').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
// Enterキーでサインインできるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin');
});
it('suspend', function() {
cy.request('POST', '/api/admin/suspend-user', {
i: this.admin.token,
userId: this.alice.id,
});
cy.visit('/');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
});
});
describe('After user singed in', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('successfully loads', () => {
cy.get('[data-cy-open-post-form]').should('be.visible');
});
it('note', () => {
cy.get('[data-cy-open-post-form]').click();
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click();
cy.contains('Hello, Misskey!');
});
});
// TODO: 投稿フォームの公開範囲指定のテスト
// TODO: 投稿フォームのファイル添付のテスト
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト

View file

@ -1,65 +0,0 @@
describe('After user signed in', () => {
beforeEach(() => {
cy.resetState();
cy.viewport('macbook-16');
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('widget edit toggle is visible', () => {
cy.get('.mk-widget-edit').should('be.visible');
});
it('widget select should be visible in edit mode', () => {
cy.get('.mk-widget-edit').click();
cy.get('.mk-widget-select').should('be.visible');
});
it('first widget should be removed', () => {
cy.get('.mk-widget-edit').click();
cy.get('.customize-container:first-child .remove._button').click();
cy.get('.customize-container').should('have.length', 2);
});
function buildWidgetTest(widgetName) {
it(`${widgetName} widget should get added`, () => {
cy.get('.mk-widget-edit').click();
cy.get('.mk-widget-select select').select(widgetName, { force: true });
cy.get('.bg._modalBg.transparent').click({ multiple: true, force: true });
cy.get('.mk-widget-add').click({ force: true });
cy.get(`.mkw-${widgetName}`).should('exist');
});
}
buildWidgetTest('memo');
buildWidgetTest('notifications');
buildWidgetTest('timeline');
buildWidgetTest('calendar');
buildWidgetTest('rss');
buildWidgetTest('trends');
buildWidgetTest('clock');
buildWidgetTest('activity');
buildWidgetTest('photos');
buildWidgetTest('digitalClock');
buildWidgetTest('federation');
buildWidgetTest('postForm');
buildWidgetTest('slideshow');
buildWidgetTest('serverMetric');
buildWidgetTest('onlineUsers');
buildWidgetTest('jobQueue');
buildWidgetTest('button');
buildWidgetTest('aiscript');
buildWidgetTest('aichan');
});

View file

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -1,22 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View file

@ -1,55 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.add('resetState', () => {
cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204);
cy.reload(true);
});
Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup';
cy.request('POST', route, {
username: username,
password: password,
}).its('body').as(username);
});
Cypress.Commands.add('login', (username, password) => {
cy.visit('/');
cy.intercept('POST', '/api/signin').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type(username);
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
cy.wait('@signin').as('signedIn');
});

View file

@ -1,32 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.on('uncaught:exception', (err, runnable) => {
if ([
// Chrome
'ResizeObserver loop limit exceeded',
// Firefox
'ResizeObserver loop completed with undelivered notifications',
].some(msg => err.message.includes(msg))) {
return false;
}
});

View file

@ -13,7 +13,6 @@
"build": "yarn workspaces foreach --topological run build && yarn run gulp", "build": "yarn workspaces foreach --topological run build && yarn run gulp",
"build-parallel": "yarn workspaces foreach --parallel --topological run build && yarn run gulp", "build-parallel": "yarn workspaces foreach --parallel --topological run build && yarn run gulp",
"start": "yarn workspace backend run start", "start": "yarn workspace backend run start",
"start:test": "yarn workspace backend run start:test",
"init": "yarn migrate", "init": "yarn migrate",
"migrate": "yarn workspace backend run migrate", "migrate": "yarn workspace backend run migrate",
"migrateandstart": "yarn migrate && yarn start", "migrateandstart": "yarn migrate && yarn start",
@ -21,11 +20,6 @@
"watch": "yarn dev", "watch": "yarn dev",
"dev": "node ./scripts/dev.mjs", "dev": "node ./scripts/dev.mjs",
"lint": "yarn workspaces foreach run lint", "lint": "yarn workspaces foreach run lint",
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "cypress run",
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
"mocha": "yarn workspace backend run mocha",
"test": "yarn mocha",
"format": "gulp format", "format": "gulp format",
"clean": "node ./scripts/clean.mjs", "clean": "node ./scripts/clean.mjs",
"clean-all": "node ./scripts/clean-all.mjs", "clean-all": "node ./scripts/clean-all.mjs",
@ -49,8 +43,6 @@
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "^5.46.1", "@typescript-eslint/parser": "^5.46.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.3.0",
"start-server-and-test": "1.14.0",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"packageManager": "yarn@3.4.1" "packageManager": "yarn@3.4.1"

View file

@ -8,11 +8,8 @@
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts", "lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
"mocha": "NODE_ENV=test mocha",
"migrate": "yarn exec typeorm migration:run -d ormconfig.js", "migrate": "yarn exec typeorm migration:run -d ormconfig.js",
"start": "node --experimental-json-modules ./built/index.js", "start": "node --experimental-json-modules ./built/index.js"
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
"test": "npm run mocha"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.3.1", "@bull-board/api": "^4.3.1",
@ -71,7 +68,6 @@
"koa-views": "7.0.2", "koa-views": "7.0.2",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"mocha": "10.2.0",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.2.6", "node-fetch": "3.2.6",
@ -139,7 +135,6 @@
"@types/koa__cors": "3.1.1", "@types/koa__cors": "3.1.1",
"@types/koa__multer": "2.0.4", "@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11", "@types/koa__router": "8.0.11",
"@types/mocha": "9.1.1",
"@types/node": "18.7.16", "@types/node": "18.7.16",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.5", "@types/nodemailer": "6.4.5",

View file

@ -70,7 +70,6 @@ import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js'; import { Webhook } from '@/models/entities/webhook.js';
import { getRedisOptions } from '@/config/redis.js'; import { getRedisOptions } from '@/config/redis.js';
import { dbLogger } from './logger.js'; import { dbLogger } from './logger.js';
import { redisClient } from './redis.js';
const sqlLogger = dbLogger.createSubLogger('sql'); const sqlLogger = dbLogger.createSubLogger('sql');
@ -209,31 +208,3 @@ export async function initDb(force = false) {
await db.initialize(); await db.initialize();
} }
} }
export async function resetDb() {
const reset = async () => {
await redisClient.flushdb();
const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
AND C.relkind = 'r'
AND nspname !~ '^pg_toast';`);
for (const table of tables) {
await db.query(`DELETE FROM "${table.table}" CASCADE`);
}
};
for (let i = 1; i <= 3; i++) {
try {
await reset();
} catch (e) {
if (i === 3) {
throw e;
} else {
await new Promise(resolve => setTimeout(resolve, SECOND));
continue;
}
}
break;
}
}

View file

@ -252,7 +252,6 @@ import * as ep___pages_unlike from './endpoints/pages/unlike.js';
import * as ep___pages_update from './endpoints/pages/update.js'; import * as ep___pages_update from './endpoints/pages/update.js';
import * as ep___ping from './endpoints/ping.js'; import * as ep___ping from './endpoints/ping.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js';
import * as ep___resetPassword from './endpoints/reset-password.js'; import * as ep___resetPassword from './endpoints/reset-password.js';
import * as ep___serverInfo from './endpoints/server-info.js'; import * as ep___serverInfo from './endpoints/server-info.js';
import * as ep___stats from './endpoints/stats.js'; import * as ep___stats from './endpoints/stats.js';
@ -546,7 +545,6 @@ const eps = [
['pages/update', ep___pages_update], ['pages/update', ep___pages_update],
['ping', ep___ping], ['ping', ep___ping],
['request-reset-password', ep___requestResetPassword], ['request-reset-password', ep___requestResetPassword],
['reset-db', ep___resetDb],
['reset-password', ep___resetPassword], ['reset-password', ep___resetPassword],
['server-info', ep___serverInfo], ['server-info', ep___serverInfo],
['stats', ep___stats], ['stats', ep___stats],

View file

@ -1,26 +0,0 @@
import { resetDb } from '@/db/postgre.js';
import { ApiError } from '@/server/api/error.js';
import define from '@/server/api/define.js';
export const meta = {
tags: ['non-productive'],
requireCredential: false,
description: 'Only available when running with <code>NODE_ENV=testing</code>. Reset the database and flush Redis.',
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async () => {
if (process.env.NODE_ENV !== 'test') throw new ApiError('ACCESS_DENIED');
await resetDb();
await new Promise(resolve => setTimeout(resolve, 1000));
});

View file

@ -1,11 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['../.eslintrc.cjs'],
env: {
node: true,
mocha: true,
},
};

View file

@ -1,113 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { initDb } from '../built/db/postgre.js';
import { initTestDb } from './utils.mjs';
function rndstr(length) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const chars_len = 62;
let str = '';
for (let i = 0; i < length; i++) {
let rand = Math.floor(Math.random() * chars_len);
if (rand === chars_len) {
rand = chars_len - 1;
}
str += chars.charAt(rand);
}
return str;
}
describe('ActivityPub', () => {
before(async () => {
//await initTestDb();
await initDb();
});
describe('Parse minimum object', () => {
const host = 'https://host1.test';
const preferredUsername = `${rndstr(8)}`;
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
const actor = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: actorId,
type: 'Person',
preferredUsername,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
const post = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `${host}/users/${rndstr(8)}`,
type: 'Note',
attributedTo: actor.id,
to: 'https://www.w3.org/ns/activitystreams#Public',
content: 'あ',
};
it('Minimum Actor', async () => {
const { MockResolver } = await import('./misc/mock-resolver.mjs');
const { createPerson } = await import('../built/remote/activitypub/models/person.js');
const resolver = new MockResolver();
resolver._register(actor.id, actor);
const user = await createPerson(actor.id, resolver);
assert.deepStrictEqual(user.uri, actor.id);
assert.deepStrictEqual(user.username, actor.preferredUsername);
assert.deepStrictEqual(user.inbox, actor.inbox);
});
it('Minimum Note', async () => {
const { MockResolver } = await import('./misc/mock-resolver.mjs');
const { createNote } = await import('../built/remote/activitypub/models/note.js');
const resolver = new MockResolver();
resolver._register(actor.id, actor);
resolver._register(post.id, post);
const note = await createNote(post.id, resolver, true);
assert.deepStrictEqual(note?.uri, post.id);
assert.deepStrictEqual(note.visibility, 'public');
assert.deepStrictEqual(note.text, post.content);
});
});
describe('Truncate long name', () => {
const host = 'https://host1.test';
const preferredUsername = `${rndstr(8)}`;
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
const name = rndstr(129);
const actor = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: actorId,
type: 'Person',
preferredUsername,
name,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
it('Actor', async () => {
const { MockResolver } = await import('./misc/mock-resolver.mjs');
const { createPerson } = await import('../built/remote/activitypub/models/person.js');
const resolver = new MockResolver();
resolver._register(actor.id, actor);
const user = await createPerson(actor.id, resolver);
assert.deepStrictEqual(user.name, actor.name.substr(0, 128));
});
});
});

View file

@ -1,55 +0,0 @@
import * as assert from 'assert';
import httpSignature from '@peertube/http-signature';
import { genRsaKeyPair } from '../built/misc/gen-key-pair.js';
import { createSignedPost, createSignedGet } from '../built/remote/activitypub/ap-request.js';
export const buildParsedSignature = (signingString, signature, algorithm) => {
return {
scheme: 'Signature',
params: {
keyId: 'KeyID', // dummy, not used for verify
algorithm: algorithm,
headers: [ '(request-target)', 'date', 'host', 'digest' ], // dummy, not used for verify
signature: signature,
},
signingString: signingString,
algorithm: algorithm.toUpperCase(),
keyId: 'KeyID', // dummy, not used for verify
};
};
describe('ap-request', () => {
it('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/inbox';
const activity = { a: 1 };
const body = JSON.stringify(activity);
const headers = {
'User-Agent': 'UA',
};
const req = createSignedPost({ key, url, body, additionalHeaders: headers });
const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
it('createSignedGet with verify', async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/outbox';
const headers = {
'User-Agent': 'UA',
};
const req = createSignedGet({ key, url, additionalHeaders: headers });
const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
});

View file

@ -1,473 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, startServer, shutdownServer } from './utils.mjs';
describe('API visibility', function() {
this.timeout(20*60*1000);
let p;
before(async () => {
p = await startServer();
});
after(async () => {
await shutdownServer(p);
});
describe('Note visibility', async () => {
//#region vars
/** protagonist */
let alice;
/** follower */
let follower;
/** non-follower */
let other;
/** non-follower who has been replied to or mentioned */
let target;
/** actor for which a specified visibility was set */
let target2;
/** public-post */
let pub;
/** home-post */
let home;
/** followers-post */
let fol;
/** specified-post */
let spe;
/** public-reply to target's post */
let pubR;
/** home-reply to target's post */
let homeR;
/** followers-reply to target's post */
let folR;
/** specified-reply to target's post */
let speR;
/** public-mention to target */
let pubM;
/** home-mention to target */
let homeM;
/** followers-mention to target */
let folM;
/** specified-mention to target */
let speM;
/** reply target post */
let tgt;
//#endregion
const show = async (noteId, by) => {
return await request('/notes/show', {
noteId,
}, by);
};
before(async () => {
//#region prepare
// signup
alice = await signup({ username: 'alice' });
follower = await signup({ username: 'follower' });
other = await signup({ username: 'other' });
target = await signup({ username: 'target' });
target2 = await signup({ username: 'target2' });
// follow alice <= follower
await request('/following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
home = await post(alice, { text: 'x', visibility: 'home' });
fol = await post(alice, { text: 'x', visibility: 'followers' });
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
// replies
tgt = await post(target, { text: 'y', visibility: 'public' });
pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
// mentions
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
//#endregion
});
//#region show post
// public
it('[show] public post can be seen by author', async(async () => {
const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public post can be seen by follower', async(async () => {
const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public post can be seen by non-follower', async(async () => {
const res = await show(pub.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public post can be seen unauthenticated', async(async () => {
const res = await show(pub.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home post can be seen by author', async(async () => {
const res = await show(home.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home post can be seen by follower', async(async () => {
const res = await show(home.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home post can be seen by non-follower', async(async () => {
const res = await show(home.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home post can be seen unauthenticated', async(async () => {
const res = await show(home.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers post can be seen by author', async(async () => {
const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers post can be seen by follower', async(async () => {
const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers post is hidden from non-follower', async(async () => {
const res = await show(fol.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] followers post is hidden when unathenticated', async(async () => {
const res = await show(fol.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it('[show] specified post can be seen by author', async(async () => {
const res = await show(spe.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified post can be seen by designated user', async(async () => {
const res = await show(spe.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified post is hidden from non-specified follower', async(async () => {
const res = await show(spe.id, follower);
assert.strictEqual(res.status, 404);
}));
it('[show] specified post is hidden from non-follower', async(async () => {
const res = await show(spe.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] specified post is hidden when unauthenticated', async(async () => {
const res = await show(spe.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region show reply
// public
it('[show] public reply can be seen by author', async(async () => {
const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public reply can be seen by replied to author', async(async () => {
const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public reply can be seen by follower', async(async () => {
const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public reply can be seen by non-follower', async(async () => {
const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public reply can be seen unauthenticated', async(async () => {
const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home reply can be seen by author', async(async () => {
const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home reply can be seen by replied to author', async(async () => {
const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home reply can be seen by follower', async(async () => {
const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home reply can be seen by non-follower', async(async () => {
const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home reply can be seen unauthenticated', async(async () => {
const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers reply can be seen by author', async(async () => {
const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers reply can be seen by replied to author', async(async () => {
const res = await show(folR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers reply can be seen by follower', async(async () => {
const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers reply is hidden from non-follower', async(async () => {
const res = await show(folR.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] followers reply is hidden when unauthenticated', async(async () => {
const res = await show(folR.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it('[show] specified reply can be seen by author', async(async () => {
const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified reply can be seen by replied to user', async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified reply is hidden from follower', async(async () => {
const res = await show(speR.id, follower);
assert.strictEqual(res.status, 404);
}));
it('[show] specified reply is hidden from non-follower', async(async () => {
const res = await show(speR.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] specified reply is hidden when unauthenticated', async(async () => {
const res = await show(speR.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region show mention
// public
it('[show] public-mention can be seen by author', async(async () => {
const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public mention can be seen by mentioned', async(async () => {
const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public mention can be seen by follower', async(async () => {
const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public mention can be seen by non-follower', async(async () => {
const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public mention can be seen unauthenticated', async(async () => {
const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, '@target x');
}));
// home
it('[show] home mention can be seen by author', async(async () => {
const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home mention can be seen by mentioned', async(async () => {
const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home mention can be seen by follower', async(async () => {
const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home mention can be seen by non-follower', async(async () => {
const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home mention can be seen unauthenticated', async(async () => {
const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, '@target x');
}));
// followers
it('[show] followers mention can be seen by author', async(async () => {
const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers mention can be seen by non-follower mentioned', async(async () => {
const res = await show(folM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers mention can be seen by follower', async(async () => {
const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers mention is hidden from non-follower', async(async () => {
const res = await show(folM.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] followers mention is hidden when unauthenticated', async(async () => {
const res = await show(folM.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it('[show] specified mention can be seen by author', async(async () => {
const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, '@target2 x');
}));
it('[show] specified mention can be seen by specified actor', async(async () => {
const res = await show(speM.id, target);
assert.strictEqual(res.body.text, '@target2 x');
}));
it('[show] specified mention is hidden from mentioned but not specified actor', async(async () => {
const res = await show(speM.id, target2);
assert.strictEqual(res.status, 404);
}));
it('[show] specified mention is hidden from follower', async(async () => {
const res = await show(speM.id, follower);
assert.strictEqual(res.status, 404);
}));
it('[show] specified mention is hidden from non-follower', async(async () => {
const res = await show(speM.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] specified mention is hidden when unauthenticated', async(async () => {
const res = await show(speM.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region Home Timeline
it('[TL] public post on author home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == pub.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[TL] public post absent from non-follower home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == pub.id);
assert.strictEqual(notes.length, 0);
}));
it('[TL] followers post on follower home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == fol.id);
assert.strictEqual(notes[0].text, 'x');
}));
//#endregion
//#region replies timeline
it('[TL] followers reply on follower reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[TL] followers reply absent from not replied to non-follower reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == folR.id);
assert.strictEqual(notes.length, 0);
}));
it('[TL] followers reply on replied to actor reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
//#endregion
//#region MTL
it('[TL] followers reply on replied to non-follower mention TL', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[TL] followers mention on mentioned non-follower mention TL', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n) => n.id == folM.id);
assert.strictEqual(notes[0].text, '@target x');
}));
//#endregion
});
});

View file

@ -1,83 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.mjs';
describe('API', function() {
this.timeout(20*60*1000);
let p;
let alice, bob, carol;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
describe('General validation', () => {
it('wrong type', async(async () => {
const res = await request('/test', {
required: true,
string: 42,
});
assert.strictEqual(res.status, 400);
}));
it('missing require param', async(async () => {
const res = await request('/test', {
string: 'a',
});
assert.strictEqual(res.status, 400);
}));
it('invalid misskey:id (empty string)', async(async () => {
const res = await request('/test', {
required: true,
id: '',
});
assert.strictEqual(res.status, 400);
}));
it('valid misskey:id', async(async () => {
const res = await request('/test', {
required: true,
id: '8wvhjghbxu',
});
assert.strictEqual(res.status, 200);
}));
it('default value', async(async () => {
const res = await request('/test', {
required: true,
string: 'a',
});
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.default, 'hello');
}));
it('can set null even if it has default value', async(async () => {
const res = await request('/test', {
required: true,
nullableDefault: null,
});
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.nullableDefault, null);
}));
it('cannot set undefined if it has default value', async(async () => {
const res = await request('/test', {
required: true,
nullableDefault: undefined,
});
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.nullableDefault, 'hello');
}));
});
});

View file

@ -1,85 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, startServer, shutdownServer } from './utils.mjs';
describe('Block', function() {
this.timeout(20*60*1000);
let p;
// alice blocks bob
let alice, bob, carol;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('can block someone', async(async () => {
const res = await request('/blocking/create', {
userId: bob.id,
}, alice);
assert.strictEqual(res.status, 200);
}));
it('cannot follow if blocked', async(async () => {
const res = await request('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('cannot react to blocking users note', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('cannot reply to blocking users note', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('canot renote blocking users note', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('cannot include blocked users in user lists');
it('removes users from user lists');
it('local timeline does not contain blocked users', async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
const res = await request('/notes/local-timeline', {}, bob);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), true);
}));
});

View file

@ -1,527 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as lolex from '@sinonjs/fake-timers';
import TestChart from '../built/services/chart/charts/test.js';
import TestGroupedChart from '../built/services/chart/charts/test-grouped.js';
import TestUniqueChart from '../built/services/chart/charts/test-unique.js';
import TestIntersectionChart from '../built/services/chart/charts/test-intersection.js';
import { initDb } from '../built/db/postgre.js';
describe('Chart', () => {
let testChart, testGroupedChart, testUniqueChart, testIntersectionChart, clock;
beforeEach(async () => {
await initDb(true);
testChart = new TestChart();
testGroupedChart = new TestGroupedChart();
testUniqueChart = new TestUniqueChart();
testIntersectionChart = new TestIntersectionChart();
clock = lolex.install({
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
shouldClearNativeTimers: true,
});
});
afterEach(() => {
clock.uninstall();
});
it('Can updates', async () => {
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
});
it('Can updates (dec)', async () => {
await testChart.decrement();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [1, 0, 0],
inc: [0, 0, 0],
total: [-1, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [1, 0, 0],
inc: [0, 0, 0],
total: [-1, 0, 0],
},
});
});
it('Empty chart', async () => {
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0],
},
});
});
it('Can updates at multiple times at same time', async () => {
await testChart.increment();
await testChart.increment();
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [3, 0, 0],
total: [3, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [3, 0, 0],
total: [3, 0, 0],
},
});
});
it('複数回saveされてもデータの更新は一度だけ', async () => {
await testChart.increment();
await testChart.save();
await testChart.save();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
});
it('Can updates at different times', async () => {
await testChart.increment();
await testChart.save();
clock.tick('01:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 1, 0],
total: [2, 1, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0],
},
});
});
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
/*
it('Can updates at different times without save', async () => {
await testChart.increment();
clock.tick('01:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 1, 0],
total: [2, 1, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
});
*/
it('Can padding', async () => {
await testChart.increment();
await testChart.save();
clock.tick('02:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 1],
total: [2, 1, 1],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0],
},
});
});
// 要求された範囲にログがひとつもない場合でもパディングできる
it('Can padding from past range', async () => {
await testChart.increment();
await testChart.save();
clock.tick('05:00:00');
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [1, 1, 1],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
});
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
// Issue #3190
it('Can padding from past range 2', async () => {
await testChart.increment();
await testChart.save();
clock.tick('05:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [2, 1, 1],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0],
},
});
});
it('Can specify offset', async () => {
await testChart.increment();
await testChart.save();
clock.tick('01:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0],
},
});
});
it('Can specify offset (floor time)', async () => {
clock.tick('00:30:00');
await testChart.increment();
await testChart.save();
clock.tick('01:30:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0],
},
});
});
describe('Grouped', () => {
it('Can updates', async () => {
await testGroupedChart.increment('alice');
await testGroupedChart.save();
const aliceChartHours = await testGroupedChart.getChart('hour', 3, null, 'alice');
const aliceChartDays = await testGroupedChart.getChart('day', 3, null, 'alice');
const bobChartHours = await testGroupedChart.getChart('hour', 3, null, 'bob');
const bobChartDays = await testGroupedChart.getChart('day', 3, null, 'bob');
assert.deepStrictEqual(aliceChartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(aliceChartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(bobChartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0],
},
});
assert.deepStrictEqual(bobChartDays, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0],
},
});
});
});
describe('Unique increment', () => {
it('Can updates', async () => {
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob');
await testUniqueChart.save();
const chartHours = await testUniqueChart.getChart('hour', 3, null);
const chartDays = await testUniqueChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: [2, 0, 0],
});
assert.deepStrictEqual(chartDays, {
foo: [2, 0, 0],
});
});
describe('Intersection', () => {
it('条件が満たされていない場合はカウントされない', async () => {
await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol');
await testIntersectionChart.save();
const chartHours = await testIntersectionChart.getChart('hour', 3, null);
const chartDays = await testIntersectionChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
a: [2, 0, 0],
b: [1, 0, 0],
aAndB: [0, 0, 0],
});
assert.deepStrictEqual(chartDays, {
a: [2, 0, 0],
b: [1, 0, 0],
aAndB: [0, 0, 0],
});
});
it('条件が満たされている場合にカウントされる', async () => {
await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol');
await testIntersectionChart.addB('alice');
await testIntersectionChart.save();
const chartHours = await testIntersectionChart.getChart('hour', 3, null);
const chartDays = await testIntersectionChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
a: [2, 0, 0],
b: [2, 0, 0],
aAndB: [1, 0, 0],
});
assert.deepStrictEqual(chartDays, {
a: [2, 0, 0],
b: [2, 0, 0],
aAndB: [1, 0, 0],
});
});
});
});
describe('Resync', () => {
it('Can resync', async () => {
testChart.total = 1;
await testChart.resync();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [1, 0, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [1, 0, 0],
},
});
});
it('Can resync (2)', async () => {
await testChart.increment();
await testChart.save();
clock.tick('01:00:00');
testChart.total = 100;
await testChart.resync();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 1, 0],
total: [100, 1, 0],
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [100, 0, 0],
},
});
});
});
});

View file

@ -1,864 +0,0 @@
/*
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.mjs';
describe('API: Endpoints', () => {
let p;
let alice, bob, carol;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
describe('signup', () => {
it('不正なユーザー名でアカウントが作成できない', async(async () => {
const res = await request('/signup', {
username: 'test.',
password: 'test'
});
assert.strictEqual(res.status, 400);
}));
it('空のパスワードでアカウントが作成できない', async(async () => {
const res = await request('/signup', {
username: 'test',
password: ''
});
assert.strictEqual(res.status, 400);
}));
it('正しくアカウントが作成できる', async(async () => {
const me = {
username: 'test1',
password: 'test1'
};
const res = await request('/signup', me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.username, me.username);
}));
it('同じユーザー名のアカウントは作成できない', async(async () => {
await signup({
username: 'test2'
});
const res = await request('/signup', {
username: 'test2',
password: 'test2'
});
assert.strictEqual(res.status, 400);
}));
});
describe('signin', () => {
it('間違ったパスワードでサインインできない', async(async () => {
await signup({
username: 'test3',
password: 'foo'
});
const res = await request('/signin', {
username: 'test3',
password: 'bar'
});
assert.strictEqual(res.status, 403);
}));
it('クエリをインジェクションできない', async(async () => {
await signup({
username: 'test4'
});
const res = await request('/signin', {
username: 'test4',
password: {
$gt: ''
}
});
assert.strictEqual(res.status, 400);
}));
it('正しい情報でサインインできる', async(async () => {
await signup({
username: 'test5',
password: 'foo'
});
const res = await request('/signin', {
username: 'test5',
password: 'foo'
});
assert.strictEqual(res.status, 200);
}));
});
describe('i/update', () => {
it('アカウント設定を更新できる', async(async () => {
const myName = '大室櫻子';
const myLocation = '七森中';
const myBirthday = '2000-09-07';
const res = await request('/i/update', {
name: myName,
location: myLocation,
birthday: myBirthday
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, myName);
assert.strictEqual(res.body.location, myLocation);
assert.strictEqual(res.body.birthday, myBirthday);
}));
it('名前を空白にできない', async(async () => {
const res = await request('/i/update', {
name: ' '
}, alice);
assert.strictEqual(res.status, 400);
}));
it('誕生日の設定を削除できる', async(async () => {
await request('/i/update', {
birthday: '2000-09-07'
}, alice);
const res = await request('/i/update', {
birthday: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.birthday, null);
}));
it('不正な誕生日の形式で怒られる', async(async () => {
const res = await request('/i/update', {
birthday: '2000/09/07'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('users/show', () => {
it('ユーザーが取得できる', async(async () => {
const res = await request('/users/show', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, alice.id);
}));
it('ユーザーが存在しなかったら怒る', async(async () => {
const res = await request('/users/show', {
userId: '000000000000000000000000'
});
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/users/show', {
userId: 'kyoppie'
});
assert.strictEqual(res.status, 400);
}));
});
describe('notes/show', () => {
it('投稿が取得できる', async(async () => {
const myPost = await post(alice, {
text: 'test'
});
const res = await request('/notes/show', {
noteId: myPost.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, myPost.id);
assert.strictEqual(res.body.text, myPost.text);
}));
it('投稿が存在しなかったら怒る', async(async () => {
const res = await request('/notes/show', {
noteId: '000000000000000000000000'
});
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/notes/show', {
noteId: 'kyoppie'
});
assert.strictEqual(res.status, 400);
}));
});
describe('notes/reactions/create', () => {
it('リアクションできる', async(async () => {
const bobPost = await post(bob);
const alice = await signup({ username: 'alice' });
const res = await request('/notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
const resNote = await request('/notes/show', {
noteId: bobPost.id,
}, alice);
assert.strictEqual(resNote.status, 200);
assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]);
}));
it('自分の投稿にもリアクションできる', async(async () => {
const myPost = await post(alice);
const res = await request('/notes/reactions/create', {
noteId: myPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
}));
it('二重にリアクションできない', async(async () => {
const bobPost = await post(bob);
await react(alice, bobPost, 'like');
const res = await request('/notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しない投稿にはリアクションできない', async(async () => {
const res = await request('/notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const res = await request('/notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/notes/reactions/create', {
noteId: 'kyoppie',
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('following/create', () => {
it('フォローできる', async(async () => {
const res = await request('/following/create', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 200);
}));
it('既にフォローしている場合は怒る', async(async () => {
const res = await request('/following/create', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォローできない', async(async () => {
const res = await request('/following/create', {
userId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('自分自身はフォローできない', async(async () => {
const res = await request('/following/create', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const res = await request('/following/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/following/create', {
userId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('following/delete', () => {
it('フォロー解除できる', async(async () => {
await request('/following/create', {
userId: alice.id
}, bob);
const res = await request('/following/delete', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 200);
}));
it('フォローしていない場合は怒る', async(async () => {
const res = await request('/following/delete', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォロー解除できない', async(async () => {
const res = await request('/following/delete', {
userId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('自分自身はフォロー解除できない', async(async () => {
const res = await request('/following/delete', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const res = await request('/following/delete', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/following/delete', {
userId: 'kyoppie'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('drive', () => {
it('ドライブ情報を取得できる', async(async () => {
await uploadFile({
userId: alice.id,
size: 256
});
await uploadFile({
userId: alice.id,
size: 512
});
await uploadFile({
userId: alice.id,
size: 1024
});
const res = await request('/drive', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).have.property('usage').eql(1792);
}));
});
describe('drive/files/create', () => {
it('ファイルを作成できる', async(async () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.png');
}));
it('ファイルに名前を付けられる', async(async () => {
const res = await assert.request(server)
.post('/drive/files/create')
.field('i', alice.token)
.field('name', 'Belmond.png')
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
expect(res).have.status(200);
expect(res.body).be.a('object');
expect(res.body).have.property('name').eql('Belmond.png');
}));
it('ファイル無しで怒られる', async(async () => {
const res = await request('/drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('SVGファイルを作成できる', async(async () => {
const res = await uploadFile(alice, __dirname + '/resources/image.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml');
}));
});
describe('drive/files/update', () => {
it('名前を更新できる', async(async () => {
const file = await uploadFile(alice);
const newName = 'いちごパスタ.png';
const res = await request('/drive/files/update', {
fileId: file.id,
name: newName
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, newName);
}));
it('他人のファイルは更新できない', async(async () => {
const file = await uploadFile(bob);
const res = await request('/drive/files/update', {
fileId: file.id,
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, folder.id);
}));
it('親フォルダを無しにできる', async(async () => {
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, null);
}));
it('他人のフォルダには入れられない', async(async () => {
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, bob)).body;
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダで怒られる', async(async () => {
const file = await uploadFile(alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
const file = await uploadFile(alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('ファイルが存在しなかったら怒る', async(async () => {
const res = await request('/drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('drive/folders/create', () => {
it('フォルダを作成できる', async(async () => {
const res = await request('/drive/folders/create', {
name: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'test');
}));
});
describe('drive/folders/update', () => {
it('名前を更新できる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
name: 'new name'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'new name');
}));
it('他人のフォルダを更新できない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, bob)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
name: 'new name'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, parentFolder.id);
}));
it('親フォルダを無しに更新できる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, null);
}));
it('他人のフォルダを親フォルダに設定できない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, bob)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
await request('/drive/folders/update', {
folderId: parentFolder.id,
parentId: folder.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
const folderA = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const folderB = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const folderC = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
await request('/drive/folders/update', {
folderId: folderB.id,
parentId: folderA.id
}, alice);
await request('/drive/folders/update', {
folderId: folderC.id,
parentId: folderB.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folderA.id,
parentId: folderC.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(自身)', async(async () => {
const folderA = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folderA.id,
parentId: folderA.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しない親フォルダを設定できない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正な親フォルダIDで怒られる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダを更新できない', async(async () => {
const res = await request('/drive/folders/update', {
folderId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
const res = await request('/drive/folders/update', {
folderId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('messaging/messages/create', () => {
it('メッセージを送信できる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: bob.id,
text: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.text, 'test');
}));
it('自分自身にはメッセージを送信できない', async(async () => {
const res = await request('/messaging/messages/create', {
userId: alice.id,
text: 'Yo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーにはメッセージを送信できない', async(async () => {
const res = await request('/messaging/messages/create', {
userId: '000000000000000000000000',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なユーザーIDで怒られる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: 'foo',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('テキストが無くて怒られる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: bob.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('文字数オーバーで怒られる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: bob.id,
text: '!'.repeat(1001)
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('notes/replies', () => {
it('自分に閲覧権限のない投稿は含まれない', async(async () => {
const alicePost = await post(alice, {
text: 'foo'
});
await post(bob, {
replyId: alicePost.id,
text: 'bar',
visibility: 'specified',
visibleUserIds: [alice.id]
});
const res = await request('/notes/replies', {
noteId: alicePost.id
}, carol);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 0);
}));
});
describe('notes/timeline', () => {
it('フォロワー限定投稿が含まれる', async(async () => {
await request('/following/create', {
userId: alice.id
}, bob);
const alicePost = await post(alice, {
text: 'foo',
visibility: 'followers'
});
const res = await request('/notes/timeline', {}, bob);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 1);
assert.strictEqual(res.body[0].id, alicePost.id);
}));
});
});
*/

View file

@ -1,42 +0,0 @@
import * as assert from 'assert';
import { parse } from 'mfm-js';
import { extractMentions } from '../built/misc/extract-mentions.js';
describe('Extract mentions', () => {
it('simple', () => {
const ast = parse('@foo @bar @baz');
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
username: 'foo',
acct: '@foo',
host: null,
}, {
username: 'bar',
acct: '@bar',
host: null,
}, {
username: 'baz',
acct: '@baz',
host: null,
}]);
});
it('nested', () => {
const ast = parse('@foo **@bar** @baz');
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
username: 'foo',
acct: '@foo',
host: null,
}, {
username: 'bar',
acct: '@bar',
host: null,
}, {
username: 'baz',
acct: '@baz',
host: null,
}]);
});
});

View file

@ -1,206 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import * as openapi from '@redocly/openapi-core';
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.mjs';
// Request Accept
const ONLY_AP = 'application/activity+json';
const PREFER_AP = 'application/activity+json, */*';
const PREFER_HTML = 'text/html, */*';
const UNSPECIFIED = '*/*';
// Response Contet-Type
const AP = 'application/activity+json; charset=utf-8';
const JSON = 'application/json; charset=utf-8';
const HTML = 'text/html; charset=utf-8';
describe('Fetch resource', function() {
this.timeout(20*60*1000);
let p;
let alice, alicesPost;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
alicesPost = await post(alice, {
text: 'test',
});
});
after(async () => {
await shutdownServer(p);
});
describe('Common', () => {
it('meta', async(async () => {
const res = await request('/meta', {
});
assert.strictEqual(res.status, 200);
}));
it('GET root', async(async () => {
const res = await simpleGet('/');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('GET docs', async(async () => {
const res = await simpleGet('/docs/ja-JP/about');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('GET api-doc', async(async () => {
const res = await simpleGet('/api-doc');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('GET api.json', async(async () => {
const res = await simpleGet('/api.json');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, JSON);
}));
it('Validate api.json', async(async () => {
const config = await openapi.loadConfig();
const result = await openapi.bundle({
config,
ref: `http://localhost:${port}/api.json`,
});
for (const problem of result.problems) {
console.log(`${problem.message} - ${problem.location[0]?.pointer}`);
}
assert.strictEqual(result.problems.length, 0);
}));
it('GET favicon.ico', async(async () => {
const res = await simpleGet('/favicon.ico');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/x-icon');
}));
it('GET apple-touch-icon.png', async(async () => {
const res = await simpleGet('/apple-touch-icon.png');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/png');
}));
it('GET twemoji svg', async(async () => {
const res = await simpleGet('/twemoji/2764.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml');
}));
it('GET twemoji svg with hyphen', async(async () => {
const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml');
}));
});
describe('/@:username', () => {
it('Only AP => AP', async(async () => {
const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer AP => AP', async(async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer HTML => HTML', async(async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('Unspecified => HTML', async(async () => {
const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
});
describe('/users/:id', () => {
it('Only AP => AP', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer AP => AP', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer HTML => Redirect to /@:username', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
}));
it('Undecided => HTML', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
}));
});
describe('/notes/:id', () => {
it('Only AP => AP', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer AP => AP', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer HTML => HTML', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('Unspecified => HTML', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
});
describe('Feeds', () => {
it('RSS', async(async () => {
const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8');
}));
it('ATOM', async(async () => {
const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8');
}));
it('JSON', async(async () => {
const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/json; charset=utf-8');
}));
});
});

View file

@ -1,116 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils.mjs';
describe('FF visibility', function() {
this.timeout(20*60*1000);
let p;
let alice, bob, follower;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
follower = await signup({ username: 'follower' });
await request('/following/create', { userId: alice.id }, follower);
});
after(async () => {
await shutdownServer(p);
});
const visible = (user) => {
return async () => {
const followingRes = await request('/users/following', {
userId: alice.id,
}, user);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, user);
assert.strictEqual(followingRes.status, 200);
assert.ok(Array.isArray(followingRes.body));
assert.strictEqual(followersRes.status, 200);
assert.ok(Array.isArray(followersRes.body));
};
};
const hidden = (user) => {
return async () => {
const followingRes = await request('/users/following', {
userId: alice.id,
}, user);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, user);
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
};
};
describe('public visibility', () => {
before(async () => {
await request('/i/update', {
ffVisibility: 'public',
}, alice);
});
it('shows followers and following to self', visible(alice));
it('shows followers and following to a follower', visible(follower));
it('shows followers and following to a non-follower', visible(bob));
it('shows followers and following when unauthenticated', visible(null));
it('provides followers in ActivityPub representation', async () => {
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(followersRes.status, 200);
});
});
describe('followers visibility', () => {
before(async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
});
it('shows followers and following to self', visible(alice));
it('shows followers and following to a follower', visible(follower));
it('hides followers and following from a non-follower', hidden(bob));
it('hides followers and following when unauthenticated', hidden(null));
it('hides followers from ActivityPub representation', async () => {
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
});
});
describe('private visibility', () => {
before(async () => {
await request('/i/update', {
ffVisibility: 'private',
}, alice);
});
it('shows followers and following to self', visible(alice));
it('hides followers and following from a follower', hidden(follower));
it('hides followers and following from a non-follower', hidden(bob));
it('hides followers and following when unauthenticated', hidden(null));
it('hides followers from ActivityPub representation', async () => {
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
});
});
});

View file

@ -1,173 +0,0 @@
import * as assert from 'assert';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { getFileInfo } from '../built/misc/get-file-info.js';
import { async } from './utils.mjs';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
describe('Get file info', () => {
it('Empty file', async (async () => {
const path = `${_dirname}/resources/emptyfile`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 0,
md5: 'd41d8cd98f00b204e9800998ecf8427e',
type: {
mime: 'application/octet-stream',
ext: null,
},
width: undefined,
height: undefined,
orientation: undefined,
});
}));
it('Generic JPEG', async (async () => {
const path = `${_dirname}/resources/Lenna.jpg`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 25360,
md5: '091b3f259662aa31e2ffef4519951168',
type: {
mime: 'image/jpeg',
ext: 'jpg',
},
width: 512,
height: 512,
orientation: undefined,
});
}));
it('Generic APNG', async (async () => {
const path = `${_dirname}/resources/anime.png`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 1868,
md5: '08189c607bea3b952704676bb3c979e0',
type: {
mime: 'image/apng',
ext: 'apng',
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it('Generic AGIF', async (async () => {
const path = `${_dirname}/resources/anime.gif`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 2248,
md5: '32c47a11555675d9267aee1a86571e7e',
type: {
mime: 'image/gif',
ext: 'gif',
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it('PNG with alpha', async (async () => {
const path = `${_dirname}/resources/with-alpha.png`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 3772,
md5: 'f73535c3e1e27508885b69b10cf6e991',
type: {
mime: 'image/png',
ext: 'png',
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it('Generic SVG', async (async () => {
const path = `${_dirname}/resources/image.svg`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 505,
md5: 'b6f52b4b021e7b92cdd04509c7267965',
type: {
mime: 'image/svg+xml',
ext: 'svg',
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it('SVG with XML definition', async (async () => {
// https://github.com/misskey-dev/misskey/issues/4413
const path = `${_dirname}/resources/with-xml-def.svg`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 544,
md5: '4b7a346cde9ccbeb267e812567e33397',
type: {
mime: 'image/svg+xml',
ext: 'svg',
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it('Dimension limit', async (async () => {
const path = `${_dirname}/resources/25000x25000.png`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 75933,
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
type: {
mime: 'application/octet-stream', // do not treat as image
ext: null,
},
width: 25000,
height: 25000,
orientation: undefined,
});
}));
it('Rotate JPEG', async (async () => {
const path = `${_dirname}/resources/rotate.jpg`;
const info = await getFileInfo(path);
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
type: {
mime: 'image/jpeg',
ext: 'jpg',
},
width: 512,
height: 256,
orientation: 8,
});
}));
});

View file

@ -1,88 +0,0 @@
import * as assert from 'assert';
import { toHtml } from '../built/mfm/to-html.js';
import { fromHtml } from '../built/mfm/from-html.js';
describe('toHtml', () => {
it('br', async () => {
const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(await toHtml(input), output);
});
it('br alt', async () => {
const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(await toHtml(input), output);
});
});
describe('fromHtml', () => {
it('p', () => {
assert.deepStrictEqual(fromHtml('<p>a</p><p>b</p>'), 'a\n\nb');
});
it('block element', () => {
assert.deepStrictEqual(fromHtml('<div>a</div><div>b</div>'), 'a\nb');
});
it('inline element', () => {
assert.deepStrictEqual(fromHtml('<ul><li>a</li><li>b</li></ul>'), 'a\nb');
});
it('block code', () => {
assert.deepStrictEqual(fromHtml('<pre><code>a\nb</code></pre>'), '```\na\nb\n```');
});
it('inline code', () => {
assert.deepStrictEqual(fromHtml('<code>a</code>'), '`a`');
});
it('quote', () => {
assert.deepStrictEqual(fromHtml('<blockquote>a\nb</blockquote>'), '> a\n> b');
});
it('br', () => {
assert.deepStrictEqual(fromHtml('<p>abc<br><br/>d</p>'), 'abc\n\nd');
});
it('link with different text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">c</a> d</p>'), 'a [c](https://example.com/b) d');
});
it('link with different text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">c</a> d</p>'), 'a [c](<https://example.com/ä>) d');
});
it('link with same text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">https://example.com/b</a> d</p>'), 'a https://example.com/b d');
});
it('link with same text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">https://example.com/ä</a> d</p>'), 'a <https://example.com/ä> d');
});
it('link with no url', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="b">c</a> d</p>'), 'a [c](b) d');
});
it('link without href', () => {
assert.deepStrictEqual(fromHtml('<p>a <a>c</a> d</p>'), 'a c d');
});
it('link without text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b"></a> d</p>'), 'a https://example.com/b d');
});
it('link without both', () => {
assert.deepStrictEqual(fromHtml('<p>a <a></a> d</p>'), 'a d');
});
it('mention', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
});
it('hashtag', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
});
});

View file

@ -1,30 +0,0 @@
import { Resolver } from '../../built/remote/activitypub/resolver.js';
export class MockResolver extends Resolver {
_rs = new Map();
async _register(uri, content, type = 'application/activity+json') {
this._rs.set(uri, {
type,
content: typeof content === 'string' ? content : JSON.stringify(content),
});
}
async resolve(value) {
if (typeof value !== 'string') return value;
const r = this._rs.get(value);
if (!r) {
throw {
name: 'StatusError',
statusCode: 404,
message: 'Not registed for mock',
};
}
const object = JSON.parse(r.content);
return object;
}
}

View file

@ -1,123 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.mjs';
describe('Mute', function() {
this.timeout(20*60*1000);
let p;
// alice mutes carol
let alice, bob, carol;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('ミュート作成', async(async () => {
const res = await request('/mute/create', {
userId: carol.id,
}, alice);
assert.strictEqual(res.status, 204);
}));
it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async(async () => {
const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' });
const res = await request('/notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false);
}));
it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' });
const res = await request('/i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
assert.strictEqual(fired, false);
});
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
await request('/notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
assert.strictEqual(fired, false);
});
describe('Timeline', () => {
it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
const res = await request('/notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false);
}));
it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => {
const aliceNote = await post(alice);
const carolNote = await post(carol);
const bobNote = await post(bob, {
renoteId: carolNote.id,
});
const res = await request('/notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false);
}));
});
describe('Notification', () => {
it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async(async () => {
const aliceNote = await post(alice);
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await request('/i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification) => notification.userId === carol.id), false);
}));
});
});

View file

@ -1,371 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { Note } from '../built/models/entities/note.js';
import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.mjs';
describe('Note', function() {
this.timeout(20*60*1000);
let p;
let Notes;
let alice, bob;
before(async () => {
p = await startServer();
const connection = await initTestDb(true);
Notes = connection.getRepository(Note);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
});
after(async () => {
await shutdownServer(p);
});
it('投稿できる', async(async () => {
const post = {
text: 'test',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
}));
it('ファイルを添付できる', async(async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await request('/notes/create', {
fileIds: [file.id],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
}));
it('他人のファイルは無視', async(async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await request('/notes/create', {
text: 'test',
fileIds: [file.id],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('存在しないファイルは無視', async(async () => {
const res = await request('/notes/create', {
text: 'test',
fileIds: ['000000000000000000000000'],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('不正なファイルIDは無視', async(async () => {
const res = await request('/notes/create', {
fileIds: ['kyoppie'],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('返信できる', async(async () => {
const bobPost = await post(bob, {
text: 'foo',
});
const alicePost = {
text: 'bar',
replyId: bobPost.id,
};
const res = await request('/notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
}));
it('renoteできる', async(async () => {
const bobPost = await post(bob, {
text: 'test',
});
const alicePost = {
renoteId: bobPost.id,
};
const res = await request('/notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}));
it('引用renoteできる', async(async () => {
const bobPost = await post(bob, {
text: 'test',
});
const alicePost = {
text: 'test',
renoteId: bobPost.id,
};
const res = await request('/notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}));
it('文字数ぎりぎりで怒られない', async(async () => {
const post = {
text: '!'.repeat(3000),
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
}));
it('文字数オーバーで怒られる', async(async () => {
const post = {
text: '!'.repeat(3001),
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないリプライ先で怒られる', async(async () => {
const post = {
text: 'test',
replyId: '000000000000000000000000',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 404);
}));
it('存在しないrenote対象で怒られる', async(async () => {
const post = {
renoteId: '000000000000000000000000',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 404);
}));
it('不正なリプライ先IDで怒られる', async(async () => {
const post = {
text: 'test',
replyId: 'foo',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 404);
}));
it('不正なrenote対象IDで怒られる', async(async () => {
const post = {
renoteId: 'foo',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 404);
}));
it('存在しないユーザーにメンションできる', async(async () => {
const post = {
text: '@ghost yo',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
}));
it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
const post = {
text: '@bob @bob @bob yo',
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
}));
describe('notes/create', () => {
it('投票を添付できる', async(async () => {
const res = await request('/notes/create', {
text: 'test',
poll: {
choices: ['foo', 'bar'],
},
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.poll != null, true);
}));
it('投票の選択肢が無くて怒られる', async(async () => {
const res = await request('/notes/create', {
poll: {},
}, alice);
assert.strictEqual(res.status, 400);
}));
it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
const res = await request('/notes/create', {
poll: {
choices: [],
},
}, alice);
assert.strictEqual(res.status, 400);
}));
it('投票の選択肢が1つで怒られる', async(async () => {
const res = await request('/notes/create', {
poll: {
choices: ['Strawberry Pasta'],
},
}, alice);
assert.strictEqual(res.status, 400);
}));
it('投票できる', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
assert.strictEqual(res.status, 204);
}));
it('複数投票できない', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
}, alice);
await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
assert.strictEqual(res.status, 409);
}));
it('許可されている場合は複数投票できる', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
multiple: true,
},
}, alice);
await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0,
}, alice);
await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2,
}, alice);
assert.strictEqual(res.status, 204);
}));
it('締め切られている場合は投票できない', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
expiredAfter: 1,
},
}, alice);
await new Promise(x => setTimeout(x, 2));
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1,
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('notes/delete', () => {
it('delete a reply', async(async () => {
const mainNoteRes = await api('notes/create', {
text: 'main post',
}, alice);
const replyOneRes = await api('notes/create', {
text: 'reply one',
replyId: mainNoteRes.body.createdNote.id,
}, alice);
const replyTwoRes = await api('notes/create', {
text: 'reply two',
replyId: mainNoteRes.body.createdNote.id,
}, alice);
const deleteOneRes = await api('notes/delete', {
noteId: replyOneRes.body.createdNote.id,
}, alice);
assert.strictEqual(deleteOneRes.status, 204);
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.strictEqual(mainNote.repliesCount, 1);
const deleteTwoRes = await api('notes/delete', {
noteId: replyTwoRes.body.createdNote.id,
}, alice);
assert.strictEqual(deleteTwoRes.status, 204);
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.strictEqual(mainNote.repliesCount, 0);
}));
});
});

View file

@ -1,13 +0,0 @@
import * as assert from 'assert';
import { query } from '../../built/prelude/url.js';
describe('url', () => {
it('query', () => {
const s = query({
foo: 'ふぅ',
bar: 'b a r',
baz: undefined,
});
assert.deepStrictEqual(s, 'foo=%E3%81%B5%E3%81%85&bar=b%20a%20r');
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

View file

@ -1,58 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import * as sinon from 'sinon';
import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.mjs';
describe('Creating a block activity', function() {
this.timeout(20*60*1000);
let p;
// alice blocks bob
let alice, bob, carol;
before(async () => {
await initTestDb();
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
bob.host = 'http://remote';
carol.host = 'http://remote';
});
beforeEach(() => {
sinon.restore();
});
after(async () => {
await shutdownServer(p);
});
it('Should federate blocks normally', async(async () => {
const createBlock = (await import('../../built/services/blocking/create')).default;
const deleteBlock = (await import('../../built/services/blocking/delete')).default;
const queues = await import('../../built/queue/index');
const spy = sinon.spy(queues, 'deliver');
await createBlock(alice, bob);
assert(spy.calledOnce);
await deleteBlock(alice, bob);
assert(spy.calledTwice);
}));
it('Should not federate blocks if federateBlocks is false', async () => {
const createBlock = (await import('../../built/services/blocking/create')).default;
const deleteBlock = (await import('../../built/services/blocking/delete')).default;
alice.federateBlocks = true;
const queues = await import('../../built/queue/index');
const spy = sinon.spy(queues, 'deliver');
await createBlock(alice, carol);
await deleteBlock(alice, carol);
assert(spy.notCalled);
});
});

View file

@ -1,543 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { Following } from '../built/models/entities/following.js';
import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.mjs';
describe('Streaming', () => {
let p;
let Followings;
const follow = async (follower, followee) => {
await Followings.save({
id: 'a',
createdAt: new Date(),
followerId: follower.id,
followeeId: followee.id,
followerHost: follower.host,
followerInbox: null,
followerSharedInbox: null,
followeeHost: followee.host,
followeeInbox: null,
followeeSharedInbox: null,
});
};
describe('Streaming', function() {
this.timeout(20*60*1000);
// Local users
let ayano, kyoko, chitose;
// Remote users
let akari, chinatsu;
let kyokoNote, list;
before(async () => {
p = await startServer();
const connection = await initTestDb(true);
Followings = connection.getRepository(Following);
ayano = await signup({ username: 'ayano' });
kyoko = await signup({ username: 'kyoko' });
chitose = await signup({ username: 'chitose' });
akari = await signup({ username: 'akari', host: 'example.com' });
chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
kyokoNote = await post(kyoko, { text: 'foo' });
// Follow: ayano => kyoko
await api('following/create', { userId: kyoko.id }, ayano);
// Follow: ayano => akari
await follow(ayano, akari);
// List: chitose => ayano, kyoko
list = await api('users/lists/create', {
name: 'my list',
}, chitose).then(x => x.body);
await api('users/lists/push', {
listId: list.id,
userId: ayano.id,
}, chitose);
await api('users/lists/push', {
listId: list.id,
userId: kyoko.id,
}, chitose);
});
after(async () => {
await shutdownServer(p);
});
describe('Events', () => {
it('mention event', async () => {
const fired = await waitFire(
kyoko, 'main', // kyoko:main
() => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko
msg => msg.type === 'mention' && msg.body.userId === ayano.id // wait ayano
);
assert.strictEqual(fired, true);
});
it('renote event', async () => {
const fired = await waitFire(
kyoko, 'main', // kyoko:main
() => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id // wait renote
);
assert.strictEqual(fired, true);
});
});
describe('Home Timeline', () => {
it('自分の投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:Home
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
msg => msg.type === 'note' && msg.body.text === 'foo'
);
assert.strictEqual(fired, true);
});
it('フォローしているユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, true);
});
it('フォローしていないユーザーの投稿は流れない', async () => {
const fired = await waitFire(
kyoko, 'homeTimeline', // kyoko:home
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
msg => msg.type === 'note' && msg.body.userId === ayano.id // wait ayano
);
assert.strictEqual(fired, false);
});
it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id], }, kyoko), // kyoko dm => ayano
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, true);
});
it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id], }, kyoko), // kyoko dm => chitose
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, false);
});
}); // Home
describe('Local Timeline', () => {
it('自分の投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
msg => msg.type === 'note' && msg.body.text === 'foo'
);
assert.strictEqual(fired, true);
});
it('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts
msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose
);
assert.strictEqual(fired, true);
});
it('リモートユーザーの投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu
);
assert.strictEqual(fired, false);
});
it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, akari), // akari posts
msg => msg.type === 'note' && msg.body.userId === akari.id // wait akari
);
assert.strictEqual(fired, false);
});
it('ホーム指定の投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, false);
});
it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, false);
});
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose
);
assert.strictEqual(fired, false);
});
});
describe('Hybrid Timeline', () => {
it('自分の投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts
msg => msg.type === 'note' && msg.body.text === 'foo'
);
assert.strictEqual(fired, true);
});
it('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts
msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose
);
assert.strictEqual(fired, true);
});
it('フォローしているリモートユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, akari), // akari posts
msg => msg.type === 'note' && msg.body.userId === akari.id // wait akari
);
assert.strictEqual(fired, true);
});
it('フォローしていないリモートユーザーの投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu
);
assert.strictEqual(fired, false);
});
it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, true);
});
it('フォローしているユーザーのホーム投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, true);
});
it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'home' }, chitose),
msg => msg.type === 'note' && msg.body.userId === chitose.id
);
assert.strictEqual(fired, false);
});
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
msg => msg.type === 'note' && msg.body.userId === chitose.id
);
assert.strictEqual(fired, false);
});
});
describe('Global Timeline', () => {
it('フォローしていないローカルユーザーの投稿が流れる', () => async () => {
const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts
msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose
);
assert.strictEqual(fired, true);
});
it('フォローしていないリモートユーザーの投稿が流れる', () => async () => {
const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu
);
assert.strictEqual(fired, true);
});
it('ホーム投稿は流れない', () => async () => {
const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts
msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko
);
assert.strictEqual(fired, false);
});
});
describe('UserList Timeline', () => {
it('リストに入れているユーザーの投稿が流れる', () => async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo' }, ayano),
msg => msg.type === 'note' && msg.body.userId === ayano.id,
{ listId: list.id, }
);
assert.strictEqual(fired, true);
});
it('リストに入れていないユーザーの投稿は流れない', () => async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo' }, chinatsu),
msg => msg.type === 'note' && msg.body.userId === chinatsu.id,
{ listId: list.id, }
);
assert.strictEqual(fired, false);
});
// #4471
it('リストに入れているユーザーのダイレクト投稿が流れる', () => async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano),
msg => msg.type === 'note' && msg.body.userId === ayano.id,
{ listId: list.id, }
);
assert.strictEqual(fired, true);
});
// #4335
it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => async () => {
const fired = await waitFire(
chitose, 'userList',
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
{ listId: list.id, }
);
assert.strictEqual(fired, false);
});
});
describe('Hashtag Timeline', () => {
it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.text, '#foo');
ws.close();
done();
}
}, {
q: [
['foo'],
],
});
post(chitose, {
text: '#foo',
});
}));
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
if (type == 'note') {
if (body.text === '#foo') fooCount++;
if (body.text === '#bar') barCount++;
if (body.text === '#foo #bar') fooBarCount++;
}
}, {
q: [
['foo', 'bar'],
],
});
post(chitose, {
text: '#foo',
});
post(chitose, {
text: '#bar',
});
post(chitose, {
text: '#foo #bar',
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
ws.close();
done();
}, 3000);
}));
it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
if (type == 'note') {
if (body.text === '#foo') fooCount++;
if (body.text === '#bar') barCount++;
if (body.text === '#foo #bar') fooBarCount++;
if (body.text === '#piyo') piyoCount++;
}
}, {
q: [
['foo'],
['bar'],
],
});
post(chitose, {
text: '#foo',
});
post(chitose, {
text: '#bar',
});
post(chitose, {
text: '#foo #bar',
});
post(chitose, {
text: '#piyo',
});
setTimeout(() => {
assert.strictEqual(fooCount, 1);
assert.strictEqual(barCount, 1);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 0);
ws.close();
done();
}, 3000);
}));
it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
let waaaCount = 0;
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
if (type == 'note') {
if (body.text === '#foo') fooCount++;
if (body.text === '#bar') barCount++;
if (body.text === '#foo #bar') fooBarCount++;
if (body.text === '#piyo') piyoCount++;
if (body.text === '#waaa') waaaCount++;
}
}, {
q: [
['foo', 'bar'],
['piyo'],
],
});
post(chitose, {
text: '#foo',
});
post(chitose, {
text: '#bar',
});
post(chitose, {
text: '#foo #bar',
});
post(chitose, {
text: '#piyo',
});
post(chitose, {
text: '#waaa',
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 1);
assert.strictEqual(waaaCount, 0);
ws.close();
done();
}, 3000);
}));
});
});
});

View file

@ -1,103 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.mjs';
describe('Note thread mute', function() {
this.timeout(20*60*1000);
let p;
let alice, bob, carol;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('notes/mentions にミュートしているスレッドの投稿が含まれない', async(async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await request('/notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note) => note.id === carolReply.id), false);
assert.strictEqual(res.body.some((note) => note.id === carolReplyWithoutMention.id), false);
}));
it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const res = await request('/i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false;
const ws = await connectStream(alice, 'main', async ({ type, body }) => {
if (type === 'unreadMention') {
if (body === bobNote.id) return;
fired = true;
}
});
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
it('i/notifications にミュートしているスレッドの通知が含まれない', async(async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await request('/i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification) => notification.note.id === carolReply.id), false);
assert.strictEqual(res.body.some((notification) => notification.note.id === carolReplyWithoutMention.id), false);
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
}));
});

View file

@ -1,60 +0,0 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.mjs';
describe('users/notes', function() {
this.timeout(20*60*1000);
let p;
let alice, jpgNote, pngNote, jpgPngNote;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
jpgNote = await post(alice, {
fileIds: [jpg.id],
});
pngNote = await post(alice, {
fileIds: [png.id],
});
jpgPngNote = await post(alice, {
fileIds: [jpg.id, png.id],
});
});
after(async() => {
await shutdownServer(p);
});
it('ファイルタイプ指定 (jpg)', async(async () => {
const res = await request('/users/notes', {
userId: alice.id,
fileType: ['image/jpeg'],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 2);
assert.strictEqual(res.body.some((note) => note.id === jpgNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === jpgPngNote.id), true);
}));
it('ファイルタイプ指定 (jpg or png)', async(async () => {
const res = await request('/users/notes', {
userId: alice.id,
fileType: ['image/jpeg', 'image/png'],
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 3);
assert.strictEqual(res.body.some((note) => note.id === jpgNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === pngNote.id), true);
assert.strictEqual(res.body.some((note) => note.id === jpgPngNote.id), true);
}));
});

View file

@ -1,322 +0,0 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as childProcess from 'child_process';
import * as http from 'node:http';
import { SIGKILL } from 'constants';
import WebSocket from 'ws';
import * as foundkey from 'foundkey-js';
import fetch from 'node-fetch';
import FormData from 'form-data';
import { DataSource } from 'typeorm';
import { loadConfig } from '../built/config/load.js';
import { entities } from '../built/db/postgre.js';
import got from 'got';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const config = loadConfig();
export const port = config.port;
export const async = (fn) => (done) => {
fn().then(() => {
done();
}, (err) => {
done(err);
});
};
export const api = async (endpoint, params, me) => {
endpoint = endpoint.replace(/^\//, '');
const auth = me ? { authorization: `Bearer ${me.token}` } : {};
const res = await got(`http://localhost:${port}/api/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...auth,
},
body: JSON.stringify(params),
retry: {
limit: 0,
},
hooks: {
beforeError: [
error => {
const { response } = error;
if (response && response.body) console.warn(response.body);
return error;
}
]
},
});
const status = res.statusCode;
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
return {
status,
body
};
};
export const request = async (endpoint, params, me) => {
const auth = me ? { authorization: `Bearer ${me.token}` } : {};
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...auth,
},
body: JSON.stringify(params),
});
const status = res.status;
const body = res.status !== 204 ? await res.json().catch() : null;
return {
body, status,
};
};
export const signup = async (params) => {
const q = Object.assign({
username: 'test',
password: 'test',
}, params);
const res = await api('signup', q);
return res.body;
};
export const post = async (user, params) => {
const q = Object.assign({
text: 'test',
}, params);
const res = await api('notes/create', q, user);
return res.body ? res.body.createdNote : null;
};
export const react = async (user, note, reaction) => {
await api('notes/reactions/create', {
noteId: note.id,
reaction: reaction,
}, user);
};
/**
* Upload file
* @param user User
* @param _path Optional, absolute path or relative from ./resources/
*/
export const uploadFile = async (user, _path) => {
const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`;
const formData = new FormData();
formData.append('i', user.token);
formData.append('file', fs.createReadStream(absPath));
formData.append('force', 'true');
const res = await got<string>(`http://localhost:${port}/api/drive/files/create`, {
method: 'POST',
body: formData,
retry: {
limit: 0,
},
});
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
return body;
};
export const uploadUrl = async (user, url) => {
let file;
const ws = await connectStream(user, 'main', (msg) => {
if (msg.type === 'driveFileCreated') {
file = msg.body;
}
});
await api('drive/files/upload-from-url', {
url,
force: true,
}, user);
await sleep(5000);
ws.close();
return file;
};
export function connectStream(user, channel, listener, params) {
return new Promise((res, rej) => {
const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`);
ws.on('open', () => {
ws.on('message', data => {
const msg = JSON.parse(data.toString());
if (msg.type === 'channel' && msg.body.id === 'a') {
listener(msg.body);
} else if (msg.type === 'connected' && msg.body.id === 'a') {
res(ws);
}
});
ws.send(JSON.stringify({
type: 'connect',
body: {
channel: channel,
id: 'a',
pong: true,
params: params,
},
}));
});
});
}
export const waitFire = async (user, channel, trgr, cond, params) => {
return new Promise(async (res, rej) => {
let timer;
let ws;
try {
ws = await connectStream(user, channel, msg => {
if (cond(msg)) {
ws.close();
if (timer) clearTimeout(timer);
res(true);
}
}, params);
} catch (e) {
rej(e);
}
if (!ws) return;
timer = setTimeout(() => {
ws.close();
res(false);
}, 3000);
try {
await trgr();
} catch (e) {
ws.close();
if (timer) clearTimeout(timer);
rej(e);
}
})
};
export const simpleGet = async (path, accept = '*/*') => {
// node-fetchだと3xxを取れない
return await new Promise((resolve, reject) => {
const req = http.request(`http://localhost:${port}${path}`, {
headers: {
Accept: accept,
},
}, res => {
if (res.statusCode >= 400) {
reject(res);
} else {
resolve({
status: res.statusCode,
type: res.headers['content-type'],
location: res.headers.location,
});
}
});
req.end();
});
};
export function launchServer(callbackSpawnedProcess, moreProcess = async () => {}) {
return (done) => {
const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test', PATH: process.env.PATH },
});
callbackSpawnedProcess(p);
p.on('message', message => {
if (message === 'ok') moreProcess().then(() => done()).catch(e => done(e));
});
};
}
export async function initTestDb(justBorrow = false, initEntities) {
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
const db = new DataSource({
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
synchronize: true && !justBorrow,
dropSchema: true && !justBorrow,
entities: initEntities || entities,
});
await db.initialize();
return db;
}
export function startServer(timeout = 60 * 1000) {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
rej('timeout to start');
}, timeout);
const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test', PATH: process.env.PATH },
});
p.on('error', e => rej(e));
p.on('message', message => {
if (message === 'ok') {
clearTimeout(t);
res(p);
}
});
});
}
export function shutdownServer(p, timeout = 20 * 1000) {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
res('force exit');
}, timeout);
p.once('exit', () => {
clearTimeout(t);
res('exited');
});
p.kill();
});
}
export function sleep(msec) {
return new Promise(res => {
setTimeout(() => {
res();
}, msec);
});
}

View file

@ -65,7 +65,6 @@
"@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1", "@typescript-eslint/parser": "^5.46.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.3.0",
"eslint": "^8.29.0", "eslint": "^8.29.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-vue": "^9.8.0" "eslint-plugin-vue": "^9.8.0"

View file

@ -1,45 +0,0 @@
import { expectType } from 'tsd';
import * as foundkey from '../src';
describe('API', () => {
test('success', async () => {
const cli = new foundkey.api.APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN'
});
const res = await cli.request('meta', { detail: true });
expectType<foundkey.entities.DetailedInstanceMetadata>(res);
});
test('conditional respose type (meta)', async () => {
const cli = new foundkey.api.APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN'
});
const res = await cli.request('meta', { detail: true });
expectType<foundkey.entities.DetailedInstanceMetadata>(res);
const res2 = await cli.request('meta', { detail: false });
expectType<foundkey.entities.LiteInstanceMetadata>(res2);
const res3 = await cli.request('meta', { });
expectType<foundkey.entities.LiteInstanceMetadata>(res3);
const res4 = await cli.request('meta', { detail: true as boolean });
expectType<foundkey.entities.LiteInstanceMetadata | foundkey.entities.DetailedInstanceMetadata>(res4);
});
test('conditional respose type (users/show)', async () => {
const cli = new foundkey.api.APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN'
});
const res = await cli.request('users/show', { userId: 'xxxxxxxx' });
expectType<foundkey.entities.UserDetailed>(res);
const res2 = await cli.request('users/show', { userIds: ['xxxxxxxx'] });
expectType<foundkey.entities.UserDetailed[]>(res2);
});
});

View file

@ -1,26 +0,0 @@
import { expectType } from 'tsd';
import * as foundkey from '../src';
describe('Streaming', () => {
test('emit type', async () => {
const stream = new foundkey.Stream('https://foundkey.test', { token: 'TOKEN' });
const mainChannel = stream.useChannel('main');
mainChannel.on('notification', notification => {
expectType<foundkey.entities.Notification>(notification);
});
});
test('params type', async () => {
const stream = new foundkey.Stream('https://foundkey.test', { token: 'TOKEN' });
// TODO: 「stream.useChannel の第二引数として受け入れる型が
// {
// otherparty?: User['id'] | null;
// group?: UserGroup['id'] | null;
// }
// になっている」というテストを行いたいけどtsdでの書き方がわからない
const messagingChannel = stream.useChannel('messaging', { otherparty: 'aaa' });
messagingChannel.on('message', message => {
expectType<foundkey.entities.MessagingMessage>(message);
});
});
});

View file

@ -1,212 +0,0 @@
import { APIClient, isAPIError } from '../src/api';
import { enableFetchMocks } from 'jest-fetch-mock';
enableFetchMocks();
function getFetchCall(call: any[]) {
const { body, method } = call[1];
if (body != null && typeof body != 'string') {
throw new Error('invalid body');
}
return {
url: call[0],
method: method,
body: JSON.parse(body as any)
};
}
describe('API', () => {
test('success', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://foundkey.test/api/i') {
if (body.i === 'TOKEN') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 400 };
}
} else {
return { status: 404 };
}
});
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
const res = await cli.request('i');
expect(res).toEqual({
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://foundkey.test/api/i',
method: 'POST',
body: { i: 'TOKEN' }
});
});
test('with params', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://foundkey.test/api/notes/show') {
if (body.i === 'TOKEN' && body.noteId === 'aaaaa') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 400 };
}
} else {
return { status: 404 };
}
});
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
const res = await cli.request('notes/show', { noteId: 'aaaaa' });
expect(res).toEqual({
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://foundkey.test/api/notes/show',
method: 'POST',
body: { i: 'TOKEN', noteId: 'aaaaa' }
});
});
test('204 No Content で null が返る', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
if (req.method == 'POST' && req.url == 'https://foundkey.test/api/reset-password') {
return { status: 204 };
} else {
return { status: 404 };
}
});
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
const res = await cli.request('reset-password', { token: 'aaa', password: 'aaa' });
expect(res).toEqual(null);
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://foundkey.test/api/reset-password',
method: 'POST',
body: { i: 'TOKEN', token: 'aaa', password: 'aaa' }
});
});
test('インスタンスの credential が指定されていても引数で credential が null ならば null としてリクエストされる', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://foundkey.test/api/i') {
if (typeof body.i === 'string') {
return JSON.stringify({ id: 'foo' });
} else {
return {
status: 401,
body: JSON.stringify({
error: {
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
}
})
};
}
} else {
return { status: 404 };
}
});
try {
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
await cli.request('i', {}, null);
} catch (e) {
expect(isAPIError(e)).toEqual(true);
}
});
test('api error', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
return {
status: 500,
body: JSON.stringify({
error: {
message: 'Internal error occurred. Please contact us if the error persists.',
code: 'INTERNAL_ERROR',
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
kind: 'server',
},
})
};
});
try {
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
await cli.request('i');
} catch (e: any) {
expect(isAPIError(e)).toEqual(true);
expect(e.id).toEqual('5d37dbcb-891e-41ca-a3d6-e690c97775ac');
}
});
test('network error', async () => {
fetchMock.resetMocks();
fetchMock.mockAbort();
try {
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
await cli.request('i');
} catch (e) {
expect(isAPIError(e)).toEqual(false);
}
});
test('json parse error', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
return {
status: 500,
body: '<html>I AM NOT JSON</html>'
};
});
try {
const cli = new APIClient({
origin: 'https://foundkey.test',
credential: 'TOKEN',
});
await cli.request('i');
} catch (e) {
expect(isAPIError(e)).toEqual(false);
}
});
});

View file

@ -1,165 +0,0 @@
import WS from 'jest-websocket-mock';
import Stream from '../src/streaming';
describe('Streaming', () => {
test('useChannel', async () => {
const server = new WS('wss://foundkey.test/streaming');
const stream = new Stream('https://foundkey.test', { token: 'TOKEN' });
const mainChannelReceived: any[] = [];
const main = stream.useChannel('main');
main.on('meUpdated', payload => {
mainChannelReceived.push(payload);
});
const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN');
const msg = JSON.parse(await server.nextMessage as string);
const mainChannelId = msg.body.id;
expect(msg.type).toEqual('connect');
expect(msg.body.channel).toEqual('main');
expect(mainChannelId != null).toEqual(true);
server.send(JSON.stringify({
type: 'channel',
body: {
id: mainChannelId,
type: 'meUpdated',
body: {
id: 'foo'
}
}
}));
expect(mainChannelReceived[0]).toEqual({
id: 'foo'
});
stream.close();
server.close();
});
test('useChannel with parameters', async () => {
const server = new WS('wss://foundkey.test/streaming');
const stream = new Stream('https://foundkey.test', { token: 'TOKEN' });
const messagingChannelReceived: any[] = [];
const messaging = stream.useChannel('messaging', { otherparty: 'aaa' });
messaging.on('message', payload => {
messagingChannelReceived.push(payload);
});
const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN');
const msg = JSON.parse(await server.nextMessage as string);
const messagingChannelId = msg.body.id;
expect(msg.type).toEqual('connect');
expect(msg.body.channel).toEqual('messaging');
expect(msg.body.params).toEqual({ otherparty: 'aaa' });
expect(messagingChannelId != null).toEqual(true);
server.send(JSON.stringify({
type: 'channel',
body: {
id: messagingChannelId,
type: 'message',
body: {
id: 'foo'
}
}
}));
expect(messagingChannelReceived[0]).toEqual({
id: 'foo'
});
stream.close();
server.close();
});
test('ちゃんとチャンネルごとにidが異なる', async () => {
const server = new WS('wss://foundkey.test/streaming');
const stream = new Stream('https://foundkey.test', { token: 'TOKEN' });
stream.useChannel('messaging', { otherparty: 'aaa' });
stream.useChannel('messaging', { otherparty: 'bbb' });
const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN');
const msg = JSON.parse(await server.nextMessage as string);
const messagingChannelId = msg.body.id;
const msg2 = JSON.parse(await server.nextMessage as string);
const messagingChannelId2 = msg2.body.id;
expect(messagingChannelId != null).toEqual(true);
expect(messagingChannelId2 != null).toEqual(true);
expect(messagingChannelId).not.toEqual(messagingChannelId2);
stream.close();
server.close();
});
test('Connection#send', async () => {
const server = new WS('wss://foundkey.test/streaming');
const stream = new Stream('https://foundkey.test', { token: 'TOKEN' });
const messaging = stream.useChannel('messaging', { otherparty: 'aaa' });
messaging.send('read', { id: 'aaa' });
const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN');
const connectMsg = JSON.parse(await server.nextMessage as string);
const channelId = connectMsg.body.id;
const msg = JSON.parse(await server.nextMessage as string);
expect(msg.type).toEqual('ch');
expect(msg.body.id).toEqual(channelId);
expect(msg.body.type).toEqual('read');
expect(msg.body.body).toEqual({ id: 'aaa' });
stream.close();
server.close();
});
test('Connection#dispose', async () => {
const server = new WS('wss://foundkey.test/streaming');
const stream = new Stream('https://foundkey.test', { token: 'TOKEN' });
const mainChannelReceived: any[] = [];
const main = stream.useChannel('main');
main.on('meUpdated', payload => {
mainChannelReceived.push(payload);
});
const ws = await server.connected;
expect(new URLSearchParams(new URL(ws.url).search).get('i')).toEqual('TOKEN');
const msg = JSON.parse(await server.nextMessage as string);
const mainChannelId = msg.body.id;
expect(msg.type).toEqual('connect');
expect(msg.body.channel).toEqual('main');
expect(mainChannelId != null).toEqual(true);
main.dispose();
server.send(JSON.stringify({
type: 'channel',
body: {
id: mainChannelId,
type: 'meUpdated',
body: {
id: 'foo'
}
}
}));
expect(mainChannelReceived.length).toEqual(0);
stream.close();
server.close();
});
// TODO: SharedConnection#dispose して一定時間経ったら disconnect メッセージがサーバーに送られてくるかのテスト
// TODO: チャンネル接続が使いまわされるかのテスト
});

View file

@ -3,7 +3,7 @@ const path = require("path");
const ruleFiles = fs const ruleFiles = fs
.readdirSync(__dirname) .readdirSync(__dirname)
.filter((file) => file !== "index.js" && !file.endsWith("test.js")); .filter((file) => file !== "index.js");
const rules = Object.fromEntries( const rules = Object.fromEntries(
ruleFiles.map((file) => [path.basename(file, ".js"), require("./" + file)]) ruleFiles.map((file) => [path.basename(file, ".js"), require("./" + file)])

1099
yarn.lock

File diff suppressed because it is too large Load diff