Merge branch 'develop' into fix-stl-note-fetch

This commit is contained in:
taichan 2024-03-07 02:04:23 +09:00 committed by GitHub
commit d3228d5570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 1764 additions and 1407 deletions

View File

@ -0,0 +1,40 @@
name: "Release Manager: sync changelog with PR"
on:
push:
branches:
- release/**
paths:
- 'CHANGELOG.md'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
edit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
- name: Get PR
run: |
echo "pr_number=$(gh pr list --limit 1 --head "${{ github.ref_name }}" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
- name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1
id: v
# CHANGELOG.mdの内容を取得
- name: Get changelog
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1
with:
version: ${{ steps.v.outputs.target_version }}
id: changelog
# PRのnotesを更新
- name: Update PR
run: |
gh pr edit ${{ steps.get_pr.outputs.pr_number }} --body "${{ steps.changelog.outputs.changelog }}"

View File

@ -0,0 +1,122 @@
name: "Release Manager [Dispatch]"
on:
workflow_dispatch:
inputs:
## Specify the type of the next release.
#version_increment_type:
# type: choice
# description: 'VERSION INCREMENT TYPE'
# default: 'patch'
# required: false
# options:
# - 'major'
# - 'minor'
# - 'patch'
merge:
type: boolean
description: 'MERGE RELEASE BRANCH TO MAIN'
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
get-pr:
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps:
- uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得
- name: Get PRs
run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr
merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog
# The first line must be `## Unreleased`
changes_template: |
## Unreleased
### General
-
### Client
-
### Server
-
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with:
pr_number: ${{ needs.get-pr.outputs.pr_number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1
needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }}
with:
# The script for version increment.
# process.env.CURRENT_VERSION: The current version.
#
# Misskey calender versioning (yyyy.MM.patch) example
version_increment_script: |
const now = new Date();
const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' });
const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' });
const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.');
const patch = Number(_patch.split('-')[0]);
if (Number.isNaN(patch)) {
console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch);
throw new Error('Invalid patch version');
}
if (year !== major || month !== minor) {
return `${year}.${month}.0`;
} else {
return `${major}.${minor}.${patch + 1}`;
}
##Semver example
#version_increment_script: |
# const [major, minor, patch] = process.env.CURRENT_VERSION.split('.');
# if ("${{ inputs.version_increment_type }}" === "major") {
# return `${Number(major) + 1}.0.0`;
# } else if ("${{ inputs.version_increment_type }}" === "minor") {
# return `${major}.${Number(minor) + 1}.0`;
# } else {
# return `${major}.${minor}.${Number(patch) + 1}`;
# }
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}

View File

@ -0,0 +1,38 @@
name: "Release Manager: release RC when ready for review"
on:
pull_request:
types: [ready_for_review]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.get_pr.outputs.ref }}
steps:
- uses: actions/checkout@v4
# PR情報を取得
- name: Get PR
run: |
pr_json=$(gh pr view ${{ github.event.pull_request.number }} --json isDraft,headRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
id: get_pr
release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1
needs: check
if: startsWith(needs.check.outputs.ref, 'release/')
with:
pr_number: ${{ github.event.pull_request.number }}
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

View File

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

View File

@ -316,6 +316,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する

View File

@ -1,9 +1,11 @@
<div align="center"> <div align="center">
<a href="https://misskey-hub.net"> <a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/> <img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="300"/>
</a> </a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **Misskey** is an open source, federated social media platform that's free forever! 🚀**
[Learn more](https://misskey-hub.net/)
--- ---
@ -22,41 +24,6 @@
<a href="https://www.patreon.com/syuilo"> <a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
</div>
<div>
<a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai.png?raw=true" align="right" height="320px"/></a>
## ✨ Features
- **ActivityPub support**\
Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
- **Reactions**\
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
- **Drive**\
With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
- **Rich Web UI**\
Misskey has a rich and easy to use Web UI!
It is highly customizable, from changing the layout and adding widgets to making custom themes.
Furthermore, plugins can be created using AiScript, an original programming language.
- And much more...
</div>
<div style="clear: both;"></div>
## Documentation
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/docs/), some of the links and graphics above also lead to specific portions of it.
## Sponsors
<div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div> </div>
## Thanks ## Thanks

4
locales/index.d.ts vendored
View File

@ -8909,6 +8909,10 @@ export interface Locale extends ILocale {
* {n} * {n}
*/ */
"reactedBySomeUsers": ParameterizedString<"n">; "reactedBySomeUsers": ParameterizedString<"n">;
/**
* {n}
*/
"likedBySomeUsers": ParameterizedString<"n">;
/** /**
* {n} * {n}
*/ */

