merge: some future changes coming from upstream (!465)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/465

Approved-by: Marie <marie@kaifa.ch>
This commit is contained in:
dakkar 2024-04-12 01:19:38 +00:00
commit ee90f2babc
60 changed files with 2178 additions and 2093 deletions

View file

@ -1,95 +0,0 @@
name: Lint
on:
push:
branches:
- stable
- develop
paths:
- packages/**
pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
jobs:
pnpm_install:
runs-on: docker
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: https://github.com/pnpm/action-setup@v2
with:
version: 8
run_install: false
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
lint:
needs: [pnpm_install]
runs-on: docker
continue-on-error: true
strategy:
matrix:
workspace:
- backend
- frontend
- sw
- misskey-js
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: https://github.com/pnpm/action-setup@v2
with:
version: 7
run_install: false
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- run: pnpm --filter ${{ matrix.workspace }} run eslint
typecheck:
needs: [pnpm_install]
runs-on: docker
continue-on-error: true
strategy:
matrix:
workspace:
- backend
- misskey-js
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
- uses: https://github.com/pnpm/action-setup@v2
with:
version: 7
run_install: false
- uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- run: pnpm --filter misskey-js run build
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter misskey-reversi run build:tsc
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter misskey-bubble-game run build
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter ${{ matrix.workspace }} run typecheck

View file

@ -1,17 +1,19 @@
<!-- ## Unreleased
## 202x.x.x (unreleased)
### General ### General
- -
### Client ### Client
- - Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
### Server ### Server
- -
-->
## 2024.3.1 ## 2024.3.1
### General ### General

View file

@ -307,6 +307,98 @@ export const handlers = [
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Nest
### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合
#### forwardRef
まずは簡単に`forwardRef`を試してみる
```typescript
export class FooService {
constructor(
@Inject(forwardRef(() => BarService))
private barService: BarService
) {
}
}
```
#### OnModuleInit
できなければ`OnModuleInit`を使う
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { BarService } from '@/core/BarService';
@Injectable()
export class FooService implements OnModuleInit {
private barService: BarService // constructorから移動してくる
constructor(
private moduleRef: ModuleRef,
) {
}
async onModuleInit() {
this.barService = this.moduleRef.get(BarService.name);
}
public async niceMethod() {
return await this.barService.incredibleMethod({ hoge: 'fuga' });
}
}
```
##### Service Unit Test
テストで`onModuleInit`を呼び出す必要がある
```typescript
// import ...
describe('test', () => {
let app: TestingModule;
let fooService: FooService; // for test case
let barService: BarService; // for test case
beforeEach(async () => {
app = await Test.createTestingModule({
imports: ...,
providers: [
FooService,
{ // mockする (mockは必須ではないかもしれない)
provide: BarService,
useFactory: () => ({
incredibleMethod: jest.fn(),
}),
},
{ // Provideにする
provide: BarService.name,
useExisting: BarService,
},
],
})
.useMocker(...
.compile();
fooService = app.get<FooService>(FooService);
barService = app.get<BarService>(BarService) as jest.Mocked<BarService>;
// onModuleInitを実行する
await fooService.onModuleInit();
});
test('nice', () => {
await fooService.niceMethod();
expect(barService.incredibleMethod).toHaveBeenCalled();
expect(barService.incredibleMethod.mock.lastCall![0])
.toEqual({ hoge: 'fuga' });
});
})
```
## Notes ## Notes
### Misskeyのドメイン固有の概念は`Mi`をprefixする ### Misskeyのドメイン固有の概念は`Mi`をprefixする

10
locales/index.d.ts vendored
View file

@ -2044,6 +2044,10 @@ export interface Locale extends ILocale {
* *
*/ */
"showNoteActionsOnlyHover": string; "showNoteActionsOnlyHover": string;
/**
*
*/
"showReactionsCount": string;
/** /**
* *
*/ */
@ -9167,7 +9171,11 @@ export interface Locale extends ILocale {
*/ */
"reactedBySomeUsers": ParameterizedString<"n">; "reactedBySomeUsers": ParameterizedString<"n">;
/** /**
* {n} * {n}
*/
"likedBySomeUsers": ParameterizedString<"n">;
/**
* {n}
*/ */
"renotedBySomeUsers": ParameterizedString<"n">; "renotedBySomeUsers": ParameterizedString<"n">;
/** /**

View file

@ -507,6 +507,7 @@ emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
showReactionsCount: "ノートのリアクション数を表示する"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする" enableAdvancedMfm: "高度なMFMを有効にする"
@ -2419,7 +2420,8 @@ _notification:
sendTestNotification: "テスト通知を送信する" sendTestNotification: "テスト通知を送信する"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
reactedBySomeUsers: "{n}人がリアクションしました" reactedBySomeUsers: "{n}人がリアクションしました"
renotedBySomeUsers: "{n}人がブーストしました" likedBySomeUsers: "{n}人がいいねしました"
renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"

View file

@ -51,21 +51,35 @@ export class FetchInstanceMetadataService {
} }
@bindThis @bindThis
public async tryLock(host: string): Promise<boolean> { // public for test
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET'); public async tryLock(host: string): Promise<string | null> {
return mutex !== '1'; // TODO: マイグレーションなのであとで消す (2024.3.1)
this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
return await this.redisClient.set(
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
'GET' // 古い値を返すなかったらnull
);
} }
@bindThis @bindThis
public unlock(host: string): Promise<'OK'> { // public for test
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0'); public unlock(host: string): Promise<number> {
return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`);
} }
@bindThis @bindThis
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> { public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
const host = instance.host; const host = instance.host;
// Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return; // finallyでunlockされてしまうのでtry内でロックチェックをしない
// returnであってもfinallyは実行される
if (!force && await this.tryLock(host) === '1') {
// 1が返ってきていたらロックされているという意味なので、何もしない
return;
}
try { try {
if (!force) { if (!force) {
const _instance = await this.federatedInstanceService.fetch(host); const _instance = await this.federatedInstanceService.fetch(host);

View file

@ -248,7 +248,7 @@ export class DriveFileEntityService {
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
detail: true, detail: true,
}) : null, }) : null,
userId: opts.withUser ? file.userId : null, userId: file.userId,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
}); });
} }

View file

@ -351,6 +351,7 @@ export class NoteEntityService implements OnModuleInit {
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
reactions: this.reactionService.convertLegacyReactions(note.reactions), reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,

View file

@ -223,6 +223,10 @@ export const packedNoteSchema = {
}], }],
}, },
}, },
reactionCount: {
type: 'number',
optional: false, nullable: false,
},
renoteCount: { renoteCount: {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -86,8 +86,8 @@
//#endregion //#endregion
//#region Script //#region Script
function importAppScript() { async function importAppScript() {
import(`/vite/${CLIENT_ENTRY}`) await import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => { .catch(async e => {
console.error(e); console.error(e);
renderError('APP_IMPORT', e); renderError('APP_IMPORT', e);

View file

@ -187,7 +187,7 @@ describe('2要素認証', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('が設定でき、OTPでログインできる。', async () => { test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
@ -197,18 +197,18 @@ describe('2要素認証', () => {
assert.strictEqual(registerResponse.body.label, username); assert.strictEqual(registerResponse.body.label, username);
assert.strictEqual(registerResponse.body.issuer, config.host); assert.strictEqual(registerResponse.body.issuer, config.host);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}, alice); }, alice);
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
@ -216,24 +216,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、セキュリティキーでログインできる。', async () => { test('が設定でき、セキュリティキーでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
@ -243,23 +243,23 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName); assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, true); assert.strictEqual(usersShowResponse.body.securityKeys, true);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
@ -268,7 +268,7 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.allowCredentials, undefined); assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url')); assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponse.body, requestOptions: signinResponse.body,
@ -277,24 +277,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => { test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
@ -302,33 +302,33 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const passwordLessResponse = await api('/i/2fa/password-less', { const passwordLessResponse = await api('i/2fa/password-less', {
value: true, value: true,
}, alice); }, alice);
assert.strictEqual(passwordLessResponse.status, 204); assert.strictEqual(passwordLessResponse.status, 204);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
password: '', password: '',
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined); assert.strictEqual(signinResponse.body.i, undefined);
const signinResponse2 = await api('/signin', { const signinResponse2 = await api('signin', {
...signinWithSecurityKeyParam({ ...signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
@ -340,24 +340,24 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => { test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
@ -365,22 +365,22 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key'; const renamedKey = 'other-key';
const updateKeyResponse = await api('/i/2fa/update-key', { const updateKeyResponse = await api('i/2fa/update-key', {
name: renamedKey, name: renamedKey,
credentialId: credentialId.toString('base64url'), credentialId: credentialId.toString('base64url'),
}, alice); }, alice);
assert.strictEqual(updateKeyResponse.status, 200); assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('/i', { const iResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
@ -389,24 +389,24 @@ describe('2要素認証', () => {
assert.notEqual(securityKeys[0].lastUsed, undefined); assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、設定したセキュリティキーを削除できる。', async () => { test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', { const registerKeyResponse = await api('i/2fa/register-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
@ -414,20 +414,20 @@ describe('2要素認証', () => {
const keyName = 'example-key'; const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41); const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ const keyDoneResponse = await api('i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
keyName, keyName,
credentialId, credentialId,
creationOptions: registerKeyResponse.body, creationOptions: registerKeyResponse.body,
}), alice); }) as any, alice);
assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す // テストの実行順によっては複数残ってるので全部消す
const iResponse = await api('/i', { const iResponse = await api('i', {
}, alice); }, alice);
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
for (const key of iResponse.body.securityKeysList) { for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('/i/2fa/remove-key', { const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
credentialId: key.id, credentialId: key.id,
@ -435,13 +435,13 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200); assert.strictEqual(removeKeyResponse.status, 200);
} }
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.securityKeys, false); assert.strictEqual(usersShowResponse.body.securityKeys, false);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
@ -449,43 +449,43 @@ describe('2要素認証', () => {
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
}); });
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => { test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('i/2fa/register', {
password, password,
}, alice); }, alice);
assert.strictEqual(registerResponse.status, 200); assert.strictEqual(registerResponse.status, 200);
const doneResponse = await api('/i/2fa/done', { const doneResponse = await api('i/2fa/done', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', { const usersShowResponse = await api('users/show', {
username, username,
}); });
assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.status, 200);
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('/i/2fa/unregister', { const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
password, password,
}, alice); }, alice);
assert.strictEqual(unregisterResponse.status, 204); assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('/signin', { const signinResponse = await api('signin', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
await api('/i/2fa/unregister', { await api('i/2fa/unregister', {
password, password,
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}, alice); }, alice);

View file

@ -7,7 +7,6 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import { import {
api, api,
failedApiCall, failedApiCall,
@ -29,10 +28,7 @@ describe('アンテナ', () => {
// エンティティとしてのアンテナを主眼においたテストを記述する // エンティティとしてのアンテナを主眼においたテストを記述する
// (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからートを取得するエンドポイントをテストする) // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからートを取得するエンドポイントをテストする)
// BUG misskey-jsとjson-schemaが一致していない。 type Antenna = misskey.entities.Antenna;
// - srcのenumにgroupが残っている
// - userGroupIdが残っている, isActiveがない
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
type User = misskey.entities.SignupResponse; type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note; type Note = misskey.entities.Note;
@ -80,7 +76,7 @@ describe('アンテナ', () => {
aliceList = await userList(alice, {}); aliceList = await userList(alice, {});
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
aliceList = await userList(alice, {}); aliceList = await userList(alice, {});
bobFile = (await uploadFile(bob)).body; bobFile = (await uploadFile(bob)).body!;
bobList = await userList(bob); bobList = await userList(bob);
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice); await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
@ -129,9 +125,9 @@ describe('アンテナ', () => {
beforeEach(async () => { beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。 // テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) { for (const user of [alice, bob]) {
const list = await api('/antennas/list', {}, user); const list = await api('antennas/list', {}, user);
for (const antenna of list.body) { for (const antenna of list.body) {
await api('/antennas/delete', { antennaId: antenna.id }, user); await api('antennas/delete', { antennaId: antenna.id }, user);
} }
} }
}); });
@ -141,11 +137,11 @@ describe('アンテナ', () => {
test('が作成できること、キーが過不足なく入っていること。', async () => { test('が作成できること、キーが過不足なく入っていること。', async () => {
const response = await successfulApiCall({ const response = await successfulApiCall({
endpoint: 'antennas/create', endpoint: 'antennas/create',
parameters: { ...defaultParam }, parameters: defaultParam,
user: alice, user: alice,
}); });
assert.match(response.id, /[0-9a-z]{10}/); assert.match(response.id, /[0-9a-z]{10}/);
const expected = { const expected: Antenna = {
id: response.id, id: response.id,
caseSensitive: false, caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(), createdAt: new Date(response.createdAt).toISOString(),
@ -161,7 +157,7 @@ describe('アンテナ', () => {
withFile: false, withFile: false,
withReplies: false, withReplies: false,
localOnly: false, localOnly: false,
} as Antenna; };
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
@ -202,27 +198,27 @@ describe('アンテナ', () => {
}); });
const antennaParamPattern = [ const antennaParamPattern = [
{ parameters: (): object => ({ name: 'x'.repeat(100) }) }, { parameters: () => ({ name: 'x'.repeat(100) }) },
{ parameters: (): object => ({ name: 'x' }) }, { parameters: () => ({ name: 'x' }) },
{ parameters: (): object => ({ src: 'home' }) }, { parameters: () => ({ src: 'home' as const }) },
{ parameters: (): object => ({ src: 'all' }) }, { parameters: () => ({ src: 'all' as const }) },
{ parameters: (): object => ({ src: 'users' }) }, { parameters: () => ({ src: 'users' as const }) },
{ parameters: (): object => ({ src: 'list' }) }, { parameters: () => ({ src: 'list' as const }) },
{ parameters: (): object => ({ userListId: null }) }, { parameters: () => ({ userListId: null }) },
{ parameters: (): object => ({ src: 'list', userListId: aliceList.id }) }, { parameters: () => ({ src: 'list' as const, userListId: aliceList.id }) },
{ parameters: (): object => ({ keywords: [['x']] }) }, { parameters: () => ({ keywords: [['x']] }) },
{ parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, { parameters: () => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) }, { parameters: () => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
{ parameters: (): object => ({ users: [alice.username] }) }, { parameters: () => ({ users: [alice.username] }) },
{ parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) }, { parameters: () => ({ users: [alice.username, bob.username, carol.username] }) },
{ parameters: (): object => ({ caseSensitive: false }) }, { parameters: () => ({ caseSensitive: false }) },
{ parameters: (): object => ({ caseSensitive: true }) }, { parameters: () => ({ caseSensitive: true }) },
{ parameters: (): object => ({ withReplies: false }) }, { parameters: () => ({ withReplies: false }) },
{ parameters: (): object => ({ withReplies: true }) }, { parameters: () => ({ withReplies: true }) },
{ parameters: (): object => ({ withFile: false }) }, { parameters: () => ({ withFile: false }) },
{ parameters: (): object => ({ withFile: true }) }, { parameters: () => ({ withFile: true }) },
{ parameters: (): object => ({ notify: false }) }, { parameters: () => ({ notify: false }) },
{ parameters: (): object => ({ notify: true }) }, { parameters: () => ({ notify: true }) },
]; ];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({ const response = await successfulApiCall({
@ -335,7 +331,7 @@ describe('アンテナ', () => {
test.each([ test.each([
{ {
label: '全体から', label: '全体から',
parameters: (): object => ({ src: 'all' }), parameters: () => ({ src: 'all' }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -346,7 +342,7 @@ describe('アンテナ', () => {
{ {
// BUG e4144a1 以降home指定は壊れている(allと同じ) // BUG e4144a1 以降home指定は壊れている(allと同じ)
label: 'ホーム指定はallと同じ', label: 'ホーム指定はallと同じ',
parameters: (): object => ({ src: 'home' }), parameters: () => ({ src: 'home' }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
@ -357,7 +353,7 @@ describe('アンテナ', () => {
{ {
// https://github.com/misskey-dev/misskey/issues/9025 // https://github.com/misskey-dev/misskey/issues/9025
label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。', label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
{ note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true }, { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
@ -367,56 +363,56 @@ describe('アンテナ', () => {
}, },
{ {
label: 'ブロックしているユーザーのノートは含む', label: 'ブロックしているユーザーのノートは含む',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: 'ブロックされているユーザーのノートは含まない', label: 'ブロックされているユーザーのノートは含まない',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) }, { note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
], ],
}, },
{ {
label: 'ミュートしているユーザーのノートは含まない', label: 'ミュートしているユーザーのノートは含まない',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) }, { note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
], ],
}, },
{ {
label: 'ミュートされているユーザーのノートは含む', label: 'ミュートされているユーザーのノートは含む',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: '「見つけやすくする」がOFFのユーザーのートも含まれる', label: '「見つけやすくする」がOFFのユーザーのートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: '鍵付きユーザーのノートも含まれる', label: '鍵付きユーザーのノートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: 'サイレンスのノートも含まれる', label: 'サイレンスのノートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
], ],
}, },
{ {
label: '削除ユーザーのノートも含まれる', label: '削除ユーザーのノートも含まれる',
parameters: (): object => ({}), parameters: () => ({}),
posts: [ posts: [
{ note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
@ -424,7 +420,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'ユーザー指定で', label: 'ユーザー指定で',
parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }), parameters: () => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -433,7 +429,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'リスト指定で', label: 'リスト指定で',
parameters: (): object => ({ src: 'list', userListId: aliceList.id }), parameters: () => ({ src: 'list', userListId: aliceList.id }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) }, { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -442,14 +438,14 @@ describe('アンテナ', () => {
}, },
{ {
label: 'CWにもマッチする', label: 'CWにもマッチする',
parameters: (): object => ({ keywords: [[keyword]] }), parameters: () => ({ keywords: [[keyword]] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
], ],
}, },
{ {
label: 'キーワード1つ', label: 'キーワード1つ',
parameters: (): object => ({ keywords: [[keyword]] }), parameters: () => ({ keywords: [[keyword]] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(alice, { text: 'test' }) }, { note: (): Promise<Note> => post(alice, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
@ -458,7 +454,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード3つ(AND)', label: 'キーワード3つ(AND)',
parameters: (): object => ({ keywords: [['A', 'B', 'C']] }), parameters: () => ({ keywords: [['A', 'B', 'C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test A' }) }, { note: (): Promise<Note> => post(bob, { text: 'test A' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A B' }) }, { note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
@ -469,7 +465,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード3つ(OR)', label: 'キーワード3つ(OR)',
parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }), parameters: () => ({ keywords: [['A'], ['B'], ['C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'test' }) }, { note: (): Promise<Note> => post(bob, { text: 'test' }) },
{ note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
@ -482,7 +478,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード3つ(AND)', label: '除外ワード3つ(AND)',
parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }), parameters: () => ({ excludeKeywords: [['A', 'B', 'C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
@ -495,7 +491,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード3つ(OR)', label: '除外ワード3つ(OR)',
parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }), parameters: () => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) }, { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
@ -508,7 +504,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード1つ(大文字小文字区別する)', label: 'キーワード1つ(大文字小文字区別する)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }), parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }) }, { note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) }, { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
@ -517,7 +513,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'キーワード1つ(大文字小文字区別しない)', label: 'キーワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }), parameters: () => ({ keywords: [['KEYWORD']], caseSensitive: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
{ note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true }, { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
@ -526,7 +522,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード1つ(大文字小文字区別する)', label: '除外ワード1つ(大文字小文字区別する)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }), parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
@ -536,7 +532,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '除外ワード1つ(大文字小文字区別しない)', label: '除外ワード1つ(大文字小文字区別しない)',
parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }), parameters: () => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) }, { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
@ -546,7 +542,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '添付ファイルを問わない', label: '添付ファイルを問わない',
parameters: (): object => ({ withFile: false }), parameters: () => ({ withFile: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -554,7 +550,7 @@ describe('アンテナ', () => {
}, },
{ {
label: '添付ファイル付きのみ', label: '添付ファイル付きのみ',
parameters: (): object => ({ withFile: true }), parameters: () => ({ withFile: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }) }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
@ -562,7 +558,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'リプライ以外', label: 'リプライ以外',
parameters: (): object => ({ withReplies: false }), parameters: () => ({ withReplies: false }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -570,7 +566,7 @@ describe('アンテナ', () => {
}, },
{ {
label: 'リプライも含む', label: 'リプライも含む',
parameters: (): object => ({ withReplies: true }), parameters: () => ({ withReplies: true }),
posts: [ posts: [
{ note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true }, { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
@ -633,7 +629,7 @@ describe('アンテナ', () => {
endpoint: 'antennas/notes', endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id, ...paginationParam }, parameters: { antennaId: antenna.id, ...paginationParam },
user: alice, user: alice,
}) as any as Note[]; });
}, offsetBy, 'desc'); }, offsetBy, 'desc');
}); });

View file

@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { api, post, signup } from '../utils.js'; import { UserToken, api, post, signup } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('API visibility', () => { describe('API visibility', () => {
@ -24,38 +24,38 @@ describe('API visibility', () => {
let target2: misskey.entities.SignupResponse; let target2: misskey.entities.SignupResponse;
/** public-post */ /** public-post */
let pub: any; let pub: misskey.entities.Note;
/** home-post */ /** home-post */
let home: any; let home: misskey.entities.Note;
/** followers-post */ /** followers-post */
let fol: any; let fol: misskey.entities.Note;
/** specified-post */ /** specified-post */
let spe: any; let spe: misskey.entities.Note;
/** public-reply to target's post */ /** public-reply to target's post */
let pubR: any; let pubR: misskey.entities.Note;
/** home-reply to target's post */ /** home-reply to target's post */
let homeR: any; let homeR: misskey.entities.Note;
/** followers-reply to target's post */ /** followers-reply to target's post */
let folR: any; let folR: misskey.entities.Note;
/** specified-reply to target's post */ /** specified-reply to target's post */
let speR: any; let speR: misskey.entities.Note;
/** public-mention to target */ /** public-mention to target */
let pubM: any; let pubM: misskey.entities.Note;
/** home-mention to target */ /** home-mention to target */
let homeM: any; let homeM: misskey.entities.Note;
/** followers-mention to target */ /** followers-mention to target */
let folM: any; let folM: misskey.entities.Note;
/** specified-mention to target */ /** specified-mention to target */
let speM: any; let speM: misskey.entities.Note;
/** reply target post */ /** reply target post */
let tgt: any; let tgt: misskey.entities.Note;
//#endregion //#endregion
const show = async (noteId: any, by: any) => { const show = async (noteId: misskey.entities.Note['id'], by?: UserToken) => {
return await api('/notes/show', { return await api('notes/show', {
noteId, noteId,
}, by); }, by);
}; };
@ -70,7 +70,7 @@ describe('API visibility', () => {
target2 = await signup({ username: 'target2' }); target2 = await signup({ username: 'target2' });
// follow alice <= follower // follow alice <= follower
await api('/following/create', { userId: alice.id }, follower); await api('following/create', { userId: alice.id }, follower);
// normal posts // normal posts
pub = await post(alice, { text: 'x', visibility: 'public' }); pub = await post(alice, { text: 'x', visibility: 'public' });
@ -111,7 +111,7 @@ describe('API visibility', () => {
}); });
test('[show] public-postを未認証が見れる', async () => { test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null); const res = await show(pub.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -132,7 +132,7 @@ describe('API visibility', () => {
}); });
test('[show] home-postを未認証が見れる', async () => { test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null); const res = await show(home.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -153,7 +153,7 @@ describe('API visibility', () => {
}); });
test('[show] followers-postを未認証が見れない', async () => { test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null); const res = await show(fol.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@ -179,7 +179,7 @@ describe('API visibility', () => {
}); });
test('[show] specified-postを未認証が見れない', async () => { test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null); const res = await show(spe.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
@ -207,7 +207,7 @@ describe('API visibility', () => {
}); });
test('[show] public-replyを未認証が見れる', async () => { test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null); const res = await show(pubR.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -233,7 +233,7 @@ describe('API visibility', () => {
}); });
test('[show] home-replyを未認証が見れる', async () => { test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null); const res = await show(homeR.id);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
@ -259,7 +259,7 @@ describe('API visibility', () => {
}); });
test('[show] followers-replyを未認証が見れない', async () => { test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null); const res = await show(folR.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@ -290,7 +290,7 @@ describe('API visibility', () => {
}); });
test('[show] specified-replyを未認証が見れない', async () => { test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null); const res = await show(speR.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
@ -318,7 +318,7 @@ describe('API visibility', () => {
}); });
test('[show] public-mentionを未認証が見れる', async () => { test('[show] public-mentionを未認証が見れる', async () => {
const res = await show(pubM.id, null); const res = await show(pubM.id);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
@ -344,7 +344,7 @@ describe('API visibility', () => {
}); });
test('[show] home-mentionを未認証が見れる', async () => { test('[show] home-mentionを未認証が見れる', async () => {
const res = await show(homeM.id, null); const res = await show(homeM.id);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
@ -370,7 +370,7 @@ describe('API visibility', () => {
}); });
test('[show] followers-mentionを未認証が見れない', async () => { test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null); const res = await show(folM.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@ -401,28 +401,28 @@ describe('API visibility', () => {
}); });
test('[show] specified-mentionを未認証が見れない', async () => { test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null); const res = await show(speM.id);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
//#region HTL //#region HTL
test('[HTL] public-post が 自分が見れる', async () => { test('[HTL] public-post が 自分が見れる', async () => {
const res = await api('/notes/timeline', { limit: 100 }, alice); const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[HTL] public-post が 非フォロワーから見れない', async () => { test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await api('/notes/timeline', { limit: 100 }, other); const res = await api('notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[HTL] followers-post が フォロワーから見れる', async () => { test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await api('/notes/timeline', { limit: 100 }, follower); const res = await api('notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id); const notes = res.body.filter((n: any) => n.id === fol.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
@ -431,21 +431,21 @@ describe('API visibility', () => {
//#region RTL //#region RTL
test('[replies] followers-reply が フォロワーから見れる', async () => { test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target); const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
@ -454,14 +454,14 @@ describe('API visibility', () => {
//#region MTL //#region MTL
test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await api('/notes/mentions', { limit: 100 }, target); const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await api('/notes/mentions', { limit: 100 }, target); const res = await api('notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id); const notes = res.body.filter((n: any) => n.id === folM.id);
assert.strictEqual(notes[0].text, '@target x'); assert.strictEqual(notes[0].text, '@target x');

View file

@ -23,32 +23,32 @@ import type * as misskey from 'misskey-js';
describe('API', () => { describe('API', () => {
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
describe('General validation', () => { describe('General validation', () => {
test('wrong type', async () => { test('wrong type', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
// @ts-expect-error string must be string
string: 42, string: 42,
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('missing require param', async () => { test('missing require param', async () => {
const res = await api('/test', { // @ts-expect-error required is required
const res = await api('test', {
string: 'a', string: 'a',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('invalid misskey:id (empty string)', async () => { test('invalid misskey:id (empty string)', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
id: '', id: '',
}); });
@ -56,7 +56,7 @@ describe('API', () => {
}); });
test('valid misskey:id', async () => { test('valid misskey:id', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
id: '8wvhjghbxu', id: '8wvhjghbxu',
}); });
@ -64,7 +64,7 @@ describe('API', () => {
}); });
test('default value', async () => { test('default value', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
string: 'a', string: 'a',
}); });
@ -73,7 +73,7 @@ describe('API', () => {
}); });
test('can set null even if it has default value', async () => { test('can set null even if it has default value', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
nullableDefault: null, nullableDefault: null,
}); });
@ -82,7 +82,7 @@ describe('API', () => {
}); });
test('cannot set undefined if it has default value', async () => { test('cannot set undefined if it has default value', async () => {
const res = await api('/test', { const res = await api('test', {
required: true, required: true,
nullableDefault: undefined, nullableDefault: undefined,
}); });
@ -99,14 +99,14 @@ describe('API', () => {
// aliceは管理者、APIを使える // aliceは管理者、APIを使える
await successfulApiCall({ await successfulApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: alice, user: alice,
}); });
// bobは一般ユーザーだからダメ // bobは一般ユーザーだからダメ
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: bob, user: bob,
}, { }, {
@ -117,7 +117,7 @@ describe('API', () => {
// publicアクセスももちろんダメ // publicアクセスももちろんダメ
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: undefined, user: undefined,
}, { }, {
@ -128,7 +128,7 @@ describe('API', () => {
// ごまがしもダメ // ごまがしもダメ
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: 'tsukawasete' }, user: { token: 'tsukawasete' },
}, { }, {
@ -138,13 +138,13 @@ describe('API', () => {
}); });
await successfulApiCall({ await successfulApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application2 }, user: { token: application2 },
}); });
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application }, user: { token: application },
}, { }, {
@ -154,7 +154,7 @@ describe('API', () => {
}); });
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application3 }, user: { token: application3 },
}, { }, {
@ -164,7 +164,7 @@ describe('API', () => {
}); });
await failedApiCall({ await failedApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { token: application4 }, user: { token: application4 },
}, { }, {
@ -177,7 +177,7 @@ describe('API', () => {
describe('Authentication header', () => { describe('Authentication header', () => {
test('一般リクエスト', async () => { test('一般リクエスト', async () => {
await successfulApiCall({ await successfulApiCall({
endpoint: '/admin/get-index-stats', endpoint: 'admin/get-index-stats',
parameters: {}, parameters: {},
user: { user: {
token: alice.token, token: alice.token,
@ -211,7 +211,7 @@ describe('API', () => {
describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { describe('tokenエラー応答でWWW-Authenticate headerを送る', () => {
describe('invalid_token', () => { describe('invalid_token', () => {
test('一般リクエスト', async () => { test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {}, { const result = await api('admin/get-index-stats', {}, {
token: 'syuilo', token: 'syuilo',
bearer: true, bearer: true,
}); });
@ -246,7 +246,7 @@ describe('API', () => {
describe('tokenがないとrealmだけおくる', () => { describe('tokenがないとrealmだけおくる', () => {
test('一般リクエスト', async () => { test('一般リクエスト', async () => {
const result = await api('/admin/get-index-stats', {}); const result = await api('admin/get-index-stats', {});
assert.strictEqual(result.status, 401); assert.strictEqual(result.status, 401);
assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"');
}); });
@ -259,7 +259,8 @@ describe('API', () => {
}); });
test('invalid_request', async () => { test('invalid_request', async () => {
const result = await api('/notes/create', { text: true }, { // @ts-expect-error text must be string
const result = await api('notes/create', { text: true }, {
token: alice.token, token: alice.token,
bearer: true, bearer: true,
}); });

View file

@ -22,7 +22,7 @@ describe('Block', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('Block作成', async () => { test('Block作成', async () => {
const res = await api('/blocking/create', { const res = await api('blocking/create', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@ -30,7 +30,7 @@ describe('Block', () => {
}); });
test('ブロックされているユーザーをフォローできない', async () => { test('ブロックされているユーザーをフォローできない', async () => {
const res = await api('/following/create', { userId: alice.id }, bob); const res = await api('following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
@ -39,7 +39,7 @@ describe('Block', () => {
test('ブロックされているユーザーにリアクションできない', async () => { test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); const res = await api('notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
@ -48,7 +48,7 @@ describe('Block', () => {
test('ブロックされているユーザーに返信できない', async () => { test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob); const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -57,7 +57,7 @@ describe('Block', () => {
test('ブロックされているユーザーのートをRenoteできない', async () => { test('ブロックされているユーザーのートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob); const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
@ -72,12 +72,13 @@ describe('Block', () => {
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, bob); const res = await api('notes/local-timeline', {}, bob);
const body = res.body as misskey.entities.Note[];
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); assert.strictEqual(body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); assert.strictEqual(body.some(note => note.id === carolNote.id), true);
}); });
}); });

View file

@ -6,47 +6,34 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { JTDDataType } from 'ajv/dist/jtd';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { Packed } from '@/misc/json-schema.js';
import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js';
import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js';
import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js';
import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js';
import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js';
import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js';
import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
import type * as Misskey from 'misskey-js';
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
describe('クリップ', () => { describe('クリップ', () => {
type User = Packed<'User'>; let alice: Misskey.entities.SignupResponse;
type Note = Packed<'Note'>; let bob: Misskey.entities.SignupResponse;
type Clip = Packed<'Clip'>; let aliceNote: Misskey.entities.Note;
let aliceHomeNote: Misskey.entities.Note;
let alice: User; let aliceFollowersNote: Misskey.entities.Note;
let bob: User; let aliceSpecifiedNote: Misskey.entities.Note;
let aliceNote: Note; let bobNote: Misskey.entities.Note;
let aliceHomeNote: Note; let bobHomeNote: Misskey.entities.Note;
let aliceFollowersNote: Note; let bobFollowersNote: Misskey.entities.Note;
let aliceSpecifiedNote: Note; let bobSpecifiedNote: Misskey.entities.Note;
let bobNote: Note;
let bobHomeNote: Note;
let bobFollowersNote: Note;
let bobSpecifiedNote: Note;
const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { const compareBy = <T extends { id: string }, >(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
return selector(a).localeCompare(selector(b)); return selector(a).localeCompare(selector(b));
}; };
type CreateParam = JTDDataType<typeof CreateParamDef>; const defaultCreate = (): Pick<Misskey.entities.ClipsCreateRequest, 'name'> => ({
const defaultCreate = (): Partial<CreateParam> => ({
name: 'test', name: 'test',
}); });
const create = async (parameters: Partial<CreateParam> = {}, request: Partial<ApiRequest> = {}): Promise<Clip> => { const create = async (parameters: Partial<Misskey.entities.ClipsCreateRequest> = {}, request: Partial<ApiRequest<'clips/create'>> = {}): Promise<Misskey.entities.Clip> => {
const clip = await successfulApiCall<Clip>({ const clip = await successfulApiCall({
endpoint: '/clips/create', endpoint: 'clips/create',
parameters: { parameters: {
...defaultCreate(), ...defaultCreate(),
...parameters, ...parameters,
@ -64,17 +51,16 @@ describe('クリップ', () => {
return clip; return clip;
}; };
const createMany = async (parameters: Partial<CreateParam>, count = 10, user = alice): Promise<Clip[]> => { const createMany = async (parameters: Partial<Misskey.entities.ClipsCreateRequest>, count = 10, user = alice): Promise<Misskey.entities.Clip[]> => {
return await Promise.all([...Array(count)].map((_, i) => create({ return await Promise.all([...Array(count)].map((_, i) => create({
name: `test${i}`, name: `test${i}`,
...parameters, ...parameters,
}, { user }))); }, { user })));
}; };
type UpdateParam = JTDDataType<typeof UpdateParamDef>; const update = async (parameters: Optional<Misskey.entities.ClipsUpdateRequest, 'name'>, request: Partial<ApiRequest<'clips/update'>> = {}): Promise<Misskey.entities.Clip> => {
const update = async (parameters: Partial<UpdateParam>, request: Partial<ApiRequest> = {}): Promise<Clip> => { const clip = await successfulApiCall({
const clip = await successfulApiCall<Clip>({ endpoint: 'clips/update',
endpoint: '/clips/update',
parameters: { parameters: {
name: 'updated', name: 'updated',
...parameters, ...parameters,
@ -92,41 +78,39 @@ describe('クリップ', () => {
return clip; return clip;
}; };
type DeleteParam = JTDDataType<typeof DeleteParamDef>; const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => {
const deleteClip = async (parameters: DeleteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return await successfulApiCall({
return await successfulApiCall<void>({ endpoint: 'clips/delete',
endpoint: '/clips/delete',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type ShowParam = JTDDataType<typeof ShowParamDef>; const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => {
const show = async (parameters: ShowParam, request: Partial<ApiRequest> = {}): Promise<Clip> => { return await successfulApiCall({
return await successfulApiCall<Clip>({ endpoint: 'clips/show',
endpoint: '/clips/show',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}); });
}; };
const list = async (request: Partial<ApiRequest>): Promise<Clip[]> => { const list = async (request: Partial<ApiRequest<'clips/list'>>): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall<Clip[]>({ return successfulApiCall({
endpoint: '/clips/list', endpoint: 'clips/list',
parameters: {}, parameters: {},
user: alice, user: alice,
...request, ...request,
}); });
}; };
const usersClips = async (request: Partial<ApiRequest>): Promise<Clip[]> => { const usersClips = async (parameters: Misskey.entities.UsersClipsRequest, request: Partial<ApiRequest<'users/clips'>> = {}): Promise<Misskey.entities.Clip[]> => {
return await successfulApiCall<Clip[]>({ return await successfulApiCall({
endpoint: '/users/clips', endpoint: 'users/clips',
parameters: {}, parameters,
user: alice, user: alice,
...request, ...request,
}); });
@ -136,23 +120,22 @@ describe('クリップ', () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
// FIXME: misskey-jsのNoteはoutdatedなので直接変換できない aliceNote = await post(alice, { text: 'test' });
aliceNote = await post(alice, { text: 'test' }) as any; aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' });
aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' });
aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' });
aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; bobNote = await post(bob, { text: 'test' });
bobNote = await post(bob, { text: 'test' }) as any; bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' });
bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' });
bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' });
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterEach(async () => { afterEach(async () => {
// テスト間で影響し合わないように毎回全部消す。 // テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) { for (const user of [alice, bob]) {
const list = await api('/clips/list', { limit: 11 }, user); const list = await api('clips/list', { limit: 11 }, user);
for (const clip of list.body) { for (const clip of list.body) {
await api('/clips/delete', { clipId: clip.id }, user); await api('clips/delete', { clipId: clip.id }, user);
} }
} }
}); });
@ -177,7 +160,7 @@ describe('クリップ', () => {
} }
await failedApiCall({ await failedApiCall({
endpoint: '/clips/create', endpoint: 'clips/create',
parameters: defaultCreate(), parameters: defaultCreate(),
user: alice, user: alice,
}, { }, {
@ -204,7 +187,8 @@ describe('クリップ', () => {
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } }, { label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
]; ];
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
endpoint: '/clips/create', endpoint: 'clips/create',
// @ts-expect-error invalid params
parameters: { parameters: {
...defaultCreate(), ...defaultCreate(),
...parameters, ...parameters,
@ -246,15 +230,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
} }, } },
...createClipDenyPattern as any, ...createClipDenyPattern as any,
])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/update', endpoint: 'clips/update',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
name: 'updated', name: 'updated',
...parameters, ...parameters,
}, },
@ -279,14 +263,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754', id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754', id: '70ca08ba-6865-4630-b6fb-8494759aa754',
} }, } },
])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/delete', endpoint: 'clips/delete',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, // @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters, ...parameters,
}, },
user: alice, user: alice,
@ -306,7 +291,7 @@ describe('クリップ', () => {
test('のID指定取得は他人のPrivateなクリップは取得できない', async () => { test('のID指定取得は他人のPrivateなクリップは取得できない', async () => {
const clip = await create({ isPublic: false }, { user: bob } ); const clip = await create({ isPublic: false }, { user: bob } );
failedApiCall({ failedApiCall({
endpoint: '/clips/show', endpoint: 'clips/show',
parameters: { clipId: clip.id }, parameters: { clipId: clip.id },
user: alice, user: alice,
}, { }, {
@ -323,7 +308,8 @@ describe('クリップ', () => {
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
} }, } },
])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({
endpoint: '/clips/show', endpoint: 'clips/show',
// @ts-expect-error clipId must not be undefined
parameters: { parameters: {
...parameters, ...parameters,
}, },
@ -356,27 +342,23 @@ describe('クリップ', () => {
test('の一覧が取得できる(空)', async () => { test('の一覧が取得できる(空)', async () => {
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
},
}); });
assert.deepStrictEqual(res, []); assert.deepStrictEqual(res, []);
}); });
test.each([ test.each([
{ label: '' }, { label: '' },
{ label: '他人アカウントから', user: (): User => bob }, { label: '他人アカウントから', user: () => bob },
])('の一覧が$label取得できる', async () => { ])('の一覧が$label取得できる', async () => {
const clips = await createMany({ isPublic: true }); const clips = await createMany({ isPublic: true });
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
},
}); });
// 返ってくる配列には順序保障がないのでidでソートして厳密比較 // 返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual( assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)), res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
clips.sort(compareBy(s => s.id))); clips.sort(compareBy(s => s.id)));
// 認証状態で見たときだけisFavoritedが入っている // 認証状態で見たときだけisFavoritedが入っている
@ -386,17 +368,16 @@ describe('クリップ', () => {
}); });
test.each([ test.each([
{ label: '未認証', user: (): undefined => undefined }, { label: '未認証', user: () => undefined },
{ label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } }, { label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } },
])('の一覧は$labelでも取得できる', async ({ parameters, user }) => { ])('の一覧は$labelでも取得できる', async ({ parameters, user }) => {
const clips = await createMany({ isPublic: true }); const clips = await createMany({ isPublic: true });
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
limit: clips.length, limit: clips.length,
...parameters, ...parameters,
}, }, {
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}); });
// 未認証で見たときはisFavoritedは入らない // 未認証で見たときはisFavoritedは入らない
@ -409,10 +390,8 @@ describe('クリップ', () => {
await create({ isPublic: false }); await create({ isPublic: false });
const aliceClip = await create({ isPublic: true }); const aliceClip = await create({ isPublic: true });
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
limit: 2, limit: 2,
},
}); });
assert.deepStrictEqual(res, [aliceClip]); assert.deepStrictEqual(res, [aliceClip]);
}); });
@ -421,17 +400,15 @@ describe('クリップ', () => {
const clips = await createMany({ isPublic: true }, 7); const clips = await createMany({ isPublic: true }, 7);
clips.sort(compareBy(s => s.id)); clips.sort(compareBy(s => s.id));
const res = await usersClips({ const res = await usersClips({
parameters: {
userId: alice.id, userId: alice.id,
sinceId: clips[1].id, sinceId: clips[1].id,
untilId: clips[5].id, untilId: clips[5].id,
limit: 4, limit: 4,
},
}); });
// Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較
assert.deepStrictEqual( assert.deepStrictEqual(
res.sort(compareBy<Clip>(s => s.id)), res.sort(compareBy<Misskey.entities.Clip>(s => s.id)),
[clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない
clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id));
}); });
@ -441,8 +418,9 @@ describe('クリップ', () => {
{ label: 'limitゼロ', parameters: { limit: 0 } }, { label: 'limitゼロ', parameters: { limit: 0 } },
{ label: 'limit最大+1', parameters: { limit: 101 } }, { label: 'limit最大+1', parameters: { limit: 101 } },
])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({
endpoint: '/users/clips', endpoint: 'users/clips',
parameters: { parameters: {
// @ts-expect-error userId must not be undefined
userId: alice.id, userId: alice.id,
...parameters, ...parameters,
}, },
@ -454,15 +432,15 @@ describe('クリップ', () => {
})); }));
test.each([ test.each([
{ label: '作成', endpoint: '/clips/create' }, { label: '作成', endpoint: 'clips/create' as const },
{ label: '更新', endpoint: '/clips/update' }, { label: '更新', endpoint: 'clips/update' as const },
{ label: '削除', endpoint: '/clips/delete' }, { label: '削除', endpoint: 'clips/delete' as const },
{ label: '取得', endpoint: '/clips/list' }, { label: '取得', endpoint: 'clips/list' as const },
{ label: 'お気に入り設定', endpoint: '/clips/favorite' }, { label: 'お気に入り設定', endpoint: 'clips/favorite' as const },
{ label: 'お気に入り解除', endpoint: '/clips/unfavorite' }, { label: 'お気に入り解除', endpoint: 'clips/unfavorite' as const },
{ label: 'お気に入り取得', endpoint: '/clips/my-favorites' }, { label: 'お気に入り取得', endpoint: 'clips/my-favorites' as const },
{ label: 'ノート追加', endpoint: '/clips/add-note' }, { label: 'ノート追加', endpoint: 'clips/add-note' as const },
{ label: 'ノート削除', endpoint: '/clips/remove-note' }, { label: 'ノート削除', endpoint: 'clips/remove-note' as const },
])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({ ])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({
endpoint: endpoint, endpoint: endpoint,
parameters: {}, parameters: {},
@ -474,35 +452,33 @@ describe('クリップ', () => {
})); }));
describe('のお気に入り', () => { describe('のお気に入り', () => {
let aliceClip: Clip; let aliceClip: Misskey.entities.Clip;
type FavoriteParam = JTDDataType<typeof FavoriteParamDef>; const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => {
const favorite = async (parameters: FavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/favorite',
endpoint: '/clips/favorite',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type UnfavoriteParam = JTDDataType<typeof UnfavoriteParamDef>; const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => {
const unfavorite = async (parameters: UnfavoriteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/unfavorite',
endpoint: '/clips/unfavorite',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
const myFavorites = async (request: Partial<ApiRequest> = {}): Promise<Clip[]> => { const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => {
return successfulApiCall<Clip[]>({ return successfulApiCall({
endpoint: '/clips/my-favorites', endpoint: 'clips/my-favorites',
parameters: {}, parameters: {},
user: alice, user: alice,
...request, ...request,
@ -568,7 +544,7 @@ describe('クリップ', () => {
test('は同じクリップに対して二回設定できない。', async () => { test('は同じクリップに対して二回設定できない。', async () => {
await favorite({ clipId: aliceClip.id }); await favorite({ clipId: aliceClip.id });
await failedApiCall({ await failedApiCall({
endpoint: '/clips/favorite', endpoint: 'clips/favorite',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
}, },
@ -586,14 +562,15 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
} }, } },
])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/favorite', endpoint: 'clips/favorite',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, // @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters, ...parameters,
}, },
user: alice, user: alice,
@ -619,7 +596,7 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '2603966e-b865-426c-94a7-af4a01241dc1', id: '2603966e-b865-426c-94a7-af4a01241dc1',
} }, } },
{ label: '他人のクリップ', user: (): User => bob, assertion: { { label: '他人のクリップ', user: () => bob, assertion: {
code: 'NOT_FAVORITED', code: 'NOT_FAVORITED',
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} }, } },
@ -628,9 +605,10 @@ describe('クリップ', () => {
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
} }, } },
])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/unfavorite', endpoint: 'clips/unfavorite',
parameters: { parameters: {
clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, // @ts-expect-error clipId must not be null
clipId: (await create({}, { user: (user ?? (() => alice))() })).id,
...parameters, ...parameters,
}, },
user: alice, user: alice,
@ -655,41 +633,38 @@ describe('クリップ', () => {
}); });
describe('に紐づくノート', () => { describe('に紐づくノート', () => {
let aliceClip: Clip; let aliceClip: Misskey.entities.Clip;
const sampleNotes = (): Note[] => [ const sampleNotes = (): Misskey.entities.Note[] => [
aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote,
bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote, bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote,
]; ];
type AddNoteParam = JTDDataType<typeof AddNoteParamDef>; const addNote = async (parameters: Misskey.entities.ClipsAddNoteRequest, request: Partial<ApiRequest<'clips/add-note'>> = {}): Promise<void> => {
const addNote = async (parameters: AddNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/add-note',
endpoint: '/clips/add-note',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type RemoveNoteParam = JTDDataType<typeof RemoveNoteParamDef>; const removeNote = async (parameters: Misskey.entities.ClipsRemoveNoteRequest, request: Partial<ApiRequest<'clips/remove-note'>> = {}): Promise<void> => {
const removeNote = async (parameters: RemoveNoteParam, request: Partial<ApiRequest> = {}): Promise<void> => { return successfulApiCall({
return successfulApiCall<void>({ endpoint: 'clips/remove-note',
endpoint: '/clips/remove-note',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
}, { }, {
status: 204, status: 204,
}); }) as any as void;
}; };
type NotesParam = JTDDataType<typeof NotesParamDef>; const notes = async (parameters: Misskey.entities.ClipsNotesRequest, request: Partial<ApiRequest<'clips/notes'>> = {}): Promise<Misskey.entities.Note[]> => {
const notes = async (parameters: Partial<NotesParam>, request: Partial<ApiRequest> = {}): Promise<Note[]> => { return successfulApiCall({
return successfulApiCall<Note[]>({ endpoint: 'clips/notes',
endpoint: '/clips/notes',
parameters, parameters,
user: alice, user: alice,
...request, ...request,
@ -715,7 +690,7 @@ describe('クリップ', () => {
test('として同じノートを二回紐づけることはできない', async () => { test('として同じノートを二回紐づけることはできない', async () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
await failedApiCall({ await failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
noteId: aliceNote.id, noteId: aliceNote.id,
@ -733,11 +708,11 @@ describe('クリップ', () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`, text: `test ${i}`,
}) as unknown)) as Note[]; }) as unknown)) as Misskey.entities.Note[];
await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id })));
await failedApiCall({ await failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
noteId: aliceNote.id, noteId: aliceNote.id,
@ -751,7 +726,7 @@ describe('クリップ', () => {
}); });
test('は他人のクリップへ追加できない。', async () => await failedApiCall({ test('は他人のクリップへ追加できない。', async () => await failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
clipId: aliceClip.id, clipId: aliceClip.id,
noteId: aliceNote.id, noteId: aliceNote.id,
@ -774,18 +749,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
} }, } },
{ label: '他人のクリップ', user: (): object => bob, assetion: { { label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
} }, } },
])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/add-note', endpoint: 'clips/add-note',
parameters: { parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id, clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id, noteId: aliceNote.id,
...parameters, ...parameters,
}, },
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}, { }, {
status: 400, status: 400,
code: 'INVALID_PARAM', code: 'INVALID_PARAM',
@ -810,18 +787,20 @@ describe('クリップ', () => {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる
} }, } },
{ label: '他人のクリップ', user: (): object => bob, assetion: { { label: '他人のクリップ', user: () => bob, assetion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる
} }, } },
])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({
endpoint: '/clips/remove-note', endpoint: 'clips/remove-note',
parameters: { parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id, clipId: aliceClip.id,
// @ts-expect-error noteId must not be undefined
noteId: aliceNote.id, noteId: aliceNote.id,
...parameters, ...parameters,
}, },
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}, { }, {
status: 400, status: 400,
code: 'INVALID_PARAM', code: 'INVALID_PARAM',
@ -925,21 +904,22 @@ describe('クリップ', () => {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} }, } },
{ label: '他人のPrivateなクリップから', user: (): object => bob, assertion: { { label: '他人のPrivateなクリップから', user: () => bob, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} }, } },
{ label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: { { label: '未認証でPrivateなクリップから', user: () => undefined, assertion: {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
} }, } },
])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({
endpoint: '/clips/notes', endpoint: 'clips/notes',
parameters: { parameters: {
// @ts-expect-error clipId must not be undefined
clipId: aliceClip.id, clipId: aliceClip.id,
...parameters, ...parameters,
}, },
user: (user ?? ((): User => alice))(), user: (user ?? (() => alice))(),
}, { }, {
status: 400, status: 400,
code: 'INVALID_PARAM', code: 'INVALID_PARAM',

View file

@ -6,22 +6,14 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { MiNote } from '@/models/Note.js'; import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import type{ Repository } from 'typeorm'
import type { Packed } from '@/misc/json-schema.js';
describe('Drive', () => { describe('Drive', () => {
let Notes: Repository<MiNote>;
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
@ -31,13 +23,13 @@ describe('Drive', () => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg' const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg';
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
alice, alice,
'main', 'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>, (msg) => msg.body.file,
10 * 1000); 10 * 1000);
const res = await api('drive/files/upload-from-url', { const res = await api('drive/files/upload-from-url', {
@ -51,7 +43,7 @@ describe('Drive', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg'); assert.strictEqual(file.name, 'Lenna.jpg');
assert.strictEqual(file.type, 'image/jpeg'); assert.strictEqual(file.type, 'image/jpeg');
}) });
test('ローカルからアップロードできる', async () => { test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
@ -59,27 +51,27 @@ describe('Drive', () => {
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg'); assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body?.type, 'image/jpeg'); assert.strictEqual(res.body.type, 'image/jpeg');
}) });
test('添付ノート一覧を取得できる', async () => { test('添付ノート一覧を取得できる', async () => {
const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id) const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id);
const note0 = await post(alice, { fileIds: [ids[0]] }); const note0 = await post(alice, { fileIds: [ids[0]] });
const note1 = await post(alice, { fileIds: [ids[0], ids[1]] }); const note1 = await post(alice, { fileIds: [ids[0], ids[1]] });
const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice); const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice);
assert.strictEqual(attached0.body.length, 2); assert.strictEqual(attached0.body.length, 2);
assert.strictEqual(attached0.body[0].id, note1.id) assert.strictEqual(attached0.body[0].id, note1.id);
assert.strictEqual(attached0.body[1].id, note0.id) assert.strictEqual(attached0.body[1].id, note0.id);
const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice); const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice);
assert.strictEqual(attached1.body.length, 1); assert.strictEqual(attached1.body.length, 1);
assert.strictEqual(attached1.body[0].id, note1.id) assert.strictEqual(attached1.body[0].id, note1.id);
const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice); const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice);
assert.strictEqual(attached2.body.length, 0) assert.strictEqual(attached2.body.length, 0);
}) });
test('添付ノート一覧は他の人から見えない', async () => { test('添付ノート一覧は他の人から見えない', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
@ -89,7 +81,5 @@ describe('Drive', () => {
const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob); const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual('error' in res.body, true); assert.strictEqual('error' in res.body, true);
});
})
}); });

View file

@ -79,6 +79,7 @@ describe('Endpoints', () => {
test('クエリをインジェクションできない', async () => { test('クエリをインジェクションできない', async () => {
const res = await api('signin', { const res = await api('signin', {
username: 'test1', username: 'test1',
// @ts-expect-error password must be string
password: { password: {
$gt: '', $gt: '',
}, },
@ -103,7 +104,7 @@ describe('Endpoints', () => {
const myLocation = '七森中'; const myLocation = '七森中';
const myBirthday = '2000-09-07'; const myBirthday = '2000-09-07';
const res = await api('/i/update', { const res = await api('i/update', {
name: myName, name: myName,
location: myLocation, location: myLocation,
birthday: myBirthday, birthday: myBirthday,
@ -117,7 +118,7 @@ describe('Endpoints', () => {
}); });
test('名前を空白にできる', async () => { test('名前を空白にできる', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
name: ' ', name: ' ',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -125,11 +126,11 @@ describe('Endpoints', () => {
}); });
test('誕生日の設定を削除できる', async () => { test('誕生日の設定を削除できる', async () => {
await api('/i/update', { await api('i/update', {
birthday: '2000-09-07', birthday: '2000-09-07',
}, alice); }, alice);
const res = await api('/i/update', { const res = await api('i/update', {
birthday: null, birthday: null,
}, alice); }, alice);
@ -139,7 +140,7 @@ describe('Endpoints', () => {
}); });
test('不正な誕生日の形式で怒られる', async () => { test('不正な誕生日の形式で怒られる', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
birthday: '2000/09/07', birthday: '2000/09/07',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
@ -148,7 +149,7 @@ describe('Endpoints', () => {
describe('users/show', () => { describe('users/show', () => {
test('ユーザーが取得できる', async () => { test('ユーザーが取得できる', async () => {
const res = await api('/users/show', { const res = await api('users/show', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -158,14 +159,14 @@ describe('Endpoints', () => {
}); });
test('ユーザーが存在しなかったら怒る', async () => { test('ユーザーが存在しなかったら怒る', async () => {
const res = await api('/users/show', { const res = await api('users/show', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}); });
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/users/show', { const res = await api('users/show', {
userId: 'kyoppie', userId: 'kyoppie',
}); });
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
@ -178,7 +179,7 @@ describe('Endpoints', () => {
text: 'test', text: 'test',
}); });
const res = await api('/notes/show', { const res = await api('notes/show', {
noteId: myPost.id, noteId: myPost.id,
}, alice); }, alice);
@ -189,14 +190,14 @@ describe('Endpoints', () => {
}); });
test('投稿が存在しなかったら怒る', async () => { test('投稿が存在しなかったら怒る', async () => {
const res = await api('/notes/show', { const res = await api('notes/show', {
noteId: '000000000000000000000000', noteId: '000000000000000000000000',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/notes/show', { const res = await api('notes/show', {
noteId: 'kyoppie', noteId: 'kyoppie',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
@ -207,14 +208,14 @@ describe('Endpoints', () => {
test('リアクションできる', async () => { test('リアクションできる', async () => {
const bobPost = await post(bob, { text: 'hi' }); const bobPost = await post(bob, { text: 'hi' });
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', { const resNote = await api('notes/show', {
noteId: bobPost.id, noteId: bobPost.id,
}, alice); }, alice);
@ -225,7 +226,7 @@ describe('Endpoints', () => {
test('自分の投稿にもリアクションできる', async () => { test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice, { text: 'hi' }); const myPost = await post(alice, { text: 'hi' });
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: myPost.id, noteId: myPost.id,
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
@ -236,19 +237,19 @@ describe('Endpoints', () => {
test('二重にリアクションすると上書きされる', async () => { test('二重にリアクションすると上書きされる', async () => {
const bobPost = await post(bob, { text: 'hi' }); const bobPost = await post(bob, { text: 'hi' });
await api('/notes/reactions/create', { await api('notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🥰', reaction: '🥰',
}, alice); }, alice);
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: bobPost.id, noteId: bobPost.id,
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const resNote = await api('/notes/show', { const resNote = await api('notes/show', {
noteId: bobPost.id, noteId: bobPost.id,
}, alice); }, alice);
@ -257,7 +258,7 @@ describe('Endpoints', () => {
}); });
test('存在しない投稿にはリアクションできない', async () => { test('存在しない投稿にはリアクションできない', async () => {
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: '000000000000000000000000', noteId: '000000000000000000000000',
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
@ -266,13 +267,14 @@ describe('Endpoints', () => {
}); });
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/notes/reactions/create', {}, alice); // @ts-expect-error param must not be empty
const res = await api('notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/notes/reactions/create', { const res = await api('notes/reactions/create', {
noteId: 'kyoppie', noteId: 'kyoppie',
reaction: '🚀', reaction: '🚀',
}, alice); }, alice);
@ -283,7 +285,7 @@ describe('Endpoints', () => {
describe('following/create', () => { describe('following/create', () => {
test('フォローできる', async () => { test('フォローできる', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -301,7 +303,7 @@ describe('Endpoints', () => {
}); });
test('既にフォローしている場合は怒る', async () => { test('既にフォローしている場合は怒る', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -309,7 +311,7 @@ describe('Endpoints', () => {
}); });
test('存在しないユーザーはフォローできない', async () => { test('存在しないユーザーはフォローできない', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}, alice); }, alice);
@ -317,7 +319,7 @@ describe('Endpoints', () => {
}); });
test('自分自身はフォローできない', async () => { test('自分自身はフォローできない', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -325,13 +327,14 @@ describe('Endpoints', () => {
}); });
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/following/create', {}, alice); // @ts-expect-error params must not be empty
const res = await api('following/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/following/create', { const res = await api('following/create', {
userId: 'foo', userId: 'foo',
}, alice); }, alice);
@ -341,11 +344,11 @@ describe('Endpoints', () => {
describe('following/delete', () => { describe('following/delete', () => {
test('フォロー解除できる', async () => { test('フォロー解除できる', async () => {
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -363,7 +366,7 @@ describe('Endpoints', () => {
}); });
test('フォローしていない場合は怒る', async () => { test('フォローしていない場合は怒る', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -371,7 +374,7 @@ describe('Endpoints', () => {
}); });
test('存在しないユーザーはフォロー解除できない', async () => { test('存在しないユーザーはフォロー解除できない', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}, alice); }, alice);
@ -379,7 +382,7 @@ describe('Endpoints', () => {
}); });
test('自分自身はフォロー解除できない', async () => { test('自分自身はフォロー解除できない', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -387,13 +390,14 @@ describe('Endpoints', () => {
}); });
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/following/delete', {}, alice); // @ts-expect-error params must not be empty
const res = await api('following/delete', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/following/delete', { const res = await api('following/delete', {
userId: 'kyoppie', userId: 'kyoppie',
}, alice); }, alice);
@ -403,20 +407,20 @@ describe('Endpoints', () => {
describe('channels/search', () => { describe('channels/search', () => {
test('空白検索で一覧を取得できる', async () => { test('空白検索で一覧を取得できる', async () => {
await api('/channels/create', { await api('channels/create', {
name: 'aaa', name: 'aaa',
description: 'bbb', description: 'bbb',
}, bob); }, bob);
await api('/channels/create', { await api('channels/create', {
name: 'ccc1', name: 'ccc1',
description: 'ddd1', description: 'ddd1',
}, bob); }, bob);
await api('/channels/create', { await api('channels/create', {
name: 'ccc2', name: 'ccc2',
description: 'ddd2', description: 'ddd2',
}, bob); }, bob);
const res = await api('/channels/search', { const res = await api('channels/search', {
query: '', query: '',
}, bob); }, bob);
@ -425,7 +429,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 3); assert.strictEqual(res.body.length, 3);
}); });
test('名前のみの検索で名前を検索できる', async () => { test('名前のみの検索で名前を検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'aaa', query: 'aaa',
type: 'nameOnly', type: 'nameOnly',
}, bob); }, bob);
@ -436,7 +440,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'aaa'); assert.strictEqual(res.body[0].name, 'aaa');
}); });
test('名前のみの検索で名前を複数検索できる', async () => { test('名前のみの検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ccc', query: 'ccc',
type: 'nameOnly', type: 'nameOnly',
}, bob); }, bob);
@ -446,7 +450,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2); assert.strictEqual(res.body.length, 2);
}); });
test('名前のみの検索で説明は検索できない', async () => { test('名前のみの検索で説明は検索できない', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'bbb', query: 'bbb',
type: 'nameOnly', type: 'nameOnly',
}, bob); }, bob);
@ -456,7 +460,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 0); assert.strictEqual(res.body.length, 0);
}); });
test('名前と説明の検索で名前を検索できる', async () => { test('名前と説明の検索で名前を検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ccc1', query: 'ccc1',
}, bob); }, bob);
@ -466,7 +470,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1'); assert.strictEqual(res.body[0].name, 'ccc1');
}); });
test('名前と説明での検索で説明を検索できる', async () => { test('名前と説明での検索で説明を検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ddd1', query: 'ddd1',
}, bob); }, bob);
@ -476,7 +480,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body[0].name, 'ccc1'); assert.strictEqual(res.body[0].name, 'ccc1');
}); });
test('名前と説明の検索で名前を複数検索できる', async () => { test('名前と説明の検索で名前を複数検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ccc', query: 'ccc',
}, bob); }, bob);
@ -485,7 +489,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.length, 2); assert.strictEqual(res.body.length, 2);
}); });
test('名前と説明での検索で説明を複数検索できる', async () => { test('名前と説明での検索で説明を複数検索できる', async () => {
const res = await api('/channels/search', { const res = await api('channels/search', {
query: 'ddd', query: 'ddd',
}, bob); }, bob);
@ -506,7 +510,7 @@ describe('Endpoints', () => {
await uploadFile(alice, { await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]), blob: new Blob([new Uint8Array(1024)]),
}); });
const res = await api('/drive', {}, alice); const res = await api('drive', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).toHaveProperty('usage', 1792); expect(res.body).toHaveProperty('usage', 1792);
@ -519,7 +523,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.jpg'); assert.strictEqual(res.body!.name, 'Lenna.jpg');
}); });
test('ファイルに名前を付けられる', async () => { test('ファイルに名前を付けられる', async () => {
@ -527,7 +531,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.jpg'); assert.strictEqual(res.body!.name, 'Belmond.jpg');
}); });
test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => { test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => {
@ -535,11 +539,12 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Belmond.png.jpg'); assert.strictEqual(res.body!.name, 'Belmond.png.jpg');
}); });
test('ファイル無しで怒られる', async () => { test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice); // @ts-expect-error params must not be empty
const res = await api('drive/files/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -549,14 +554,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg'); assert.strictEqual(res.body!.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml'); assert.strictEqual(res.body!.type, 'image/svg+xml');
}); });
for (const type of ['webp', 'avif']) { for (const type of ['webp', 'avif']) {
const mediaType = `image/${type}`; const mediaType = `image/${type}`;
const getWebpublicType = async (user: any, fileId: string): Promise<string> => { const getWebpublicType = async (user: misskey.entities.SignupResponse, fileId: string): Promise<string> => {
// drive/files/create does not expose webpublicType directly, so get it by posting it // drive/files/create does not expose webpublicType directly, so get it by posting it
const res = await post(user, { const res = await post(user, {
text: mediaType, text: mediaType,
@ -573,10 +578,10 @@ describe('Endpoints', () => {
const res = await uploadFile(alice, { path }); const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path); assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body.type, mediaType); assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id); const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp'); assert.strictEqual(webpublicType, 'image/webp');
}); });
@ -584,10 +589,10 @@ describe('Endpoints', () => {
const path = `without-alpha.${type}`; const path = `without-alpha.${type}`;
const res = await uploadFile(alice, { path }); const res = await uploadFile(alice, { path });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, path); assert.strictEqual(res.body!.name, path);
assert.strictEqual(res.body.type, mediaType); assert.strictEqual(res.body!.type, mediaType);
const webpublicType = await getWebpublicType(alice, res.body.id); const webpublicType = await getWebpublicType(alice, res.body!.id);
assert.strictEqual(webpublicType, 'image/webp'); assert.strictEqual(webpublicType, 'image/webp');
}); });
} }
@ -598,8 +603,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const newName = 'いちごパスタ.png'; const newName = 'いちごパスタ.png';
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
name: newName, name: newName,
}, alice); }, alice);
@ -611,8 +616,8 @@ describe('Endpoints', () => {
test('他人のファイルは更新できない', async () => { test('他人のファイルは更新できない', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
name: 'いちごパスタ.png', name: 'いちごパスタ.png',
}, bob); }, bob);
@ -621,12 +626,12 @@ describe('Endpoints', () => {
test('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: folder.id, folderId: folder.id,
}, alice); }, alice);
@ -638,17 +643,17 @@ describe('Endpoints', () => {
test('親フォルダを無しにできる', async () => { test('親フォルダを無しにできる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
await api('/drive/files/update', { await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: folder.id, folderId: folder.id,
}, alice); }, alice);
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: null, folderId: null,
}, alice); }, alice);
@ -659,12 +664,12 @@ describe('Endpoints', () => {
test('他人のフォルダには入れられない', async () => { test('他人のフォルダには入れられない', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, bob)).body; }, bob)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: folder.id, folderId: folder.id,
}, alice); }, alice);
@ -674,8 +679,8 @@ describe('Endpoints', () => {
test('存在しないフォルダで怒られる', async () => { test('存在しないフォルダで怒られる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: '000000000000000000000000', folderId: '000000000000000000000000',
}, alice); }, alice);
@ -685,8 +690,8 @@ describe('Endpoints', () => {
test('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
folderId: 'foo', folderId: 'foo',
}, alice); }, alice);
@ -694,7 +699,7 @@ describe('Endpoints', () => {
}); });
test('ファイルが存在しなかったら怒る', async () => { test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: '000000000000000000000000', fileId: '000000000000000000000000',
name: 'いちごパスタ.png', name: 'いちごパスタ.png',
}, alice); }, alice);
@ -706,8 +711,8 @@ describe('Endpoints', () => {
const file = (await uploadFile(alice)).body; const file = (await uploadFile(alice)).body;
const newName = ''; const newName = '';
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: file.id, fileId: file!.id,
name: newName, name: newName,
}, alice); }, alice);
@ -715,7 +720,7 @@ describe('Endpoints', () => {
}); });
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', { const res = await api('drive/files/update', {
fileId: 'kyoppie', fileId: 'kyoppie',
name: 'いちごパスタ.png', name: 'いちごパスタ.png',
}, alice); }, alice);
@ -726,7 +731,7 @@ describe('Endpoints', () => {
describe('drive/folders/create', () => { describe('drive/folders/create', () => {
test('フォルダを作成できる', async () => { test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', { const res = await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice); }, alice);
@ -738,11 +743,11 @@ describe('Endpoints', () => {
describe('drive/folders/update', () => { describe('drive/folders/update', () => {
test('名前を更新できる', async () => { test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
name: 'new name', name: 'new name',
}, alice); }, alice);
@ -753,11 +758,11 @@ describe('Endpoints', () => {
}); });
test('他人のフォルダを更新できない', async () => { test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, bob)).body; }, bob)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
name: 'new name', name: 'new name',
}, alice); }, alice);
@ -766,14 +771,14 @@ describe('Endpoints', () => {
}); });
test('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
@ -784,18 +789,18 @@ describe('Endpoints', () => {
}); });
test('親フォルダを無しに更新できる', async () => { test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: null, parentId: null,
}, alice); }, alice);
@ -806,14 +811,14 @@ describe('Endpoints', () => {
}); });
test('他人のフォルダを親フォルダに設定できない', async () => { test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, bob)).body; }, bob)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
@ -822,18 +827,18 @@ describe('Endpoints', () => {
}); });
test('フォルダが循環するような構造にできない', async () => { test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const parentFolder = (await api('/drive/folders/create', { const parentFolder = (await api('drive/folders/create', {
name: 'parent', name: 'parent',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: parentFolder.id, folderId: parentFolder.id,
parentId: folder.id, parentId: folder.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: parentFolder.id, parentId: parentFolder.id,
}, alice); }, alice);
@ -842,25 +847,25 @@ describe('Endpoints', () => {
}); });
test('フォルダが循環するような構造にできない(再帰的)', async () => { test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const folderB = (await api('/drive/folders/create', { const folderB = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const folderC = (await api('/drive/folders/create', { const folderC = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: folderB.id, folderId: folderB.id,
parentId: folderA.id, parentId: folderA.id,
}, alice); }, alice);
await api('/drive/folders/update', { await api('drive/folders/update', {
folderId: folderC.id, folderId: folderC.id,
parentId: folderB.id, parentId: folderB.id,
}, alice); }, alice);
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folderA.id, folderId: folderA.id,
parentId: folderC.id, parentId: folderC.id,
}, alice); }, alice);
@ -869,11 +874,11 @@ describe('Endpoints', () => {
}); });
test('フォルダが循環するような構造にできない(自身)', async () => { test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folderA.id, folderId: folderA.id,
parentId: folderA.id, parentId: folderA.id,
}, alice); }, alice);
@ -882,11 +887,11 @@ describe('Endpoints', () => {
}); });
test('存在しない親フォルダを設定できない', async () => { test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: '000000000000000000000000', parentId: '000000000000000000000000',
}, alice); }, alice);
@ -895,11 +900,11 @@ describe('Endpoints', () => {
}); });
test('不正な親フォルダIDで怒られる', async () => { test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('drive/folders/create', {
name: 'test', name: 'test',
}, alice)).body; }, alice)).body;
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: folder.id, folderId: folder.id,
parentId: 'foo', parentId: 'foo',
}, alice); }, alice);
@ -908,7 +913,7 @@ describe('Endpoints', () => {
}); });
test('存在しないフォルダを更新できない', async () => { test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: '000000000000000000000000', folderId: '000000000000000000000000',
}, alice); }, alice);
@ -916,7 +921,7 @@ describe('Endpoints', () => {
}); });
test('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', { const res = await api('drive/folders/update', {
folderId: 'foo', folderId: 'foo',
}, alice); }, alice);
@ -937,7 +942,7 @@ describe('Endpoints', () => {
visibleUserIds: [alice.id], visibleUserIds: [alice.id],
}); });
const res = await api('/notes/replies', { const res = await api('notes/replies', {
noteId: alicePost.id, noteId: alicePost.id,
}, carol); }, carol);
@ -949,7 +954,7 @@ describe('Endpoints', () => {
describe('notes/timeline', () => { describe('notes/timeline', () => {
test('フォロワー限定投稿が含まれる', async () => { test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', { await api('following/create', {
userId: carol.id, userId: carol.id,
}, dave); }, dave);
@ -958,7 +963,7 @@ describe('Endpoints', () => {
visibility: 'followers', visibility: 'followers',
}); });
const res = await api('/notes/timeline', {}, dave); const res = await api('notes/timeline', {}, dave);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -979,12 +984,12 @@ describe('Endpoints', () => {
test('他者に関するメモを更新できる', async () => { test('他者に関するメモを更新できる', async () => {
const memo = '10月まで低浮上とのこと。'; const memo = '10月まで低浮上とのこと。';
const res1 = await api('/users/update-memo', { const res1 = await api('users/update-memo', {
memo, memo,
userId: bob.id, userId: bob.id,
}, alice); }, alice);
const res2 = await api('/users/show', { const res2 = await api('users/show', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
assert.strictEqual(res1.status, 204); assert.strictEqual(res1.status, 204);
@ -994,12 +999,12 @@ describe('Endpoints', () => {
test('自分に関するメモを更新できる', async () => { test('自分に関するメモを更新できる', async () => {
const memo = 'チケットを月末までに買う。'; const memo = 'チケットを月末までに買う。';
const res1 = await api('/users/update-memo', { const res1 = await api('users/update-memo', {
memo, memo,
userId: alice.id, userId: alice.id,
}, alice); }, alice);
const res2 = await api('/users/show', { const res2 = await api('users/show', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(res1.status, 204); assert.strictEqual(res1.status, 204);
@ -1009,17 +1014,17 @@ describe('Endpoints', () => {
test('メモを削除できる', async () => { test('メモを削除できる', async () => {
const memo = '10月まで低浮上とのこと。'; const memo = '10月まで低浮上とのこと。';
await api('/users/update-memo', { await api('users/update-memo', {
memo, memo,
userId: bob.id, userId: bob.id,
}, alice); }, alice);
await api('/users/update-memo', { await api('users/update-memo', {
memo: '', memo: '',
userId: bob.id, userId: bob.id,
}, alice); }, alice);
const res = await api('/users/show', { const res = await api('users/show', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@ -1032,21 +1037,21 @@ describe('Endpoints', () => {
const memoCarolToBob = '例の件について今度問いただす。'; const memoCarolToBob = '例の件について今度問いただす。';
await Promise.all([ await Promise.all([
api('/users/update-memo', { api('users/update-memo', {
memo: memoAliceToBob, memo: memoAliceToBob,
userId: bob.id, userId: bob.id,
}, alice), }, alice),
api('/users/update-memo', { api('users/update-memo', {
memo: memoCarolToBob, memo: memoCarolToBob,
userId: bob.id, userId: bob.id,
}, carol), }, carol),
]); ]);
const [resAlice, resCarol] = await Promise.all([ const [resAlice, resCarol] = await Promise.all([
api('/users/show', { api('users/show', {
userId: bob.id, userId: bob.id,
}, alice), }, alice),
api('/users/show', { api('users/show', {
userId: bob.id, userId: bob.id,
}, carol), }, carol),
]); ]);

View file

@ -18,7 +18,7 @@ describe('export-clips', () => {
// XXX: Any better way to get the result? // XXX: Any better way to get the result?
async function pollFirstDriveFile() { async function pollFirstDriveFile() {
while (true) { while (true) {
const files = (await api('/drive/files', {}, alice)).body; const files = (await api('drive/files', {}, alice)).body;
if (!files.length) { if (!files.length) {
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
continue; continue;
@ -26,7 +26,7 @@ describe('export-clips', () => {
if (files.length > 1) { if (files.length > 1) {
throw new Error('Too many files?'); throw new Error('Too many files?');
} }
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body; const file = (await api('drive/files/show', { fileId: files[0].id }, alice)).body;
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`)); const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
return await res.json(); return await res.json();
} }
@ -44,16 +44,16 @@ describe('export-clips', () => {
beforeEach(async () => { beforeEach(async () => {
// Clean all clips and files of alice // Clean all clips and files of alice
const clips = (await api('/clips/list', {}, alice)).body; const clips = (await api('clips/list', {}, alice)).body;
for (const clip of clips) { for (const clip of clips) {
const res = await api('/clips/delete', { clipId: clip.id }, alice); const res = await api('clips/delete', { clipId: clip.id }, alice);
if (res.status !== 204) { if (res.status !== 204) {
throw new Error('Failed to delete clip'); throw new Error('Failed to delete clip');
} }
} }
const files = (await api('/drive/files', {}, alice)).body; const files = (await api('drive/files', {}, alice)).body;
for (const file of files) { for (const file of files) {
const res = await api('/drive/files/delete', { fileId: file.id }, alice); const res = await api('drive/files/delete', { fileId: file.id }, alice);
if (res.status !== 204) { if (res.status !== 204) {
throw new Error('Failed to delete file'); throw new Error('Failed to delete file');
} }
@ -61,13 +61,13 @@ describe('export-clips', () => {
}); });
test('basic export', async () => { test('basic export', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'foo', name: 'foo',
description: 'bar', description: 'bar',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
@ -77,7 +77,7 @@ describe('export-clips', () => {
}); });
test('export with notes', async () => { test('export with notes', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'foo', name: 'foo',
description: 'bar', description: 'bar',
}, alice); }, alice);
@ -96,14 +96,14 @@ describe('export-clips', () => {
}); });
for (const note of [note1, note2]) { for (const note of [note1, note2]) {
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip.id, clipId: clip.id,
noteId: note.id, noteId: note.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
} }
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
@ -116,14 +116,14 @@ describe('export-clips', () => {
}); });
test('multiple clips', async () => { test('multiple clips', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'kawaii', name: 'kawaii',
description: 'kawaii', description: 'kawaii',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const clip1 = res.body; const clip1 = res.body;
res = await api('/clips/create', { res = await api('clips/create', {
name: 'yuri', name: 'yuri',
description: 'yuri', description: 'yuri',
}, alice); }, alice);
@ -138,19 +138,19 @@ describe('export-clips', () => {
text: 'baz2', text: 'baz2',
}); });
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip1.id, clipId: clip1.id,
noteId: note1.id, noteId: note1.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip2.id, clipId: clip2.id,
noteId: note2.id, noteId: note2.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();
@ -163,7 +163,7 @@ describe('export-clips', () => {
}); });
test('Clipping other user\'s note', async () => { test('Clipping other user\'s note', async () => {
let res = await api('/clips/create', { let res = await api('clips/create', {
name: 'kawaii', name: 'kawaii',
description: 'kawaii', description: 'kawaii',
}, alice); }, alice);
@ -175,13 +175,13 @@ describe('export-clips', () => {
visibility: 'followers', visibility: 'followers',
}); });
res = await api('/clips/add-note', { res = await api('clips/add-note', {
clipId: clip.id, clipId: clip.id,
noteId: note.id, noteId: note.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice); res = await api('i/export-clips', {}, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile(); const exported = await pollFirstDriveFile();

View file

@ -23,13 +23,13 @@ const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => { describe('Webリソース', () => {
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let aliceUploadedFile: any; let aliceUploadedFile: misskey.entities.DriveFile | null;
let alicesPost: any; let alicesPost: misskey.entities.Note;
let alicePage: any; let alicePage: misskey.entities.Page;
let alicePlay: any; let alicePlay: misskey.entities.Flash;
let aliceClip: any; let aliceClip: misskey.entities.Clip;
let aliceGalleryPost: any; let aliceGalleryPost: misskey.entities.GalleryPost;
let aliceChannel: any; let aliceChannel: misskey.entities.Channel;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
@ -77,7 +77,7 @@ describe('Webリソース', () => {
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
aliceUploadedFile = await uploadFile(alice); aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, { alicesPost = await post(alice, {
text: 'test', text: 'test',
}); });
@ -85,7 +85,7 @@ describe('Webリソース', () => {
alicePlay = await play(alice, {}); alicePlay = await play(alice, {});
aliceClip = await clip(alice, {}); aliceClip = await clip(alice, {});
aliceGalleryPost = await galleryPost(alice, { aliceGalleryPost = await galleryPost(alice, {
fileIds: [aliceUploadedFile.body.id], fileIds: [aliceUploadedFile!.id],
}); });
aliceChannel = await channel(alice, {}); aliceChannel = await channel(alice, {});

View file

@ -19,15 +19,15 @@ describe('FF visibility', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -39,36 +39,36 @@ describe('FF visibility', () => {
test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -78,36 +78,36 @@ describe('FF visibility', () => {
test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -116,15 +116,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -136,36 +136,36 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -175,36 +175,36 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -213,15 +213,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -231,34 +231,34 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
@ -267,34 +267,34 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
@ -302,19 +302,19 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -326,45 +326,45 @@ describe('FF visibility', () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -374,45 +374,45 @@ describe('FF visibility', () => {
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -421,15 +421,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -441,36 +441,36 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true); assert.strictEqual(Array.isArray(followingRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
@ -480,36 +480,36 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
@ -518,15 +518,15 @@ describe('FF visibility', () => {
}); });
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@ -536,34 +536,34 @@ describe('FF visibility', () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followingRes = await api('/users/following', { const followingRes = await api('users/following', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followingRes.status, 400); assert.strictEqual(followingRes.status, 400);
@ -572,34 +572,34 @@ describe('FF visibility', () => {
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);
const followersRes = await api('/users/followers', { const followersRes = await api('users/followers', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
@ -609,7 +609,7 @@ describe('FF visibility', () => {
describe('AP', () => { describe('AP', () => {
test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'public', followingVisibility: 'public',
}, alice); }, alice);
@ -617,7 +617,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 200); assert.strictEqual(followingRes.status, 200);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'followers', followingVisibility: 'followers',
}, alice); }, alice);
@ -625,7 +625,7 @@ describe('FF visibility', () => {
assert.strictEqual(followingRes.status, 403); assert.strictEqual(followingRes.status, 403);
} }
{ {
await api('/i/update', { await api('i/update', {
followingVisibility: 'private', followingVisibility: 'private',
}, alice); }, alice);
@ -636,7 +636,7 @@ describe('FF visibility', () => {
test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
{ {
await api('/i/update', { await api('i/update', {
followersVisibility: 'public', followersVisibility: 'public',
}, alice); }, alice);
@ -644,7 +644,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 200); assert.strictEqual(followersRes.status, 200);
} }
{ {
await api('/i/update', { await api('i/update', {
followersVisibility: 'followers', followersVisibility: 'followers',
}, alice); }, alice);
@ -652,7 +652,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 403); assert.strictEqual(followersRes.status, 403);
} }
{ {
await api('/i/update', { await api('i/update', {
followersVisibility: 'private', followersVisibility: 'private',
}, alice); }, alice);

View file

@ -55,7 +55,7 @@ describe('Account Move', () => {
}, 1000 * 10); }, 1000 * 10);
test('Able to create an alias', async () => { test('Able to create an alias', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, bob); }, bob);
@ -67,7 +67,7 @@ describe('Account Move', () => {
}); });
test('Able to create a local alias without hostname', async () => { test('Able to create a local alias without hostname', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: ['@alice'], alsoKnownAs: ['@alice'],
}, bob); }, bob);
@ -77,7 +77,7 @@ describe('Account Move', () => {
}); });
test('Able to create a local alias without @', async () => { test('Able to create a local alias without @', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: ['alice'], alsoKnownAs: ['alice'],
}, bob); }, bob);
@ -87,7 +87,7 @@ describe('Account Move', () => {
}); });
test('Able to set remote user (but may fail)', async () => { test('Able to set remote user (but may fail)', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: ['@syuilo@example.com'], alsoKnownAs: ['@syuilo@example.com'],
}, bob); }, bob);
@ -97,7 +97,7 @@ describe('Account Move', () => {
}); });
test('Unable to add duplicated aliases to alsoKnownAs', async () => { test('Unable to add duplicated aliases to alsoKnownAs', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
}, bob); }, bob);
@ -107,7 +107,7 @@ describe('Account Move', () => {
}); });
test('Unable to add itself', async () => { test('Unable to add itself', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@bob@${url.hostname}`], alsoKnownAs: [`@bob@${url.hostname}`],
}, bob); }, bob);
@ -117,7 +117,7 @@ describe('Account Move', () => {
}); });
test('Unable to add a nonexisting local account to alsoKnownAs', async () => { test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
const res1 = await api('/i/update', { const res1 = await api('i/update', {
alsoKnownAs: [`@nonexist@${url.hostname}`], alsoKnownAs: [`@nonexist@${url.hostname}`],
}, bob); }, bob);
@ -125,7 +125,7 @@ describe('Account Move', () => {
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER'); assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
const res2 = await api('/i/update', { const res2 = await api('i/update', {
alsoKnownAs: ['@alice', 'nonexist'], alsoKnownAs: ['@alice', 'nonexist'],
}, bob); }, bob);
@ -135,7 +135,7 @@ describe('Account Move', () => {
}); });
test('Able to add two existing local account to alsoKnownAs', async () => { test('Able to add two existing local account to alsoKnownAs', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
}, bob); }, bob);
@ -146,10 +146,10 @@ describe('Account Move', () => {
}); });
test('Able to properly overwrite alsoKnownAs', async () => { test('Able to properly overwrite alsoKnownAs', async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, bob); }, bob);
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`], alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob); }, bob);
@ -164,27 +164,27 @@ describe('Account Move', () => {
let antennaId = ''; let antennaId = '';
beforeAll(async () => { beforeAll(async () => {
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, root); }, root);
const listRoot = await api('/users/lists/create', { const listRoot = await api('users/lists/create', {
name: secureRndstr(8), name: secureRndstr(8),
}, root); }, root);
await api('/users/lists/push', { await api('users/lists/push', {
listId: listRoot.body.id, listId: listRoot.body.id,
userId: alice.id, userId: alice.id,
}, root); }, root);
await api('/following/create', { await api('following/create', {
userId: root.id, userId: root.id,
}, alice); }, alice);
await api('/following/create', { await api('following/create', {
userId: eve.id, userId: eve.id,
}, alice); }, alice);
const antenna = await api('/antennas/create', { const antenna = await api('antennas/create', {
name: secureRndstr(8), name: secureRndstr(8),
src: 'home', src: 'home',
keywords: [secureRndstr(8)], keywords: [[secureRndstr(8)]],
excludeKeywords: [], excludeKeywords: [],
users: [], users: [],
caseSensitive: false, caseSensitive: false,
@ -195,48 +195,48 @@ describe('Account Move', () => {
}, alice); }, alice);
antennaId = antenna.body.id; antennaId = antenna.body.id;
await api('/i/update', { await api('i/update', {
alsoKnownAs: [`@alice@${url.hostname}`], alsoKnownAs: [`@alice@${url.hostname}`],
}, bob); }, bob);
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, carol); }, carol);
await api('/mute/create', { await api('mute/create', {
userId: alice.id, userId: alice.id,
}, dave); }, dave);
await api('/blocking/create', { await api('blocking/create', {
userId: alice.id, userId: alice.id,
}, dave); }, dave);
await api('/following/create', { await api('following/create', {
userId: eve.id, userId: eve.id,
}, dave); }, dave);
await api('/following/create', { await api('following/create', {
userId: dave.id, userId: dave.id,
}, eve); }, eve);
const listEve = await api('/users/lists/create', { const listEve = await api('users/lists/create', {
name: secureRndstr(8), name: secureRndstr(8),
}, eve); }, eve);
await api('/users/lists/push', { await api('users/lists/push', {
listId: listEve.body.id, listId: listEve.body.id,
userId: bob.id, userId: bob.id,
}, eve); }, eve);
await api('/i/update', { await api('i/update', {
isLocked: true, isLocked: true,
}, frank); }, frank);
await api('/following/create', { await api('following/create', {
userId: frank.id, userId: frank.id,
}, alice); }, alice);
await api('/following/requests/accept', { await api('following/requests/accept', {
userId: alice.id, userId: alice.id,
}, frank); }, frank);
}, 1000 * 10); }, 1000 * 10);
test('Prohibit the root account from moving', async () => { test('Prohibit the root account from moving', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`, moveToAccount: `@bob@${url.hostname}`,
}, root); }, root);
@ -246,7 +246,7 @@ describe('Account Move', () => {
}); });
test('Unable to move to a nonexisting local account', async () => { test('Unable to move to a nonexisting local account', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@nonexist@${url.hostname}`, moveToAccount: `@nonexist@${url.hostname}`,
}, alice); }, alice);
@ -256,7 +256,7 @@ describe('Account Move', () => {
}); });
test('Unable to move if alsoKnownAs is invalid', async () => { test('Unable to move if alsoKnownAs is invalid', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@carol@${url.hostname}`, moveToAccount: `@carol@${url.hostname}`,
}, alice); }, alice);
@ -266,7 +266,7 @@ describe('Account Move', () => {
}); });
test('Relationships have been properly migrated', async () => { test('Relationships have been properly migrated', async () => {
const move = await api('/i/move', { const move = await api('i/move', {
moveToAccount: `@bob@${url.hostname}`, moveToAccount: `@bob@${url.hostname}`,
}, alice); }, alice);
@ -275,13 +275,13 @@ describe('Account Move', () => {
await sleep(1000 * 3); // wait for jobs to finish await sleep(1000 * 3); // wait for jobs to finish
// Unfollow delayed? // Unfollow delayed?
const aliceFollowings = await api('/users/following', { const aliceFollowings = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
assert.strictEqual(aliceFollowings.status, 200); assert.strictEqual(aliceFollowings.status, 200);
assert.strictEqual(aliceFollowings.body.length, 3); assert.strictEqual(aliceFollowings.body.length, 3);
const carolFollowings = await api('/users/following', { const carolFollowings = await api('users/following', {
userId: carol.id, userId: carol.id,
}, carol); }, carol);
assert.strictEqual(carolFollowings.status, 200); assert.strictEqual(carolFollowings.status, 200);
@ -289,25 +289,25 @@ describe('Account Move', () => {
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
const blockings = await api('/blocking/list', {}, dave); const blockings = await api('blocking/list', {}, dave);
assert.strictEqual(blockings.status, 200); assert.strictEqual(blockings.status, 200);
assert.strictEqual(blockings.body.length, 2); assert.strictEqual(blockings.body.length, 2);
assert.strictEqual(blockings.body[0].blockeeId, bob.id); assert.strictEqual(blockings.body[0].blockeeId, bob.id);
assert.strictEqual(blockings.body[1].blockeeId, alice.id); assert.strictEqual(blockings.body[1].blockeeId, alice.id);
const mutings = await api('/mute/list', {}, dave); const mutings = await api('mute/list', {}, dave);
assert.strictEqual(mutings.status, 200); assert.strictEqual(mutings.status, 200);
assert.strictEqual(mutings.body.length, 2); assert.strictEqual(mutings.body.length, 2);
assert.strictEqual(mutings.body[0].muteeId, bob.id); assert.strictEqual(mutings.body[0].muteeId, bob.id);
assert.strictEqual(mutings.body[1].muteeId, alice.id); assert.strictEqual(mutings.body[1].muteeId, alice.id);
const rootLists = await api('/users/lists/list', {}, root); const rootLists = await api('users/lists/list', {}, root);
assert.strictEqual(rootLists.status, 200); assert.strictEqual(rootLists.status, 200);
assert.strictEqual(rootLists.body[0].userIds.length, 2); assert.strictEqual(rootLists.body[0].userIds.length, 2);
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
const eveLists = await api('/users/lists/list', {}, eve); const eveLists = await api('users/lists/list', {}, eve);
assert.strictEqual(eveLists.status, 200); assert.strictEqual(eveLists.status, 200);
assert.strictEqual(eveLists.body[0].userIds.length, 1); assert.strictEqual(eveLists.body[0].userIds.length, 1);
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
@ -315,13 +315,13 @@ describe('Account Move', () => {
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => { test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
await successfulApiCall({ await successfulApiCall({
endpoint: '/following/create', endpoint: 'following/create',
parameters: { parameters: {
userId: frank.id, userId: frank.id,
}, },
user: bob, user: bob,
}); });
const followers = await api('/users/followers', { const followers = await api('users/followers', {
userId: frank.id, userId: frank.id,
}, frank); }, frank);
@ -333,7 +333,7 @@ describe('Account Move', () => {
test('Unfollowed after 10 sec (24 hours in production).', async () => { test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8); await sleep(1000 * 8);
const following = await api('/users/following', { const following = await api('users/following', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@ -342,7 +342,7 @@ describe('Account Move', () => {
}); });
test('Unable to move if the destination account has already moved.', async () => { test('Unable to move if the destination account has already moved.', async () => {
const res = await api('/i/move', { const res = await api('i/move', {
moveToAccount: `@alice@${url.hostname}`, moveToAccount: `@alice@${url.hostname}`,
}, bob); }, bob);
@ -352,7 +352,7 @@ describe('Account Move', () => {
}); });
test('Follow and follower counts are properly adjusted', async () => { test('Follow and follower counts are properly adjusted', async () => {
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, eve); }, eve);
const newAlice = await Users.findOneByOrFail({ id: alice.id }); const newAlice = await Users.findOneByOrFail({ id: alice.id });
@ -365,7 +365,7 @@ describe('Account Move', () => {
assert.strictEqual(newEve.followingCount, 1); assert.strictEqual(newEve.followingCount, 1);
assert.strictEqual(newEve.followersCount, 1); assert.strictEqual(newEve.followersCount, 1);
await api('/following/delete', { await api('following/delete', {
userId: alice.id, userId: alice.id,
}, eve); }, eve);
newEve = await Users.findOneByOrFail({ id: eve.id }); newEve = await Users.findOneByOrFail({ id: eve.id });
@ -374,49 +374,49 @@ describe('Account Move', () => {
}); });
test.each([ test.each([
'/antennas/create', 'antennas/create',
'/channels/create', 'channels/create',
'/channels/favorite', 'channels/favorite',
'/channels/follow', 'channels/follow',
'/channels/unfavorite', 'channels/unfavorite',
'/channels/unfollow', 'channels/unfollow',
'/clips/add-note', 'clips/add-note',
'/clips/create', 'clips/create',
'/clips/favorite', 'clips/favorite',
'/clips/remove-note', 'clips/remove-note',
'/clips/unfavorite', 'clips/unfavorite',
'/clips/update', 'clips/update',
'/drive/files/upload-from-url', 'drive/files/upload-from-url',
'/flash/create', 'flash/create',
'/flash/like', 'flash/like',
'/flash/unlike', 'flash/unlike',
'/flash/update', 'flash/update',
'/following/create', 'following/create',
'/gallery/posts/create', 'gallery/posts/create',
'/gallery/posts/like', 'gallery/posts/like',
'/gallery/posts/unlike', 'gallery/posts/unlike',
'/gallery/posts/update', 'gallery/posts/update',
'/i/claim-achievement', 'i/claim-achievement',
'/i/move', 'i/move',
'/i/import-blocking', 'i/import-blocking',
'/i/import-following', 'i/import-following',
'/i/import-muting', 'i/import-muting',
'/i/import-user-lists', 'i/import-user-lists',
'/i/pin', 'i/pin',
'/mute/create', 'mute/create',
'/notes/create', 'notes/create',
'/notes/favorites/create', 'notes/favorites/create',
'/notes/polls/vote', 'notes/polls/vote',
'/notes/reactions/create', 'notes/reactions/create',
'/pages/create', 'pages/create',
'/pages/like', 'pages/like',
'/pages/unlike', 'pages/unlike',
'/pages/update', 'pages/update',
'/renote-mute/create', 'renote-mute/create',
'/users/lists/create', 'users/lists/create',
'/users/lists/pull', 'users/lists/pull',
'/users/lists/push', 'users/lists/push',
])('Prohibit access after moving: %s', async (endpoint) => { ] as const)('Prohibit access after moving: %s', async (endpoint) => {
const res = await api(endpoint, {}, alice); const res = await api(endpoint, {}, alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
@ -424,11 +424,11 @@ describe('Account Move', () => {
}); });
test('Prohibit access after moving: /antennas/update', async () => { test('Prohibit access after moving: /antennas/update', async () => {
const res = await api('/antennas/update', { const res = await api('antennas/update', {
antennaId, antennaId,
name: secureRndstr(8), name: secureRndstr(8),
src: 'users', src: 'users',
keywords: [secureRndstr(8)], keywords: [[secureRndstr(8)]],
excludeKeywords: [], excludeKeywords: [],
users: [eve.id], users: [eve.id],
caseSensitive: false, caseSensitive: false,
@ -447,12 +447,12 @@ describe('Account Move', () => {
const res = await uploadFile(alice); const res = await uploadFile(alice);
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED');
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
}); });
test('Prohibit updating alsoKnownAs after moving', async () => { test('Prohibit updating alsoKnownAs after moving', async () => {
const res = await api('/i/update', { const res = await api('i/update', {
alsoKnownAs: [`@eve@${url.hostname}`], alsoKnownAs: [`@eve@${url.hostname}`],
}, alice); }, alice);

View file

@ -19,21 +19,31 @@ describe('Mute', () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
// Mute: alice ==> carol
await api('mute/create', {
userId: carol.id,
}, alice);
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await api('/mute/create', { const res = await api('mute/create', {
userId: carol.id, userId: bob.id,
}, alice); }, alice);
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
// 単体でも走らせられるように副作用消す
await api('mute/delete', {
userId: bob.id,
}, alice);
}); });
test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' }); const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' }); const carolNote = await post(carol, { text: '@alice hi' });
const res = await api('/notes/mentions', {}, alice); const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -43,11 +53,11 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' }); await post(carol, { text: '@alice hi' });
const res = await api('/i', {}, alice); const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false); assert.strictEqual(res.body.hasUnreadMentions, false);
@ -55,7 +65,7 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
@ -64,8 +74,8 @@ describe('Mute', () => {
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
await api('/notifications/mark-all-as-read', {}, alice); await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
@ -78,7 +88,7 @@ describe('Mute', () => {
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -94,7 +104,7 @@ describe('Mute', () => {
renoteId: carolNote.id, renoteId: carolNote.id,
}); });
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -110,7 +120,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like'); await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like'); await react(carol, aliceNote, 'like');
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -123,7 +133,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id }); await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id }); await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -137,7 +147,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' }); await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' }); await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -151,7 +161,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id }); await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id }); await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -165,7 +175,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id }); await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id }); await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -175,30 +185,36 @@ describe('Mute', () => {
}); });
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
}); });
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice); await api('i/update', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
}); });
}); });
@ -208,7 +224,7 @@ describe('Mute', () => {
await react(bob, aliceNote, 'like'); await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like'); await react(carol, aliceNote, 'like');
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -220,7 +236,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi', replyId: aliceNote.id }); await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id }); await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -234,7 +250,7 @@ describe('Mute', () => {
await post(bob, { text: '@alice hi' }); await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' }); await post(carol, { text: '@alice hi' });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -248,7 +264,7 @@ describe('Mute', () => {
await post(bob, { text: 'hi', renoteId: aliceNote.id }); await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id }); await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -262,7 +278,7 @@ describe('Mute', () => {
await post(bob, { renoteId: aliceNote.id }); await post(bob, { renoteId: aliceNote.id });
await post(carol, { renoteId: aliceNote.id }); await post(carol, { renoteId: aliceNote.id });
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -272,24 +288,27 @@ describe('Mute', () => {
}); });
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => { test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
await api('/i/follow', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/i/follow', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
await api('following/delete', { userId: alice.id }, bob);
await api('following/delete', { userId: alice.id }, carol);
}); });
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => { test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
await api('/i/update/', { isLocked: true }, alice); await api('i/update', { isLocked: true }, alice);
await api('/following/create', { userId: alice.id }, bob); await api('following/create', { userId: alice.id }, bob);
await api('/following/create', { userId: alice.id }, carol); await api('following/create', { userId: alice.id }, carol);
const res = await api('/i/notifications-grouped', {}, alice); const res = await api('i/notifications-grouped', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

View file

@ -31,7 +31,7 @@ describe('Note', () => {
text: 'test', text: 'test',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -41,7 +41,7 @@ describe('Note', () => {
test('ファイルを添付できる', async () => { test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', { const res = await api('notes/create', {
fileIds: [file.id], fileIds: [file.id],
}, alice); }, alice);
@ -53,7 +53,7 @@ describe('Note', () => {
test('他人のファイルで怒られる', async () => { test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await api('/notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',
fileIds: [file.id], fileIds: [file.id],
}, alice); }, alice);
@ -64,7 +64,7 @@ describe('Note', () => {
}, 1000 * 10); }, 1000 * 10);
test('存在しないファイルで怒られる', async () => { test('存在しないファイルで怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',
fileIds: ['000000000000000000000000'], fileIds: ['000000000000000000000000'],
}, alice); }, alice);
@ -75,7 +75,7 @@ describe('Note', () => {
}); });
test('不正なファイルIDで怒られる', async () => { test('不正なファイルIDで怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
fileIds: ['kyoppie'], fileIds: ['kyoppie'],
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
@ -93,7 +93,7 @@ describe('Note', () => {
replyId: bobPost.id, replyId: bobPost.id,
}; };
const res = await api('/notes/create', alicePost, alice); const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -111,7 +111,7 @@ describe('Note', () => {
renoteId: bobPost.id, renoteId: bobPost.id,
}; };
const res = await api('/notes/create', alicePost, alice); const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -129,7 +129,7 @@ describe('Note', () => {
renoteId: bobPost.id, renoteId: bobPost.id,
}; };
const res = await api('/notes/create', alicePost, alice); const res = await api('notes/create', alicePost, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -142,7 +142,7 @@ describe('Note', () => {
const bobPost = await post(bob, { const bobPost = await post(bob, {
text: 'test', text: 'test',
}); });
const res = await api('/notes/create', { const res = await api('notes/create', {
text: ' ', text: ' ',
renoteId: bobPost.id, renoteId: bobPost.id,
}, alice); }, alice);
@ -152,7 +152,7 @@ describe('Note', () => {
}); });
test('visibility: followersでrenoteできる', async () => { test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', { const createRes = await api('notes/create', {
text: 'test', text: 'test',
visibility: 'followers', visibility: 'followers',
}, alice); }, alice);
@ -160,7 +160,7 @@ describe('Note', () => {
assert.strictEqual(createRes.status, 200); assert.strictEqual(createRes.status, 200);
const renoteId = createRes.body.createdNote.id; const renoteId = createRes.body.createdNote.id;
const renoteRes = await api('/notes/create', { const renoteRes = await api('notes/create', {
visibility: 'followers', visibility: 'followers',
renoteId, renoteId,
}, alice); }, alice);
@ -169,7 +169,7 @@ describe('Note', () => {
assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId); assert.strictEqual(renoteRes.body.createdNote.renoteId, renoteId);
assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers'); assert.strictEqual(renoteRes.body.createdNote.visibility, 'followers');
const deleteRes = await api('/notes/delete', { const deleteRes = await api('notes/delete', {
noteId: renoteRes.body.createdNote.id, noteId: renoteRes.body.createdNote.id,
}, alice); }, alice);
@ -177,11 +177,11 @@ describe('Note', () => {
}); });
test('visibility: followersなートに対してフォロワーはリプライできる', async () => { test('visibility: followersなートに対してフォロワーはリプライできる', async () => {
await api('/following/create', { await api('following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'followers', visibility: 'followers',
}, alice); }, alice);
@ -189,7 +189,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const replyId = aliceNote.body.createdNote.id; const replyId = aliceNote.body.createdNote.id;
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note', text: 'reply to alice note',
replyId, replyId,
}, bob); }, bob);
@ -197,20 +197,20 @@ describe('Note', () => {
assert.strictEqual(bobReply.status, 200); assert.strictEqual(bobReply.status, 200);
assert.strictEqual(bobReply.body.createdNote.replyId, replyId); assert.strictEqual(bobReply.body.createdNote.replyId, replyId);
await api('/following/delete', { await api('following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
}); });
test('visibility: followersなートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => { test('visibility: followersなートに対してフォロワーでないユーザーがリプライしようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'followers', visibility: 'followers',
}, alice); }, alice);
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note', text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id, replyId: aliceNote.body.createdNote.id,
}, bob); }, bob);
@ -220,7 +220,7 @@ describe('Note', () => {
}); });
test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => { test('visibility: specifiedなートに対してvisibility: specifiedで返信できる', async () => {
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [bob.id], visibleUserIds: [bob.id],
@ -228,7 +228,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note', text: 'reply to alice note',
replyId: aliceNote.body.createdNote.id, replyId: aliceNote.body.createdNote.id,
visibility: 'specified', visibility: 'specified',
@ -239,7 +239,7 @@ describe('Note', () => {
}); });
test('visibility: specifiedなートに対してvisibility: follwersで返信しようとすると怒られる', async () => { test('visibility: specifiedなートに対してvisibility: follwersで返信しようとすると怒られる', async () => {
const aliceNote = await api('/notes/create', { const aliceNote = await api('notes/create', {
text: 'direct note to bob', text: 'direct note to bob',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [bob.id], visibleUserIds: [bob.id],
@ -247,7 +247,7 @@ describe('Note', () => {
assert.strictEqual(aliceNote.status, 200); assert.strictEqual(aliceNote.status, 200);
const bobReply = await api('/notes/create', { const bobReply = await api('notes/create', {
text: 'reply to alice note with visibility: followers', text: 'reply to alice note with visibility: followers',
replyId: aliceNote.body.createdNote.id, replyId: aliceNote.body.createdNote.id,
visibility: 'followers', visibility: 'followers',
@ -261,7 +261,7 @@ describe('Note', () => {
const post = { const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字 text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
@ -269,7 +269,7 @@ describe('Note', () => {
const post = { const post = {
text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字 text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -278,7 +278,7 @@ describe('Note', () => {
text: 'test', text: 'test',
replyId: '000000000000000000000000', replyId: '000000000000000000000000',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -286,7 +286,7 @@ describe('Note', () => {
const post = { const post = {
renoteId: '000000000000000000000000', renoteId: '000000000000000000000000',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -295,7 +295,7 @@ describe('Note', () => {
text: 'test', text: 'test',
replyId: 'foo', replyId: 'foo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -303,7 +303,7 @@ describe('Note', () => {
const post = { const post = {
renoteId: 'foo', renoteId: 'foo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
@ -312,7 +312,7 @@ describe('Note', () => {
text: '@ghost yo', text: '@ghost yo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -324,7 +324,7 @@ describe('Note', () => {
text: '@bob @bob @bob yo', text: '@bob @bob @bob yo',
}; };
const res = await api('/notes/create', post, alice); const res = await api('notes/create', post, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
@ -337,25 +337,25 @@ describe('Note', () => {
describe('添付ファイル情報', () => { describe('添付ファイル情報', () => {
test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const res = await api('/notes/create', { const res = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.files.length, 1); assert.strictEqual(res.body.createdNote.files.length, 1);
assert.strictEqual(res.body.createdNote.files[0].id, file.body.id); assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id);
}); });
test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const res = await api('/notes', { const res = await api('notes', {
withFiles: true, withFiles: true,
}, alice); }, alice);
@ -364,23 +364,23 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.files.length, 1); assert.strictEqual(myNote.files.length, 1);
assert.strictEqual(myNote.files[0].id, file.body.id); assert.strictEqual(myNote.files[0].id, file.body!.id);
}); });
test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const renoted = await api('/notes/create', { const renoted = await api('notes/create', {
renoteId: createdNote.body.createdNote.id, renoteId: createdNote.body.createdNote.id,
}, alice); }, alice);
assert.strictEqual(renoted.status, 200); assert.strictEqual(renoted.status, 200);
const res = await api('/notes', { const res = await api('notes', {
renote: true, renote: true,
}, alice); }, alice);
@ -389,24 +389,24 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.files.length, 1); assert.strictEqual(myNote.renote.files.length, 1);
assert.strictEqual(myNote.renote.files[0].id, file.body.id); assert.strictEqual(myNote.renote.files[0].id, file.body!.id);
}); });
test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', { const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id, replyId: createdNote.body.createdNote.id,
text: 'this is reply', text: 'this is reply',
}, alice); }, alice);
assert.strictEqual(reply.status, 200); assert.strictEqual(reply.status, 200);
const res = await api('/notes', { const res = await api('notes', {
reply: true, reply: true,
}, alice); }, alice);
@ -415,29 +415,29 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.reply.files.length, 1); assert.strictEqual(myNote.reply.files.length, 1);
assert.strictEqual(myNote.reply.files[0].id, file.body.id); assert.strictEqual(myNote.reply.files[0].id, file.body!.id);
}); });
test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const createdNote = await api('/notes/create', { const createdNote = await api('notes/create', {
fileIds: [file.body.id], fileIds: [file.body!.id],
}, alice); }, alice);
assert.strictEqual(createdNote.status, 200); assert.strictEqual(createdNote.status, 200);
const reply = await api('/notes/create', { const reply = await api('notes/create', {
replyId: createdNote.body.createdNote.id, replyId: createdNote.body.createdNote.id,
text: 'this is reply', text: 'this is reply',
}, alice); }, alice);
assert.strictEqual(reply.status, 200); assert.strictEqual(reply.status, 200);
const renoted = await api('/notes/create', { const renoted = await api('notes/create', {
renoteId: reply.body.createdNote.id, renoteId: reply.body.createdNote.id,
}, alice); }, alice);
assert.strictEqual(renoted.status, 200); assert.strictEqual(renoted.status, 200);
const res = await api('/notes', { const res = await api('notes', {
renote: true, renote: true,
}, alice); }, alice);
@ -446,7 +446,7 @@ describe('Note', () => {
const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id);
assert.notEqual(myNote, null); assert.notEqual(myNote, null);
assert.strictEqual(myNote.renote.reply.files.length, 1); assert.strictEqual(myNote.renote.reply.files.length, 1);
assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id); assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id);
}); });
test('NSFWが強制されている場合変更できない', async () => { test('NSFWが強制されている場合変更できない', async () => {
@ -472,7 +472,7 @@ describe('Note', () => {
priority: 0, priority: 0,
value: true, value: true,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -483,15 +483,15 @@ describe('Note', () => {
}, alice); }, alice);
assert.strictEqual(assign.status, 204); assert.strictEqual(assign.status, 204);
assert.strictEqual(file.body.isSensitive, false); assert.strictEqual(file.body!.isSensitive, false);
const nsfwfile = await uploadFile(alice); const nsfwfile = await uploadFile(alice);
assert.strictEqual(nsfwfile.status, 200); assert.strictEqual(nsfwfile.status, 200);
assert.strictEqual(nsfwfile.body.isSensitive, true); assert.strictEqual(nsfwfile.body!.isSensitive, true);
const liftnsfw = await api('drive/files/update', { const liftnsfw = await api('drive/files/update', {
fileId: nsfwfile.body.id, fileId: nsfwfile.body!.id,
isSensitive: false, isSensitive: false,
}, alice); }, alice);
@ -499,7 +499,7 @@ describe('Note', () => {
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE'); assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
const oldaddnsfw = await api('drive/files/update', { const oldaddnsfw = await api('drive/files/update', {
fileId: file.body.id, fileId: file.body!.id,
isSensitive: true, isSensitive: true,
}, alice); }, alice);
@ -518,7 +518,7 @@ describe('Note', () => {
describe('notes/create', () => { describe('notes/create', () => {
test('投票を添付できる', async () => { test('投票を添付できる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['foo', 'bar'], choices: ['foo', 'bar'],
@ -531,14 +531,15 @@ describe('Note', () => {
}); });
test('投票の選択肢が無くて怒られる', async () => { test('投票の選択肢が無くて怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
// @ts-expect-error poll must not be empty
poll: {}, poll: {},
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('投票の選択肢が無くて怒られる (空の配列)', async () => { test('投票の選択肢が無くて怒られる (空の配列)', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
poll: { poll: {
choices: [], choices: [],
}, },
@ -547,7 +548,7 @@ describe('Note', () => {
}); });
test('投票の選択肢が1つで怒られる', async () => { test('投票の選択肢が1つで怒られる', async () => {
const res = await api('/notes/create', { const res = await api('notes/create', {
poll: { poll: {
choices: ['Strawberry Pasta'], choices: ['Strawberry Pasta'],
}, },
@ -556,14 +557,14 @@ describe('Note', () => {
}); });
test('投票できる', async () => { test('投票できる', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
}, },
}, alice); }, alice);
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 1, choice: 1,
}, alice); }, alice);
@ -572,19 +573,19 @@ describe('Note', () => {
}); });
test('複数投票できない', async () => { test('複数投票できない', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
}, },
}, alice); }, alice);
await api('/notes/polls/vote', { await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 0, choice: 0,
}, alice); }, alice);
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 2, choice: 2,
}, alice); }, alice);
@ -593,7 +594,7 @@ describe('Note', () => {
}); });
test('許可されている場合は複数投票できる', async () => { test('許可されている場合は複数投票できる', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
@ -601,17 +602,17 @@ describe('Note', () => {
}, },
}, alice); }, alice);
await api('/notes/polls/vote', { await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 0, choice: 0,
}, alice); }, alice);
await api('/notes/polls/vote', { await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 1, choice: 1,
}, alice); }, alice);
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 2, choice: 2,
}, alice); }, alice);
@ -620,7 +621,7 @@ describe('Note', () => {
}); });
test('締め切られている場合は投票できない', async () => { test('締め切られている場合は投票できない', async () => {
const { body } = await api('/notes/create', { const { body } = await api('notes/create', {
text: 'test', text: 'test',
poll: { poll: {
choices: ['sakura', 'izumi', 'ako'], choices: ['sakura', 'izumi', 'ako'],
@ -630,7 +631,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const res = await api('/notes/polls/vote', { const res = await api('notes/polls/vote', {
noteId: body.createdNote.id, noteId: body.createdNote.id,
choice: 1, choice: 1,
}, alice); }, alice);
@ -649,7 +650,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', { const note1 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -666,7 +667,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204); assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -683,7 +684,7 @@ describe('Note', () => {
assert.strictEqual(sensitive.status, 204); assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogeTesthuge', text: 'hogeTesthuge',
}, alice); }, alice);
@ -702,7 +703,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', { const note1 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -719,7 +720,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204); assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, alice); }, alice);
@ -736,7 +737,7 @@ describe('Note', () => {
assert.strictEqual(prohibited.status, 204); assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', { const note2 = await api('notes/create', {
text: 'hogeTesthuge', text: 'hogeTesthuge',
}, alice); }, alice);
@ -755,7 +756,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', { const note1 = await api('notes/create', {
text: 'hogetesthuge', text: 'hogetesthuge',
}, tom); }, tom);
@ -783,7 +784,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 0, value: 0,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -799,7 +800,7 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', { const note = await api('notes/create', {
text: '@bob potentially annoying text', text: '@bob potentially annoying text',
}, alice); }, alice);
@ -837,7 +838,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 0, value: 0,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -853,10 +854,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', { const note = await api('notes/create', {
text: 'potentially annoying text', text: 'potentially annoying text',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [ bob.id ], visibleUserIds: [bob.id],
}, alice); }, alice);
assert.strictEqual(note.status, 400); assert.strictEqual(note.status, 400);
@ -893,7 +894,7 @@ describe('Note', () => {
priority: 1, priority: 1,
value: 1, value: 1,
}, },
}, } as any,
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@ -909,10 +910,10 @@ describe('Note', () => {
await new Promise(x => setTimeout(x, 2)); await new Promise(x => setTimeout(x, 2));
const note = await api('/notes/create', { const note = await api('notes/create', {
text: '@bob potentially annoying text', text: '@bob potentially annoying text',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [ bob.id ], visibleUserIds: [bob.id],
}, alice); }, alice);
assert.strictEqual(note.status, 200); assert.strictEqual(note.status, 200);

View file

@ -22,7 +22,7 @@ describe('Renote Mute', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await api('/renote-mute/create', { const res = await api('renote-mute/create', {
userId: carol.id, userId: carol.id,
}, alice); }, alice);
@ -37,7 +37,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await sleep(100);
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -54,7 +54,7 @@ describe('Renote Mute', () => {
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await sleep(100);
const res = await api('/notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

View file

@ -601,7 +601,7 @@ describe('Streaming', () => {
// #10443 // #10443
test('ミュートしているサーバのートがリストTLに流れない', async () => { test('ミュートしているサーバのートがリストTLに流れない', async () => {
await api('/i/update', { await api('i/update', {
mutedInstances: ['example.com'], mutedInstances: ['example.com'],
}, chitose); }, chitose);
@ -618,7 +618,7 @@ describe('Streaming', () => {
// #10443 // #10443
test('ミュートしているサーバのートに対するリプライがリストTLに流れない', async () => { test('ミュートしているサーバのートに対するリプライがリストTLに流れない', async () => {
await api('/i/update', { await api('i/update', {
mutedInstances: ['example.com'], mutedInstances: ['example.com'],
}, chitose); }, chitose);
@ -635,7 +635,7 @@ describe('Streaming', () => {
// #10443 // #10443
test('ミュートしているサーバのートに対するリートがリストTLに流れない', async () => { test('ミュートしているサーバのートに対するリートがリストTLに流れない', async () => {
await api('/i/update', { await api('i/update', {
mutedInstances: ['example.com'], mutedInstances: ['example.com'],
}, chitose); }, chitose);

View file

@ -24,12 +24,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); 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 carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/notes/mentions', {}, alice); const res = await api('notes/mentions', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);
@ -40,15 +40,15 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const res = await api('/i', {}, alice); const res = await api('i', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false); assert.strictEqual(res.body.hasUnreadMentions, false);
@ -56,11 +56,11 @@ describe('Note thread mute', () => {
test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => { test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => {
// 状態リセット // 状態リセット
await api('/i/read-all-unread-notes', {}, alice); await api('i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false; let fired = false;
@ -84,12 +84,12 @@ describe('Note thread mute', () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); 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 carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await api('/i/notifications', {}, alice); const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(Array.isArray(res.body), true);

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,9 @@ import type * as misskey from 'misskey-js';
describe('users/notes', () => { describe('users/notes', () => {
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let jpgNote: any; let jpgNote: misskey.entities.Note;
let pngNote: any; let pngNote: misskey.entities.Note;
let jpgPngNote: any; let jpgPngNote: misskey.entities.Note;
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
@ -31,7 +31,7 @@ describe('users/notes', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
test('withFiles', async () => { test('withFiles', async () => {
const res = await api('/users/notes', { const res = await api('users/notes', {
userId: alice.id, userId: alice.id,
withFiles: true, withFiles: true,
}, alice); }, alice);

View file

@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('ユーザー', () => { describe('ユーザー', () => {
@ -24,31 +24,12 @@ describe('ユーザー', () => {
}, {}); }, {});
}; };
// BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う const show = async (id: string, me = root): Promise<misskey.entities.UserDetailed> => {
type UserLite = misskey.entities.UserLite & { return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me });
badgeRoles: any[],
};
type UserDetailedNotMe = UserLite &
misskey.entities.UserDetailed & {
roles: any[],
};
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
achievements: object[],
loggedInDays: number,
policies: object,
};
type User = MeDetailed & { token: string };
const show = async (id: string, me = root): Promise<MeDetailed | UserDetailedNotMe> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
}; };
// UserLiteのキーが過不足なく入っている // UserLiteのキーが過不足なく入っている
const userLite = (user: User): Partial<UserLite> => { const userLite = (user: misskey.entities.UserLite): Partial<misskey.entities.UserLite> => {
return stripUndefined({ return stripUndefined({
id: user.id, id: user.id,
name: user.name, name: user.name,
@ -72,7 +53,7 @@ describe('ユーザー', () => {
}; };
// UserDetailedNotMeのキーが過不足なく入っている // UserDetailedNotMeのキーが過不足なく入っている
const userDetailedNotMe = (user: User): Partial<UserDetailedNotMe> => { const userDetailedNotMe = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({ return stripUndefined({
...userLite(user), ...userLite(user),
url: user.url, url: user.url,
@ -114,7 +95,7 @@ describe('ユーザー', () => {
}; };
// Relations関連のキーが過不足なく入っている // Relations関連のキーが過不足なく入っている
const userDetailedNotMeWithRelations = (user: User): Partial<UserDetailedNotMe> => { const userDetailedNotMeWithRelations = (user: misskey.entities.SignupResponse): Partial<misskey.entities.UserDetailedNotMe> => {
return stripUndefined({ return stripUndefined({
...userDetailedNotMe(user), ...userDetailedNotMe(user),
isFollowing: user.isFollowing ?? false, isFollowing: user.isFollowing ?? false,
@ -131,7 +112,7 @@ describe('ユーザー', () => {
}; };
// MeDetailedのキーが過不足なく入っている // MeDetailedのキーが過不足なく入っている
const meDetailed = (user: User, security = false): Partial<MeDetailed> => { const meDetailed = (user: misskey.entities.SignupResponse, security = false): Partial<misskey.entities.MeDetailed> => {
return stripUndefined({ return stripUndefined({
...userDetailedNotMe(user), ...userDetailedNotMe(user),
avatarId: user.avatarId, avatarId: user.avatarId,
@ -162,6 +143,7 @@ describe('ユーザー', () => {
mutedWords: user.mutedWords, mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords, hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances, mutedInstances: user.mutedInstances,
// @ts-expect-error 後方互換性
mutingNotificationTypes: user.mutingNotificationTypes, mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig, notificationRecieveConfig: user.notificationRecieveConfig,
emailNotificationTypes: user.emailNotificationTypes, emailNotificationTypes: user.emailNotificationTypes,
@ -176,61 +158,53 @@ describe('ユーザー', () => {
}); });
}; };
let root: User; let root: misskey.entities.SignupResponse;
let alice: User; let alice: misskey.entities.SignupResponse;
let aliceNote: misskey.entities.Note; let aliceNote: misskey.entities.Note;
let alicePage: misskey.entities.Page;
let aliceList: misskey.entities.UserList;
let bob: User; let bob: misskey.entities.SignupResponse;
let bobNote: misskey.entities.Note;
let carol: User; // NOTE: これがないと落ちるbob の updatedAt が null になってしまうため?)
let dave: User; let bobNote: misskey.entities.Note; // eslint-disable-line @typescript-eslint/no-unused-vars
let ellen: User;
let frank: User;
let usersReplying: User[]; let carol: misskey.entities.SignupResponse;
let userNoNote: User; let usersReplying: misskey.entities.SignupResponse[];
let userNotExplorable: User;
let userLocking: User; let userNoNote: misskey.entities.SignupResponse;
let userAdmin: User; let userNotExplorable: misskey.entities.SignupResponse;
let roleAdmin: any; let userLocking: misskey.entities.SignupResponse;
let userModerator: User; let userAdmin: misskey.entities.SignupResponse;
let roleModerator: any; let roleAdmin: misskey.entities.Role;
let userRolePublic: User; let userModerator: misskey.entities.SignupResponse;
let rolePublic: any; let roleModerator: misskey.entities.Role;
let userRoleBadge: User; let userRolePublic: misskey.entities.SignupResponse;
let roleBadge: any; let rolePublic: misskey.entities.Role;
let userSilenced: User; let userRoleBadge: misskey.entities.SignupResponse;
let roleSilenced: any; let roleBadge: misskey.entities.Role;
let userSuspended: User; let userSilenced: misskey.entities.SignupResponse;
let userDeletedBySelf: User; let roleSilenced: misskey.entities.Role;
let userDeletedByAdmin: User; let userSuspended: misskey.entities.SignupResponse;
let userFollowingAlice: User; let userDeletedBySelf: misskey.entities.SignupResponse;
let userFollowedByAlice: User; let userDeletedByAdmin: misskey.entities.SignupResponse;
let userBlockingAlice: User; let userFollowingAlice: misskey.entities.SignupResponse;
let userBlockedByAlice: User; let userFollowedByAlice: misskey.entities.SignupResponse;
let userMutingAlice: User; let userBlockingAlice: misskey.entities.SignupResponse;
let userMutedByAlice: User; let userBlockedByAlice: misskey.entities.SignupResponse;
let userRnMutingAlice: User; let userMutingAlice: misskey.entities.SignupResponse;
let userRnMutedByAlice: User; let userMutedByAlice: misskey.entities.SignupResponse;
let userFollowRequesting: User; let userRnMutingAlice: misskey.entities.SignupResponse;
let userFollowRequested: User; let userRnMutedByAlice: misskey.entities.SignupResponse;
let userFollowRequesting: misskey.entities.SignupResponse;
let userFollowRequested: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
root = await signup({ username: 'root' }); root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
aliceNote = await post(alice, { text: 'test' }) as any; aliceNote = await post(alice, { text: 'test' });
alicePage = await page(alice);
aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
bobNote = await post(bob, { text: 'test' }) as any; bobNote = await post(bob, { text: 'test' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
ellen = await signup({ username: 'ellen' });
frank = await signup({ username: 'frank' });
// @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする // @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする
usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => {
@ -241,7 +215,7 @@ describe('ユーザー', () => {
} }
return (await acc).concat(u); return (await acc).concat(u);
}, Promise.resolve([] as User[])); }, Promise.resolve([] as misskey.entities.SignupResponse[]));
userNoNote = await signup({ username: 'userNoNote' }); userNoNote = await signup({ username: 'userNoNote' });
userNotExplorable = await signup({ username: 'userNotExplorable' }); userNotExplorable = await signup({ username: 'userNotExplorable' });
@ -309,7 +283,7 @@ describe('ユーザー', () => {
beforeEach(async () => { beforeEach(async () => {
alice = { alice = {
...alice, ...alice,
...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any, ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }),
}; };
aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice }); aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice });
}); });
@ -322,7 +296,7 @@ describe('ユーザー', () => {
endpoint: 'signup', endpoint: 'signup',
parameters: { username: 'zoe', password: 'password' }, parameters: { username: 'zoe', password: 'password' },
user: undefined, user: undefined,
}) as unknown as User; // BUG MeDetailedに足りないキーがある }) as unknown as misskey.entities.SignupResponse; // BUG MeDetailedに足りないキーがある
// signupの時はtokenが含まれる特別なMeDetailedが返ってくる // signupの時はtokenが含まれる特別なMeDetailedが返ってくる
assert.match(response.token, /[a-zA-Z0-9]{16}/); assert.match(response.token, /[a-zA-Z0-9]{16}/);
@ -332,7 +306,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.name, null); assert.strictEqual(response.name, null);
assert.strictEqual(response.username, 'zoe'); assert.strictEqual(response.username, 'zoe');
assert.strictEqual(response.host, null); assert.strictEqual(response.host, null);
assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); response.avatarUrl && assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.strictEqual(response.avatarBlurhash, null); assert.strictEqual(response.avatarBlurhash, null);
assert.deepStrictEqual(response.avatarDecorations, []); assert.deepStrictEqual(response.avatarDecorations, []);
assert.strictEqual(response.isBot, false); assert.strictEqual(response.isBot, false);
@ -407,6 +381,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedWords, []);
assert.deepStrictEqual(response.mutedInstances, []); assert.deepStrictEqual(response.mutedInstances, []);
// @ts-expect-error 後方互換のため
assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.notificationRecieveConfig, {}); assert.deepStrictEqual(response.notificationRecieveConfig, {});
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
@ -436,68 +411,66 @@ describe('ユーザー', () => {
//#region 自分の情報の更新(i/update) //#region 自分の情報の更新(i/update)
test.each([ test.each([
{ parameters: (): object => ({ name: null }) }, { parameters: () => ({ name: null }) },
{ parameters: (): object => ({ name: 'x'.repeat(50) }) }, { parameters: () => ({ name: 'x'.repeat(50) }) },
{ parameters: (): object => ({ name: 'x' }) }, { parameters: () => ({ name: 'x' }) },
{ parameters: (): object => ({ name: 'My name' }) }, { parameters: () => ({ name: 'My name' }) },
{ parameters: (): object => ({ description: null }) }, { parameters: () => ({ description: null }) },
{ parameters: (): object => ({ description: 'x'.repeat(1500) }) }, { parameters: () => ({ description: 'x'.repeat(1500) }) },
{ parameters: (): object => ({ description: 'x' }) }, { parameters: () => ({ description: 'x' }) },
{ parameters: (): object => ({ description: 'My description' }) }, { parameters: () => ({ description: 'My description' }) },
{ parameters: (): object => ({ location: null }) }, { parameters: () => ({ location: null }) },
{ parameters: (): object => ({ location: 'x'.repeat(50) }) }, { parameters: () => ({ location: 'x'.repeat(50) }) },
{ parameters: (): object => ({ location: 'x' }) }, { parameters: () => ({ location: 'x' }) },
{ parameters: (): object => ({ location: 'My location' }) }, { parameters: () => ({ location: 'My location' }) },
{ parameters: (): object => ({ birthday: '0000-00-00' }) }, { parameters: () => ({ birthday: '0000-00-00' }) },
{ parameters: (): object => ({ birthday: '9999-99-99' }) }, { parameters: () => ({ birthday: '9999-99-99' }) },
{ parameters: (): object => ({ lang: 'en-US' }) }, { parameters: () => ({ lang: 'en-US' as const }) },
{ parameters: (): object => ({ fields: [] }) }, { parameters: () => ({ fields: [] }) },
{ parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) }, { parameters: () => ({ fields: [{ name: 'x', value: 'x' }] }) },
{ parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない { parameters: () => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
{ parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, { parameters: () => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
{ parameters: (): object => ({ isLocked: true }) }, { parameters: () => ({ isLocked: true }) },
{ parameters: (): object => ({ isLocked: false }) }, { parameters: () => ({ isLocked: false }) },
{ parameters: (): object => ({ isExplorable: false }) }, { parameters: () => ({ isExplorable: false }) },
{ parameters: (): object => ({ isExplorable: true }) }, { parameters: () => ({ isExplorable: true }) },
{ parameters: (): object => ({ hideOnlineStatus: true }) }, { parameters: () => ({ hideOnlineStatus: true }) },
{ parameters: (): object => ({ hideOnlineStatus: false }) }, { parameters: () => ({ hideOnlineStatus: false }) },
{ parameters: (): object => ({ publicReactions: false }) }, { parameters: () => ({ publicReactions: false }) },
{ parameters: (): object => ({ publicReactions: true }) }, { parameters: () => ({ publicReactions: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: true }) }, { parameters: () => ({ autoAcceptFollowed: true }) },
{ parameters: (): object => ({ autoAcceptFollowed: false }) }, { parameters: () => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) }, { parameters: () => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) }, { parameters: () => ({ noCrawle: false }) },
{ parameters: (): object => ({ preventAiLearning: false }) }, { parameters: () => ({ preventAiLearning: false }) },
{ parameters: (): object => ({ preventAiLearning: true }) }, { parameters: () => ({ preventAiLearning: true }) },
{ parameters: (): object => ({ isBot: true }) }, { parameters: () => ({ isBot: true }) },
{ parameters: (): object => ({ isBot: false }) }, { parameters: () => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) }, { parameters: () => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) }, { parameters: () => ({ isCat: false }) },
{ parameters: (): object => ({ speakAsCat: true }) }, { parameters: () => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ speakAsCat: false }) }, { parameters: () => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) }, { parameters: () => ({ receiveAnnouncementEmail: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) }, { parameters: () => ({ receiveAnnouncementEmail: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) }, { parameters: () => ({ alwaysMarkNsfw: true }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: false }) }, { parameters: () => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ alwaysMarkNsfw: true }) }, { parameters: () => ({ autoSensitive: true }) },
{ parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: () => ({ autoSensitive: false }) },
{ parameters: (): object => ({ autoSensitive: true }) }, { parameters: () => ({ followingVisibility: 'private' as const }) },
{ parameters: (): object => ({ autoSensitive: false }) }, { parameters: () => ({ followingVisibility: 'followers' as const }) },
{ parameters: (): object => ({ followingVisibility: 'private' }) }, { parameters: () => ({ followingVisibility: 'public' as const }) },
{ parameters: (): object => ({ followingVisibility: 'followers' }) }, { parameters: () => ({ followersVisibility: 'private' as const }) },
{ parameters: (): object => ({ followingVisibility: 'public' }) }, { parameters: () => ({ followersVisibility: 'followers' as const }) },
{ parameters: (): object => ({ followersVisibility: 'private' }) }, { parameters: () => ({ followersVisibility: 'public' as const }) },
{ parameters: (): object => ({ followersVisibility: 'followers' }) }, { parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ followersVisibility: 'public' }) }, { parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, { parameters: () => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedWords: [] }) }, { parameters: () => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, { parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: (): object => ({ mutedInstances: [] }) }, { parameters: () => ({ notificationRecieveConfig: {} }) },
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) }, { parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ notificationRecieveConfig: {} }) }, { parameters: () => ({ emailNotificationTypes: [] }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
] as const)('を書き換えることができる($#)', async ({ parameters }) => { ] as const)('を書き換えることができる($#)', async ({ parameters }) => {
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice }); const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice });
const expected = { ...meDetailed(alice, true), ...parameters() }; const expected = { ...meDetailed(alice, true), ...parameters() };
@ -506,13 +479,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Avatar)', async () => { test('を書き換えることができる(Avatar)', async () => {
const aliceFile = (await uploadFile(alice)).body; const aliceFile = (await uploadFile(alice)).body;
const parameters = { avatarId: aliceFile.id }; const parameters = { avatarId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/);
const expected = { const expected = {
...meDetailed(alice, true), ...meDetailed(alice, true),
avatarId: aliceFile.id, avatarId: aliceFile!.id,
avatarBlurhash: response.avatarBlurhash, avatarBlurhash: response.avatarBlurhash,
avatarUrl: response.avatarUrl, avatarUrl: response.avatarUrl,
}; };
@ -531,13 +504,13 @@ describe('ユーザー', () => {
test('を書き換えることができる(Banner)', async () => { test('を書き換えることができる(Banner)', async () => {
const aliceFile = (await uploadFile(alice)).body; const aliceFile = (await uploadFile(alice)).body;
const parameters = { bannerId: aliceFile.id }; const parameters = { bannerId: aliceFile!.id };
const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/);
const expected = { const expected = {
...meDetailed(alice, true), ...meDetailed(alice, true),
bannerId: aliceFile.id, bannerId: aliceFile!.id,
bannerBlurhash: response.bannerBlurhash, bannerBlurhash: response.bannerBlurhash,
bannerUrl: response.bannerUrl, bannerUrl: response.bannerUrl,
}; };
@ -612,13 +585,13 @@ describe('ユーザー', () => {
//#region ユーザー(users) //#region ユーザー(users)
test.each([ test.each([
{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id }, { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: misskey.entities.UserLite): string => u.id },
{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をリスト形式で取得することができる($label', async ({ parameters, selector }) => { ] as const)('をリスト形式で取得することができる($label', async ({ parameters, selector }) => {
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -631,15 +604,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true }, { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: () => userNotExplorable, excluded: true },
{ label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true }, { label: 'ミュートユーザーが含まれない', user: () => userMutedByAlice, excluded: true },
{ label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true }, { label: 'ブロックされているユーザーが含まれない', user: () => userBlockedByAlice, excluded: true },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => { ] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
const parameters = { limit: 100 }; const parameters = { limit: 100 };
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
@ -653,39 +626,44 @@ describe('ユーザー', () => {
//#region ユーザー情報(users/show) //#region ユーザー情報(users/show)
test.each([ test.each([
{ label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed }, { label: 'ID指定で自分自身を', parameters: () => ({ userId: alice.id }), user: () => alice, type: meDetailed },
{ label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations }, { label: 'ID指定で他人を', parameters: () => ({ userId: alice.id }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, { label: 'ID指定かつ未認証', parameters: () => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
{ label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed }, { label: '@指定で自分自身を', parameters: () => ({ username: alice.username }), user: () => alice, type: meDetailed },
{ label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations }, { label: '@指定で他人を', parameters: () => ({ username: alice.username }), user: () => bob, type: userDetailedNotMeWithRelations },
{ label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, { label: '@指定かつ未認証', parameters: () => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
] as const)('を取得することができる($label', async ({ parameters, user, type }) => { ] as const)('を取得することができる($label', async ({ parameters, user, type }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() }); const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() });
const expected = type(alice); const expected = type(alice);
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin }, { label: 'Administratorになっている', user: () => userAdmin, me: () => userAdmin, selector: (user: misskey.entities.MeDetailed) => user.isAdmin },
{ label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined }, // @ts-expect-error UserDetailedNotMe doesn't include isAdmin
{ label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator }, { label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
{ label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined }, { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
{ label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced }, // @ts-expect-error UserDetailedNotMe doesn't include isModerator
//{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended }, { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
{ label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted }, { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
{ label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, // FIXME: 落ちる
{ label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted }, //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, { label: '削除済みになっている', user: () => userDeletedBySelf, me: () => userDeletedBySelf, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
{ label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing }, // @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed }, { label: '自分以外から見たときは削除済みか判定できない', user: () => userDeletedBySelf, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking }, { label: '削除済み(byAdmin)になっている', user: () => userDeletedByAdmin, me: () => userDeletedByAdmin, selector: (user: misskey.entities.MeDetailed) => user.isDeleted },
{ label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked }, // @ts-expect-error UserDetailedNotMe doesn't include isDeleted
{ label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted }, { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: () => userDeletedByAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isDeleted, expected: () => undefined },
{ label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted }, { label: 'フォロー中になっている', user: () => userFollowedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowing },
{ label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou }, { label: 'フォローされている', user: () => userFollowingAlice, selector: (user: misskey.entities.UserDetailed) => user.isFollowed },
{ label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou }, { label: 'ブロック中になっている', user: () => userBlockedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocking },
{ label: 'ブロックされている', user: () => userBlockingAlice, selector: (user: misskey.entities.UserDetailed) => user.isBlocked },
{ label: 'ミュート中になっている', user: () => userMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isMuted },
{ label: 'リノートミュート中になっている', user: () => userRnMutedByAlice, selector: (user: misskey.entities.UserDetailed) => user.isRenoteMuted },
{ label: 'フォローリクエスト中になっている', user: () => userFollowRequested, me: () => userFollowRequesting, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestFromYou },
{ label: 'フォローリクエストされている', user: () => userFollowRequesting, me: () => userFollowRequested, selector: (user: misskey.entities.UserDetailed) => user.hasPendingFollowRequestToYou },
] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => { ] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice }); const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice });
assert.strictEqual(selector(response), (expected ?? ((): true => true))()); assert.strictEqual(selector(response as any), (expected ?? ((): true => true))());
}); });
test('を取得することができ、Publicなロールがセットされていること', async () => { test('を取得することができ、Publicなロールがセットされていること', async () => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice }); const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice });
@ -727,17 +705,18 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root }, { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: () => userSuspended, me: () => root },
// BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる
//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: () => userSuspended, me: () => bob, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
// @ts-expect-error excluded は上でコメントアウトされているので
] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => {
const parameters = { userIds: [user().id] }; const parameters = { userIds: [user().id] };
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice });
@ -762,15 +741,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
const parameters = { query: user().username, limit: 1 }; const parameters = { query: user().username, limit: 1 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
@ -784,30 +763,30 @@ describe('ユーザー', () => {
//#region ID指定検索(users/search-by-username-and-host) //#region ID指定検索(users/search-by-username-and-host)
test.each([ test.each([
{ label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, { label: '自分', parameters: { username: 'alice' }, user: () => [alice] },
{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: () => [alice] },
{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: () => [userFollowedByAlice] },
{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] }, { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: () => [] },
{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] }, { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: () => [bob] },
{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] }, { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: () => [bob] },
{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] }, { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: () => [bob] },
{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] }, { label: 'ローカル', parameters: { host: null, limit: 1 }, user: () => [userFollowedByAlice] },
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] }, { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: () => [userFollowedByAlice] },
])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => { ])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = await Promise.all(user().map(u => show(u.id, alice))); const expected = await Promise.all(user().map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => { ] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
const parameters = { username: user().username }; const parameters = { username: user().username };
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
@ -829,15 +808,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true }, { label: 'ブロックしてきているユーザーが含まれない', user: () => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
//{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, //{ label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => { ] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0]; const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0];
await post(alice, { text: `@${user().username} test`, replyId: replyTo.id }); await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
@ -851,12 +830,12 @@ describe('ユーザー', () => {
//#region ハッシュタグ(hashtags/users) //#region ハッシュタグ(hashtags/users)
test.each([ test.each([
{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.followersCount) },
{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => u.createdAt },
{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: misskey.entities.UserDetailedNotMe): string => String(u.updatedAt) },
] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => { ] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => {
const hashtag = 'test_hashtag'; const hashtag = 'test_hashtag';
await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice }); await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
@ -870,15 +849,15 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.each([ test.each([
{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: () => userNotExplorable },
{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, { label: 'ミュートユーザーが含まれる', user: () => userMutedByAlice },
{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, { label: 'ブロックされているユーザーが含まれる', user: () => userBlockedByAlice },
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, { label: 'ブロックしてきているユーザーが含まれる', user: () => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking }, { label: '承認制ユーザーが含まれる', user: () => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サイレンスユーザーが含まれる', user: () => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: 'サスペンドユーザーが含まれない', user: () => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, { label: '削除済ユーザーが含まれる', user: () => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, { label: '削除済(byAdmin)ユーザーが含まれる', user: () => userDeletedByAdmin },
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => { ] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => {
const hashtag = `user_test${user().username}`; const hashtag = `user_test${user().username}`;
if (user() !== userSuspended) { if (user() !== userSuspended) {

7
packages/backend/test/global.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FIXME = any;

View file

@ -4,10 +4,10 @@
*/ */
import Ajv from 'ajv'; import Ajv from 'ajv';
import { Schema } from '@/misc/schema'; import { Schema } from '@/misc/json-schema.js';
export const getValidator = (paramDef: Schema) => { export const getValidator = (paramDef: Schema) => {
const ajv = new Ajv({ const ajv = new Ajv.default({
useDefaults: true, useDefaults: true,
}); });
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);

View file

@ -5,7 +5,7 @@
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUnusedParameters": false, "noUnusedParameters": false,
"noUnusedLocals": true, "noUnusedLocals": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"declaration": false, "declaration": false,
"sourceMap": true, "sourceMap": true,
@ -18,6 +18,7 @@
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"skipLibCheck": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"resolveJsonModule": true, "resolveJsonModule": true,

View file

@ -51,7 +51,7 @@ describe('AnnouncementService', () => {
function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) { function createAnnouncement(data: Partial<MiAnnouncement & { createdAt: Date }> = {}) {
return announcementsRepository.insert({ return announcementsRepository.insert({
id: genAidx(data.createdAt ?? new Date()), id: genAidx(data.createdAt?.getTime() ?? Date.now()),
updatedAt: null, updatedAt: null,
title: 'Title', title: 'Title',
text: 'Text', text: 'Text',

View file

@ -19,8 +19,8 @@ import { DI } from '@/di-symbols.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
function mockRedis() { function mockRedis() {
const hash = {}; const hash = {} as any;
const set = jest.fn((key, value) => { const set = jest.fn((key: string, value) => {
const ret = hash[key]; const ret = hash[key];
hash[key] = value; hash[key] = value;
return ret; return ret;
@ -56,12 +56,13 @@ describe('FetchInstanceMetadataService', () => {
} else if (token === DI.redis) { } else if (token === DI.redis) {
return mockRedis; return mockRedis;
} }
return null;
}) })
.compile(); .compile();
app.enableShutdownHooks(); app.enableShutdownHooks();
fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService); fetchInstanceMetadataService = app.get<FetchInstanceMetadataService>(FetchInstanceMetadataService) as jest.Mocked<FetchInstanceMetadataService>;
federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>; federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService) as jest.Mocked<FederatedInstanceService>;
redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>; redisClient = app.get<Redis>(DI.redis) as jest.Mocked<Redis>;
httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>; httpRequestService = app.get<HttpRequestService>(HttpRequestService) as jest.Mocked<HttpRequestService>;
@ -74,11 +75,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => { test('Lock and update', async () => {
redisClient.set = mockRedis(); redisClient.set = mockRedis();
const now = Date.now(); const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } }); federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); }); httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -88,11 +90,12 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and don\'t update', async () => { test('Lock and don\'t update', async () => {
redisClient.set = mockRedis(); redisClient.set = mockRedis();
const now = Date.now(); const now = Date.now();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } }); federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); }); httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' });
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
@ -101,15 +104,33 @@ describe('FetchInstanceMetadataService', () => {
test('Do nothing when lock not acquired', async () => { test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis(); redisClient.set = mockRedis();
federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } }); const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); }); httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.tryLock('example.com');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(2); expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0); expect(unlockSpy).toHaveBeenCalledTimes(0);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
}); });
test('Do when lock not acquired but forced', async () => {
redisClient.set = mockRedis();
const now = Date.now();
federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
expect(unlockSpy).toHaveBeenCalledTimes(1);
expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
}); });

View file

@ -90,7 +90,8 @@ describe('RelayService', () => {
expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver).toHaveBeenCalled();
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo'); expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo');
expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow'); expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object');
expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow');
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');

View file

@ -228,11 +228,14 @@ describe('RoleService', () => {
}, },
target: 'conditional', target: 'conditional',
condFormula: { condFormula: {
id: '232a4221-9816-49a6-a967-ae0fac52ec5e',
type: 'and', type: 'and',
values: [{ values: [{
id: '2a37ef43-2d93-4c4d-87f6-f2fdb7d9b530',
type: 'followersMoreThanOrEq', type: 'followersMoreThanOrEq',
value: 10, value: 10,
}, { }, {
id: '1bd67839-b126-4f92-bad0-4e285dab453b',
type: 'createdMoreThan', type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7, sec: 60 * 60 * 24 * 7,
}], }],

View file

@ -9,11 +9,10 @@ import { basename, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws'; import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit } from 'node-fetch'; import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { Packed } from '@/misc/json-schema.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { entities } from '../src/postgres.js'; import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js'; import { loadConfig } from '../src/config.js';
@ -21,7 +20,7 @@ import type * as misskey from 'misskey-js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
interface UserToken { export interface UserToken {
token: string; token: string;
bearer?: boolean; bearer?: boolean;
} }
@ -35,20 +34,15 @@ export const cookie = (me: UserToken): string => {
return `token=${me.token};`; return `token=${me.token};`;
}; };
export const api = async (endpoint: string, params: any, me?: UserToken) => { export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = {
const normalized = endpoint.replace(/^\//, ''); endpoint: E,
return await request(`api/${normalized}`, params, me); parameters: P,
};
export type ApiRequest = {
endpoint: string,
parameters: object,
user: UserToken | undefined, user: UserToken | undefined,
}; };
export const successfulApiCall = async <T, >(request: ApiRequest, assertion: { export const successfulApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status?: number, status?: number,
} = {}): Promise<T> => { } = {}): Promise<misskey.api.SwitchCaseResponseType<E, P>> => {
const { endpoint, parameters, user } = request; const { endpoint, parameters, user } = request;
const res = await api(endpoint, parameters, user); const res = await api(endpoint, parameters, user);
const status = assertion.status ?? (res.body == null ? 204 : 200); const status = assertion.status ?? (res.body == null ? 204 : 200);
@ -56,7 +50,7 @@ export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body; return res.body;
}; };
export const failedApiCall = async <T, >(request: ApiRequest, assertion: { export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: {
status: number, status: number,
code: string, code: string,
id: string id: string
@ -70,7 +64,7 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body; return res.body;
}; };
const request = async (path: string, params: any, me?: UserToken): Promise<{ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{
status: number, status: number,
headers: Headers, headers: Headers,
body: any body: any
@ -86,7 +80,7 @@ const request = async (path: string, params: any, me?: UserToken): Promise<{
bodyAuth.i = me.token; bodyAuth.i = me.token;
} }
const res = await relativeFetch(path, { const res = await relativeFetch(`api/${path}`, {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify(Object.assign(bodyAuth, params)), body: JSON.stringify(Object.assign(bodyAuth, params)),
@ -141,7 +135,7 @@ export const signup = async (params?: Partial<misskey.Endpoints['signup']['req']
return res.body; return res.body;
}; };
export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => { export const post = async (user: UserToken, params: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = params; const q = params;
const res = await api('notes/create', q, user); const res = await api('notes/create', q, user);
@ -159,8 +153,8 @@ export const createAppToken = async (user: UserToken, permissions: (typeof missk
}; };
// 非公開ートをAPI越しに見たときのート NoteEntityService.ts // 非公開ートをAPI越しに見たときのート NoteEntityService.ts
export const hiddenNote = (note: any): any => { export const hiddenNote = (note: misskey.entities.Note): misskey.entities.Note => {
const temp = { const temp: misskey.entities.Note = {
...note, ...note,
fileIds: [], fileIds: [],
files: [], files: [],
@ -173,21 +167,22 @@ export const hiddenNote = (note: any): any => {
return temp; return temp;
}; };
export const react = async (user: UserToken, note: any, reaction: string): Promise<any> => { export const react = async (user: UserToken, note: misskey.entities.Note, reaction: string): Promise<void> => {
await api('notes/reactions/create', { await api('notes/reactions/create', {
noteId: note.id, noteId: note.id,
reaction: reaction, reaction: reaction,
}, user); }, user);
}; };
export const userList = async (user: UserToken, userList: any = {}): Promise<any> => { export const userList = async (user: UserToken, userList: Partial<misskey.entities.UserList> = {}): Promise<misskey.entities.UserList> => {
const res = await api('users/lists/create', { const res = await api('users/lists/create', {
name: 'test', name: 'test',
...userList,
}, user); }, user);
return res.body; return res.body;
}; };
export const page = async (user: UserToken, page: any = {}): Promise<any> => { export const page = async (user: UserToken, page: Partial<misskey.entities.Page> = {}): Promise<misskey.entities.Page> => {
const res = await api('pages/create', { const res = await api('pages/create', {
alignCenter: false, alignCenter: false,
content: [ content: [
@ -198,7 +193,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
}, },
], ],
eyeCatchingImageId: null, eyeCatchingImageId: null,
font: 'sans-serif', font: 'sans-serif' as any,
hideTitleWhenPinned: false, hideTitleWhenPinned: false,
name: '1678594845072', name: '1678594845072',
script: '', script: '',
@ -210,7 +205,7 @@ export const page = async (user: UserToken, page: any = {}): Promise<any> => {
return res.body; return res.body;
}; };
export const play = async (user: UserToken, play: any = {}): Promise<any> => { export const play = async (user: UserToken, play: Partial<misskey.entities.Flash> = {}): Promise<misskey.entities.Flash> => {
const res = await api('flash/create', { const res = await api('flash/create', {
permissions: [], permissions: [],
script: 'test', script: 'test',
@ -221,7 +216,7 @@ export const play = async (user: UserToken, play: any = {}): Promise<any> => {
return res.body; return res.body;
}; };
export const clip = async (user: UserToken, clip: any = {}): Promise<any> => { export const clip = async (user: UserToken, clip: Partial<misskey.entities.Clip> = {}): Promise<misskey.entities.Clip> => {
const res = await api('clips/create', { const res = await api('clips/create', {
description: null, description: null,
isPublic: true, isPublic: true,
@ -231,18 +226,18 @@ export const clip = async (user: UserToken, clip: any = {}): Promise<any> => {
return res.body; return res.body;
}; };
export const galleryPost = async (user: UserToken, channel: any = {}): Promise<any> => { export const galleryPost = async (user: UserToken, galleryPost: Partial<misskey.entities.GalleryPost> = {}): Promise<misskey.entities.GalleryPost> => {
const res = await api('gallery/posts/create', { const res = await api('gallery/posts/create', {
description: null, description: null,
fileIds: [], fileIds: [],
isSensitive: false, isSensitive: false,
title: 'test', title: 'test',
...channel, ...galleryPost,
}, user); }, user);
return res.body; return res.body;
}; };
export const channel = async (user: UserToken, channel: any = {}): Promise<any> => { export const channel = async (user: UserToken, channel: Partial<misskey.entities.Channel> = {}): Promise<misskey.entities.Channel> => {
const res = await api('channels/create', { const res = await api('channels/create', {
bannerId: null, bannerId: null,
description: null, description: null,
@ -252,7 +247,7 @@ export const channel = async (user: UserToken, channel: any = {}): Promise<any>
return res.body; return res.body;
}; };
export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise<any> => { export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
const res = await api('admin/roles/create', { const res = await api('admin/roles/create', {
asBadge: false, asBadge: false,
canEditMembersByModerator: false, canEditMembersByModerator: false,
@ -260,7 +255,7 @@ export const role = async (user: UserToken, role: any = {}, policies: any = {}):
condFormula: { condFormula: {
id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85', id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
type: 'isRemote', type: 'isRemote',
}, } as any,
description: '', description: '',
displayOrder: 0, displayOrder: 0,
iconUrl: null, iconUrl: null,
@ -298,7 +293,7 @@ interface UploadOptions {
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
status: number, status: number,
headers: Headers, headers: Headers,
body: misskey.Endpoints['drive/files/create']['res'] | null body: misskey.entities.DriveFile | null
}> => { }> => {
const absPath = path == null const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url) ? new URL('resources/Lenna.jpg', import.meta.url)
@ -335,14 +330,14 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
}; };
}; };
export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'DriveFile'>> => { export const uploadUrl = async (user: UserToken, url: string): Promise<misskey.entities.DriveFile> => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
user, user,
'main', 'main',
(msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker,
(msg) => msg.body.file as Packed<'DriveFile'>, (msg) => msg.body.file,
60 * 1000, 60 * 1000,
); );

View file

@ -189,14 +189,26 @@ export async function mainBoot() {
if ($i.followersCount >= 500) claimAchievement('followers500'); if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000'); if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { const createdAt = new Date($i.createdAt);
const createdAtThreeYearsLater = new Date($i.createdAt);
createdAtThreeYearsLater.setFullYear(createdAtThreeYearsLater.getFullYear() + 3);
if (now >= createdAtThreeYearsLater) {
claimAchievement('passedSinceAccountCreated3');
claimAchievement('passedSinceAccountCreated2');
claimAchievement('passedSinceAccountCreated1');
} else {
const createdAtTwoYearsLater = new Date($i.createdAt);
createdAtTwoYearsLater.setFullYear(createdAtTwoYearsLater.getFullYear() + 2);
if (now >= createdAtTwoYearsLater) {
claimAchievement('passedSinceAccountCreated2');
claimAchievement('passedSinceAccountCreated1');
} else {
const createdAtOneYearLater = new Date($i.createdAt);
createdAtOneYearLater.setFullYear(createdAtOneYearLater.getFullYear() + 1);
if (now >= createdAtOneYearLater) {
claimAchievement('passedSinceAccountCreated1'); claimAchievement('passedSinceAccountCreated1');
} }
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
} }
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
} }
if (claimedAchievements.length >= 30) { if (claimedAchievements.length >= 30) {
@ -231,7 +243,7 @@ export async function mainBoot() {
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
} }

View file

@ -69,7 +69,7 @@ import * as os from '@/os.js';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js'; import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{ const props = defineProps<{
audio: Misskey.entities.DriveFile; audio: Misskey.entities.DriveFile;
@ -99,8 +99,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) { if (iAmModerator) {
menu.push({ menu.push({
type: 'divider',
}, {
text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
danger: true, danger: true,
@ -108,6 +106,17 @@ function showMenu(ev: MouseEvent) {
}); });
} }
if ($i?.id === props.audio.userId) {
menu.push({
type: 'divider',
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.audio.id}`,
});
}
menuShowing.value = true; menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, { os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right', align: 'right',

View file

@ -60,7 +60,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
image: Misskey.entities.DriveFile; image: Misskey.entities.DriveFile;
@ -115,6 +115,13 @@ function showMenu(ev: MouseEvent) {
action: () => { action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
}, },
}] : []), ...($i?.id === props.image.userId ? [{
type: 'divider' as const,
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.image.id}`,
}] : [])], ev.currentTarget ?? ev.target); }] : [])], ev.currentTarget ?? ev.target);
} }

View file

@ -97,7 +97,7 @@ import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
import hasAudio from '@/scripts/media-has-audio.js'; import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{ const props = defineProps<{
video: Misskey.entities.DriveFile; video: Misskey.entities.DriveFile;
@ -125,8 +125,6 @@ function showMenu(ev: MouseEvent) {
if (iAmModerator) { if (iAmModerator) {
menu.push({ menu.push({
type: 'divider',
}, {
text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg',
danger: true, danger: true,
@ -134,6 +132,17 @@ function showMenu(ev: MouseEvent) {
}); });
} }
if ($i?.id === props.video.userId) {
menu.push({
type: 'divider',
}, {
type: 'link' as const,
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.video.id}`,
});
}
menuShowing.value = true; menuShowing.value = true;
os.popupMenu(menu, ev.currentTarget ?? ev.target, { os.popupMenu(menu, ev.currentTarget ?? ev.target, {
align: 'right', align: 'right',

View file

@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
<MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more> <template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template> </template>
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer"> <footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else :class="$style.footerButton" class="_button" disabled> <button v-else :class="$style.footerButton" class="_button" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -135,12 +135,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -195,6 +195,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -626,6 +627,14 @@ function undoRenote(note) : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
if (props.mock) { if (props.mock) {
return; return;

View file

@ -113,10 +113,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/> <MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA> </MkA>
</div> </div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<button class="_button" :class="$style.noteFooterButton" @click="reply()"> <button class="_button" :class="$style.noteFooterButton" @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote() : boostVisibility()" @mousedown="renoted ? undoRenote() : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled> <button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -144,12 +144,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -236,6 +236,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import number from '@/filters/number.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
@ -594,11 +595,11 @@ function like(): void {
} }
} }
function undoReact(note): void { function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = note.myReaction; const oldReaction = targetNote.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
misskeyApi('notes/reactions/delete', { misskeyApi('notes/reactions/delete', {
noteId: note.id, noteId: targetNote.id,
}); });
} }
@ -619,6 +620,14 @@ function undoRenote() : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement): boolean => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;

View file

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
@ -60,6 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span> <span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
@ -211,6 +213,7 @@ const rejectFollowRequest = () => {
} }
.icon_reactionGroup, .icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup { .icon_renoteGroup {
display: grid; display: grid;
align-items: center; align-items: center;
@ -223,11 +226,15 @@ const rejectFollowRequest = () => {
} }
.icon_reactionGroup { .icon_reactionGroup {
background: #e99a0b; background: var(--eventReaction);
}
.icon_reactionGroupHeart {
background: var(--eventReactionHeart);
} }
.icon_renoteGroup { .icon_renoteGroup {
background: #36d298; background: var(--eventRenote);
} }
.icon_app { .icon_app {
@ -256,49 +263,49 @@ const rejectFollowRequest = () => {
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest { .t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
padding: 3px; padding: 3px;
background: #36aed2; background: var(--eventFollow);
pointer-events: none; pointer-events: none;
} }
.t_renote { .t_renote {
padding: 3px; padding: 3px;
background: #36d298; background: var(--eventRenote);
pointer-events: none; pointer-events: none;
} }
.t_quote { .t_quote {
padding: 3px; padding: 3px;
background: #36d298; background: var(--eventRenote);
pointer-events: none; pointer-events: none;
} }
.t_reply { .t_reply {
padding: 3px; padding: 3px;
background: #007aff; background: var(--eventReply);
pointer-events: none; pointer-events: none;
} }
.t_mention { .t_mention {
padding: 3px; padding: 3px;
background: #88a6b7; background: var(--eventOther);
pointer-events: none; pointer-events: none;
} }
.t_pollEnded { .t_pollEnded {
padding: 3px; padding: 3px;
background: #88a6b7; background: var(--eventOther);
pointer-events: none; pointer-events: none;
} }
.t_achievementEarned { .t_achievementEarned {
padding: 3px; padding: 3px;
background: #cb9a11; background: var(--eventAchievement);
pointer-events: none; pointer-events: none;
} }
.t_roleAssigned { .t_roleAssigned {
padding: 3px; padding: 3px;
background: #88a6b7; background: var(--eventOther);
pointer-events: none; pointer-events: none;
} }

View file

@ -63,6 +63,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null, reactionAcceptance: null,
renoteCount: 0, renoteCount: 0,
repliesCount: 1, repliesCount: 1,
reactionCount: 0,
reactions: {}, reactions: {},
reactionEmojis: {}, reactionEmojis: {},
fileIds: [], fileIds: [],

View file

@ -68,6 +68,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null, reactionAcceptance: null,
renoteCount: 0, renoteCount: 0,
repliesCount: 1, repliesCount: 1,
reactionCount: 0,
reactions: {}, reactions: {},
reactionEmojis: {}, reactionEmojis: {},
fileIds: [], fileIds: [],

View file

@ -58,6 +58,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
reactionAcceptance: null, reactionAcceptance: null,
renoteCount: 0, renoteCount: 0,
repliesCount: 1, repliesCount: 1,
reactionCount: 0,
reactions: {}, reactions: {},
reactionEmojis: {}, reactionEmojis: {},
fileIds: ['0000000002'], fileIds: ['0000000002'],

View file

@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
<MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more> <template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template> </template>
@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer"> <footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else :class="$style.footerButton" class="_button" disabled> <button v-else :class="$style.footerButton" class="_button" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -137,12 +137,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -196,6 +196,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
@ -627,6 +628,14 @@ function undoRenote(note) : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
if (props.mock) { if (props.mock) {
return; return;

View file

@ -120,11 +120,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="appearNote.createdAt" mode="detail" colored/> <MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA> </MkA>
</div> </div>
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
<footer :class="$style.footer"> <footer :class="$style.footer">
<button class="_button" :class="$style.noteFooterButton" @click="reply()"> <button class="_button" :class="$style.noteFooterButton" @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
</button> </button>
<button <button
v-if="canRenote" v-if="canRenote"
@ -135,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renoted ? undoRenote() : boostVisibility()" @mousedown="renoted ? undoRenote() : boostVisibility()"
> >
<i class="ph-rocket-launch ph-bold ph-lg"></i> <i class="ph-rocket-launch ph-bold ph-lg"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p> <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
</button> </button>
<button v-else class="_button" :class="$style.noteFooterButton" disabled> <button v-else class="_button" :class="$style.noteFooterButton" disabled>
<i class="ph-prohibit ph-bold ph-lg"></i> <i class="ph-prohibit ph-bold ph-lg"></i>
@ -152,12 +152,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()"> <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
<i class="ph-heart ph-bold ph-lg"></i> <i class="ph-heart ph-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i>
</button> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ph-paperclip ph-bold ph-lg"></i> <i class="ph-paperclip ph-bold ph-lg"></i>
@ -243,6 +243,7 @@ import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import number from '@/filters/number.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -603,11 +604,11 @@ function like(): void {
} }
} }
function undoReact(note): void { function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = note.myReaction; const oldReaction = targetNote.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
misskeyApi('notes/reactions/delete', { misskeyApi('notes/reactions/delete', {
noteId: note.id, noteId: targetNote.id,
}); });
} }
@ -628,6 +629,14 @@ function undoRenote() : void {
} }
} }
function toggleReact() {
if (appearNote.value.myReaction == null) {
react();
} else {
undoReact(appearNote.value);
}
}
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement): boolean => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;

View file

@ -14,10 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.form_vertical]: chosen.place === 'vertical', [$style.form_vertical]: chosen.place === 'vertical',
}]" }]"
> >
<a :href="chosen.url" target="_blank" :class="$style.link"> <component
:is="self ? 'MkA' : 'a'"
:class="$style.link"
v-bind="self ? {
to: chosen.url.substring(local.length),
} : {
href: chosen.url,
rel: 'nofollow noopener',
target: '_blank',
}"
>
<img :src="chosen.imageUrl" :class="$style.img"> <img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ph-info ph-bold ph-lg"></i></button> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ph-info ph-bold ph-lg"></i></button>
</a> </component>
</div> </div>
<div v-else :class="$style.menu"> <div v-else :class="$style.menu">
<div :class="$style.menuContainer"> <div :class="$style.menuContainer">
@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { host } from '@/config.js'; import { url as local, host } from '@/config.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -96,6 +106,9 @@ const choseAd = (): Ad | null => {
}; };
const chosen = ref(choseAd()); const chosen = ref(choseAd());
const self = computed(() => chosen.value?.url.startsWith(local));
const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void { function reduceFrequency(): void {

View file

@ -373,7 +373,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
this.currentRoute.value = res.route; this.currentRoute.value = res.route;
this.currentKey = res.route.globalCacheKey ?? key ?? path; this.currentKey = res.route.globalCacheKey ?? key ?? path;
if (emitChange) { if (emitChange && res.route.path !== '/:(*)') {
this.emit('change', { this.emit('change', {
beforePath, beforePath,
path, path,
@ -408,6 +408,9 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
if (cancel) return; if (cancel) return;
} }
const res = this.navigate(path, null); const res = this.navigate(path, null);
if (res.route.path === '/:(*)') {
location.href = path;
} else {
this.emit('push', { this.emit('push', {
beforePath, beforePath,
path: res._parsedRoute.fullPath, path: res._parsedRoute.fullPath,
@ -416,6 +419,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
key: this.currentKey, key: this.currentKey,
}); });
} }
}
public replace(path: string, key?: string | null) { public replace(path: string, key?: string | null) {
const res = this.navigate(path, key); const res = this.navigate(path, key);

View file

@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
<MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
<MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch> <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch> <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
<MkSwitch v-model="showTickerOnReplies">Show instance ticker on replies</MkSwitch> <MkSwitch v-model="showTickerOnReplies">Show instance ticker on replies</MkSwitch>
@ -328,6 +329,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline')); const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm')); const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction')); const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));

View file

@ -70,6 +70,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'animation', 'animation',
'animatedMfm', 'animatedMfm',
'advancedMfm', 'advancedMfm',
'showReactionsCount',
'loadRawImages', 'loadRawImages',
'warnMissingAltText', 'warnMissingAltText',
'imageNewTab', 'imageNewTab',

View file

@ -54,6 +54,7 @@ export function useNoteCapture(props: {
const currentCount = (note.value.reactions || {})[reaction] || 0; const currentCount = (note.value.reactions || {})[reaction] || 0;
note.value.reactions[reaction] = currentCount + 1; note.value.reactions[reaction] = currentCount + 1;
note.value.reactionCount += 1;
if ($i && (body.userId === $i.id)) { if ($i && (body.userId === $i.id)) {
note.value.myReaction = reaction; note.value.myReaction = reaction;
@ -68,6 +69,7 @@ export function useNoteCapture(props: {
const currentCount = (note.value.reactions || {})[reaction] || 0; const currentCount = (note.value.reactions || {})[reaction] || 0;
note.value.reactions[reaction] = Math.max(0, currentCount - 1); note.value.reactions[reaction] = Math.max(0, currentCount - 1);
note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction]; if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction];
if ($i && (body.userId === $i.id)) { if ($i && (body.userId === $i.id)) {

View file

@ -256,6 +256,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: true, default: true,
}, },
showReactionsCount: {
where: 'device',
default: false,
},
enableQuickAddMfmFunction: { enableQuickAddMfmFunction: {
where: 'device', where: 'device',
default: false, default: false,

View file

@ -41,6 +41,13 @@
--thread-width: 2px; --thread-width: 2px;
//--ad: rgb(255 169 0 / 10%); //--ad: rgb(255 169 0 / 10%);
--eventFollow: #36aed2;
--eventRenote: #36d298;
--eventReply: #007aff;
--eventReactionHeart: #dd2e44;
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
--eventOther: #88a6b7;
} }
html.radius-misskey { html.radius-misskey {

View file

@ -49,6 +49,9 @@ const devConfig = {
}, },
'/url': httpUrl, '/url': httpUrl,
'/proxy': httpUrl, '/proxy': httpUrl,
'/_info_card_': httpUrl,
'/bios': httpUrl,
'/cli': httpUrl,
}, },
}, },
build: { build: {

View file

@ -4119,6 +4119,7 @@ export type components = {
reactions: { reactions: {
[key: string]: number; [key: string]: number;
}; };
reactionCount: number;
renoteCount: number; renoteCount: number;
repliesCount: number; repliesCount: number;
uri?: string; uri?: string;

File diff suppressed because it is too large Load diff