View File

@ -2355,6 +2355,7 @@ _notification:
sendTestNotification: "テスト通知を送信する" sendTestNotification: "テスト通知を送信する"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます"
reactedBySomeUsers: "{n}人がリアクションしました" reactedBySomeUsers: "{n}人がリアクションしました"
likedBySomeUsers: "{n}人がいいねしました"
renotedBySomeUsers: "{n}人がリノートしました" renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.3.0", "version": "2024.3.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

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

@ -333,6 +333,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 () => {
@ -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);
@ -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);
@ -853,7 +854,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: 'potentially annoying text', text: 'potentially annoying text',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [bob.id], visibleUserIds: [bob.id],
@ -909,7 +910,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',
visibility: 'specified', visibility: 'specified',
visibleUserIds: [bob.id], visibleUserIds: [bob.id],

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,
@ -71,7 +52,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,
@ -111,7 +92,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,
@ -128,7 +109,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,
@ -159,6 +140,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,
@ -173,61 +155,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) => {
@ -238,7 +212,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' });
@ -306,7 +280,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 });
}); });
@ -319,7 +293,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}/);
@ -329,7 +303,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);
@ -401,6 +375,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']);
@ -430,66 +405,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 => ({ injectFeaturedNote: true }) }, { parameters: () => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) }, { parameters: () => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) }, { parameters: () => ({ receiveAnnouncementEmail: true }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: false }) }, { parameters: () => ({ receiveAnnouncementEmail: false }) },
{ parameters: (): object => ({ alwaysMarkNsfw: true }) }, { parameters: () => ({ alwaysMarkNsfw: true }) },
{ parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: () => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ autoSensitive: true }) }, { parameters: () => ({ autoSensitive: true }) },
{ parameters: (): object => ({ autoSensitive: false }) }, { parameters: () => ({ autoSensitive: false }) },
{ parameters: (): object => ({ followingVisibility: 'private' }) }, { parameters: () => ({ followingVisibility: 'private' as const }) },
{ parameters: (): object => ({ followingVisibility: 'followers' }) }, { parameters: () => ({ followingVisibility: 'followers' as const }) },
{ parameters: (): object => ({ followingVisibility: 'public' }) }, { parameters: () => ({ followingVisibility: 'public' as const }) },
{ parameters: (): object => ({ followersVisibility: 'private' }) }, { parameters: () => ({ followersVisibility: 'private' as const }) },
{ parameters: (): object => ({ followersVisibility: 'followers' }) }, { parameters: () => ({ followersVisibility: 'followers' as const }) },
{ parameters: (): object => ({ followersVisibility: 'public' }) }, { parameters: () => ({ followersVisibility: 'public' as const }) },
{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, { parameters: () => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: () => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: [] }) }, { parameters: () => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, { parameters: () => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) }, { parameters: () => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) }, { parameters: () => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
{ parameters: (): object => ({ notificationRecieveConfig: {} }) }, { parameters: () => ({ notificationRecieveConfig: {} }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, { parameters: () => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) }, { parameters: () => ({ 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() };
@ -498,13 +473,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,
}; };
@ -523,13 +498,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,
}; };
@ -579,13 +554,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 });
@ -598,15 +573,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 });
@ -620,39 +595,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 });
@ -694,17 +674,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 });
@ -729,15 +710,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 });
@ -751,30 +732,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 });
@ -796,15 +777,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 });
@ -818,12 +799,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 });
@ -837,15 +818,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) {

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

@ -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

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</ol> </ol>
<ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list"> <ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
<li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> <li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji"/> <MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/> <MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span> <span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true" :fallbackToImage="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button> </button>
</div> </div>

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkCustomEmoji class="emoji" :name="emoji.name"/> <MkCustomEmoji class="emoji" :name="emoji.name" :fallbackToImage="true"/>
</button> </button>
</div> </div>
<div v-if="searchResultUnicode.length > 0" class="body"> <div v-if="searchResultUnicode.length > 0" class="body">

View File

@ -66,7 +66,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;
@ -96,8 +96,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 ? 'ti ti-eye' : 'ti ti-eye-exclamation', icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
danger: true, danger: true,
@ -105,6 +103,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

@ -59,7 +59,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;
@ -114,6 +114,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

@ -94,7 +94,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;
@ -122,8 +122,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 ? 'ti ti-eye' : 'ti ti-eye-exclamation', icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
danger: true, danger: true,
@ -131,6 +129,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

@ -93,7 +93,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="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction"> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
<template #more> <template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template> </template>
@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<footer :class="$style.footer"> <footer :class="$style.footer">
<button :class="$style.footerButton" class="_button" @click="reply()"> <button :class="$style.footerButton" class="_button" @click="reply()">
<i class="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></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"
@ -111,17 +111,17 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renote()" @mousedown="renote()"
> >
<i class="ti ti-repeat"></i> <i class="ti ti-repeat"></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="ti ti-ban"></i> <i class="ti ti-ban"></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="ti ti-heart"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i> <i v-else class="ti ti-plus"></i>
</button> <p v-if="appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
<i class="ti ti-minus"></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="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
@ -175,6 +175,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';
@ -420,6 +421,14 @@ function undoReact(targetNote: Misskey.entities.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

@ -106,10 +106,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="ti ti-arrow-back-up"></i> <i class="ti ti-arrow-back-up"></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"
@ -119,17 +119,17 @@ SPDX-License-Identifier: AGPL-3.0-only
@mousedown="renote()" @mousedown="renote()"
> >
<i class="ti ti-repeat"></i> <i class="ti ti-repeat"></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="ti ti-ban"></i> <i class="ti ti-ban"></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="ti ti-heart"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i> <i v-else class="ti ti-plus"></i>
</button> <p v-if="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="ti ti-minus"></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="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
@ -209,6 +209,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';
@ -401,14 +402,22 @@ function react(viaKeyboard = false): 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,
}); });
} }
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'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note'].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_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" 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"/>
@ -57,6 +58,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>
@ -201,6 +203,7 @@ const rejectFollowRequest = () => {
} }
.icon_reactionGroup, .icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup { .icon_renoteGroup {
display: grid; display: grid;
align-items: center; align-items: center;
@ -213,11 +216,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 {
@ -246,49 +253,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

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl"/> <MkCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl" :fallbackToImage="true"/>
<MkEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/> <MkEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/>
</template> </template>

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

@ -48,10 +48,18 @@ export const Missing = {
name: Default.args.name, name: Default.args.name,
}, },
} satisfies StoryObj<typeof MkCustomEmoji>; } satisfies StoryObj<typeof MkCustomEmoji>;
export const Error = { export const ErrorToText = {
...Default, ...Default,
args: { args: {
url: 'https://example.com/404', url: 'https://example.com/404',
name: Default.args.name, name: Default.args.name,
}, },
} satisfies StoryObj<typeof MkCustomEmoji>; } satisfies StoryObj<typeof MkCustomEmoji>;
export const ErrorToImage = {
...Default,
args: {
url: 'https://example.com/404',
name: Default.args.name,
fallbackToImage: true,
},
} satisfies StoryObj<typeof MkCustomEmoji>;

View File

@ -5,11 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<img <img
v-if="errored" v-if="errored && fallbackToImage"
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
src="/client-assets/dummy.png" src="/client-assets/dummy.png"
:title="alt" :title="alt"
/> />
<span v-else-if="errored">:{{ customEmojiName }}:</span>
<img <img
v-else v-else
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
@ -44,6 +45,7 @@ const props = defineProps<{
useOriginalSize?: boolean; useOriginalSize?: boolean;
menu?: boolean; menu?: boolean;
menuReaction?: boolean; menuReaction?: boolean;
fallbackToImage?: boolean;
}>(); }>();
const react = inject<((name: string) => void) | null>('react', null); const react = inject<((name: string) => void) | null>('react', null);

View File

@ -407,6 +407,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
useOriginalSize: scale >= 2.5, useOriginalSize: scale >= 2.5,
menu: props.enableEmojiMenu, menu: props.enableEmojiMenu,
menuReaction: props.enableEmojiMenuReaction, menuReaction: props.enableEmojiMenuReaction,
fallbackToImage: false,
})]; })];
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition

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

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="misskey">Misskey</div> <div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div> <div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"> <span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }">
<MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true"/> <MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true" :fallbackToImage="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :noStyle="true"/> <MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :noStyle="true"/>
</span> </span>
</div> </div>

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<template #item="{element}"> <template #item="{element}">
<button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)"> <button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/> <MkEmoji v-else :emoji="element" :normal="true"/>
</button> </button>
</template> </template>
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<template #item="{element}"> <template #item="{element}">
<button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)"> <button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)">
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="element" :normal="true"/> <MkEmoji v-else :emoji="element" :normal="true"/>
</button> </button>
</template> </template>

View File

@ -35,6 +35,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;
@ -49,6 +50,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

@ -22,6 +22,13 @@
} }
//--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;
} }
::selection { ::selection {

View File

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

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2024.3.0", "version": "2024.3.1",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts", "types": "./built/dts/index.d.ts",
"exports": { "exports": {

View File

@ -3987,6 +3987,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;