test(backend): add federation test (#14582)
* test(backend): add federation test
* fix(ci): install pnpm
* fix(ci): cd
* fix(ci): build entire project
* fix(ci): skip frontend build
* fix(ci): pull submodule when checkout
* chore: show log for debugging
* Revert "chore: show log for debugging"
This reverts commit a930964b8d
.
* fix(ci): build entire project
* chore: omit unused globals
* refactor: use strictEqual and simplify some asserts
* test: follow requests
* refactor: add resolveRemoteNote function
* refactor: refine resolveRemoteUser function
* refactor: cache admin credentials
* refactor: simplify assertion with excluded fields
* refactor: use assert
* test: note
* chore: labeler detect federation
* test: blocking
* test: move
* fix: use appropriate TLD
* chore: shorter purge interval
* fix(ci): change TLD
* refactor: delete trivial comment
* test(user): isCat
* chore: use jest
* chore: omit logs
* chore: add memo
* fix(ci): omit unnecessary build
* test: pinning Note
* fix: build daemon in container
* style: indent
* test(streaming): timeline
* chore: rename
* fix: delete role after test
* refactor: resolve users by uri
* fix: delete antenna after test
* test: api timeline
* test: Note deletion
* refactor: sleep function
* test: notification
* style: indent
* refactor: type-safe host
* docs: update description
* refactor: resolve function params
* fix(block): wrong test name
* fix: invalid type
* fix: longer timeout for fire testing
* test(timeline): hashtag
* test(note): vote delivery
* fix: wrong description
* fix: hashtag channel param type
* refactor: wrap basic cases
* test(timeline): add homeTimeline tests
* fix(timeline): correct wrong case and description
* test(notification): add tests for Note
* refactor(user): wrap profile consistency with describe
* chore(note): add issue link
* test(timeline): add test
* test(user): suspension
* test: emoji
* refactor: fetch admin first
* perf: faster tests
* test(drive): sensitive flag
* test(emoji): add tests
* chore: ignore .config/docker.env
* chore: hard-coded tester IP address
* test(emoji): custom emoji are surrounded by zero width space
* refactor: client and username as property
* test(notification): mute
* fix(notification): correct description
* test(block): mention
* refactor(emoji): addCustomEmoji function
* fix: typo
* test(note): add reaction tests
* test(timeline): Note deletion
* fix: unnecessary ts-expect-error
* refactor: unnecessary fetch mocking
* chore: add TODO comments
* test(user): deletion
* chore: enable --frozen-lockfile
* fix(ci): copying configs
* docs: update CONTRIBUTING.md
* docs: fix typo
* chore: set default sleep duration
* fix(notification): omit flaky tests
* fix(notification): correct type
* test(notification): add api endpoint tests
* chore: remove redundant mute test
* refactor: use param client
* fix: start timer after trigger
* refactor: remove unnecessary any
* chore: shorter timeout for checking if fired
* fix(block): remove outdated comment
* refactor: shorten remote user variable name
* refactor(block): use existing function
* refactor: file upload
* docs: update description
* test(user): ffVisibility
* fix: `/api/signin` -> `/api/signin-flow`
* test: abuse report
* refactor: use existing type
* refactor: extract duplicate configs to template file
* fix: typo
* fix: avoid conflict
* refactor: change container dependency
* perf: start misskey parallelly
* fix: remove dependency
* chore(backend): add typecheck
* test: add check for #14728
* chore: enable eslint check
* perf: don't start linked services when test
* test(note): remote note deletion for moderation
* chore: define config template
* chore: write setup script
* refactor: omit unnecessary conditional
* refactor: clarify scope
* refactor: omit type assertion
* refactor: omit logs
* style
* refactor: redundant promise
* refactor: unnecessary imports
* refactor: use readable error code
* refactor: cache set in signin function
* refactor: optimize import
This commit is contained in:
parent
3cea890eba
commit
b990ae6b23
|
@ -6,7 +6,7 @@
|
||||||
'packages/backend:test':
|
'packages/backend:test':
|
||||||
- any:
|
- any:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: ['packages/backend/test/**/*']
|
- any-glob-to-any-file: ['packages/backend/test/**/*', 'packages/backend/test-federation/**/*']
|
||||||
|
|
||||||
'packages/frontend':
|
'packages/frontend':
|
||||||
- any:
|
- any:
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
name: Test (federation)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-federation.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/test-federation.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.16.0]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
- name: Install FFmpeg
|
||||||
|
uses: FedericoCarboni/setup-ffmpeg@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4.0.3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
- name: Build Misskey
|
||||||
|
run: |
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm i --frozen-lockfile
|
||||||
|
pnpm build
|
||||||
|
- name: Setup
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
bash ./setup.sh
|
||||||
|
sudo chmod 644 ./certificates/*.test.key
|
||||||
|
- name: Start servers
|
||||||
|
# https://github.com/docker/compose/issues/1294#issuecomment-374847206
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
docker compose up -d --scale tester=0
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
docker compose run --no-deps tester
|
||||||
|
- name: Stop servers
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
docker compose down
|
|
@ -37,7 +37,7 @@ coverage
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
!/.config/cypress-devcontainer.yml
|
!/.config/cypress-devcontainer.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
compose.yml
|
./compose.yml
|
||||||
.devcontainer/compose.yml
|
.devcontainer/compose.yml
|
||||||
!/.devcontainer/compose.yml
|
!/.devcontainer/compose.yml
|
||||||
|
|
||||||
|
|
|
@ -181,31 +181,45 @@ MK_DEV_PREFER=backend pnpm dev
|
||||||
- HMR may not work in some environments such as Windows.
|
- HMR may not work in some environments such as Windows.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
You can run non-backend tests by executing following commands:
|
||||||
|
```sh
|
||||||
### Run test
|
pnpm --filter frontend test
|
||||||
Create a config file.
|
pnpm --filter misskey-js test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Backend tests require manual preparation of servers. See the next section for more on this.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
There are three types of test codes for the backend:
|
||||||
|
- Unit tests: [`/packages/backend/test/unit`](/packages/backend/test/unit)
|
||||||
|
- Single-server E2E tests: [`/packages/backend/test/e2e`](/packages/backend/test/e2e)
|
||||||
|
- Multiple-server E2E tests: [`/packages/backend/test-federation`](/packages/backend/test-federation)
|
||||||
|
|
||||||
|
#### Running Unit Tests or Single-server E2E Tests
|
||||||
|
1. Create a config file:
|
||||||
|
```sh
|
||||||
cp .github/misskey/test.yml .config/
|
cp .github/misskey/test.yml .config/
|
||||||
```
|
```
|
||||||
Prepare DB/Redis for testing.
|
|
||||||
```
|
2. Start DB and Redis servers for testing:
|
||||||
|
```sh
|
||||||
docker compose -f packages/backend/test/compose.yml up
|
docker compose -f packages/backend/test/compose.yml up
|
||||||
```
|
```
|
||||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
Instead, you can prepare an empty (data can be erased) DB and edit `.config/test.yml` appropriately.
|
||||||
|
|
||||||
Run all test.
|
3. Run all tests:
|
||||||
|
```sh
|
||||||
|
pnpm --filter backend test # unit tests
|
||||||
|
pnpm --filter backend test:e2e # single-server E2E tests
|
||||||
```
|
```
|
||||||
pnpm test
|
If you want to run a specific test, run as a following command:
|
||||||
|
```sh
|
||||||
|
pnpm --filter backend test -- packages/backend/test/unit/activitypub.ts
|
||||||
|
pnpm --filter backend test:e2e -- packages/backend/test/e2e/nodeinfo.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Run specify test
|
#### Running Multiple-server E2E Tests
|
||||||
```
|
See [`/packages/backend/test-federation/README.md`](/packages/backend/test-federation/README.md).
|
||||||
pnpm jest -- foo.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### e2e tests
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Environment Variable
|
## Environment Variable
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default [
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
project: ['./tsconfig.json', './test/tsconfig.json'],
|
project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'],
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||||
|
* https://jestjs.io/docs/en/configuration.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
const base = require('./jest.config.cjs');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...base,
|
||||||
|
testMatch: [
|
||||||
|
'<rootDir>/test-federation/test/**/*.test.ts',
|
||||||
|
],
|
||||||
|
};
|
|
@ -19,16 +19,18 @@
|
||||||
"watch": "node ./scripts/watch.mjs",
|
"watch": "node ./scripts/watch.mjs",
|
||||||
"restart": "pnpm build && pnpm start",
|
"restart": "pnpm build && pnpm start",
|
||||||
"dev": "node ./scripts/dev.mjs",
|
"dev": "node ./scripts/dev.mjs",
|
||||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint",
|
"lint": "pnpm typecheck && pnpm eslint",
|
||||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
||||||
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
|
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
|
||||||
|
"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
|
||||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||||
"test": "pnpm jest",
|
"test": "pnpm jest",
|
||||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||||
|
"test:fed": "pnpm jest:fed",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage",
|
"test-and-coverage": "pnpm jest-and-coverage",
|
||||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||||
"generate-api-json": "node ./scripts/generate_api_json.js"
|
"generate-api-json": "node ./scripts/generate_api_json.js"
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
# based on https://github.com/misskey-dev/misskey-hub/blob/7071f63a1c80ee35c71f0fd8a6d8722c118c7574/src/docs/admin/nginx.md
|
||||||
|
|
||||||
|
# For WebSocket
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${HOST};
|
||||||
|
|
||||||
|
# For SSL domain validation
|
||||||
|
root /var/www/html;
|
||||||
|
location /.well-known/acme-challenge/ { allow all; }
|
||||||
|
location /.well-known/pki-validation/ { allow all; }
|
||||||
|
location / { return 301 https://$server_name$request_uri; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name ${HOST};
|
||||||
|
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:ssl_session_cache:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
ssl_trusted_certificate /etc/nginx/certificates/rootCA.crt;
|
||||||
|
ssl_certificate /etc/nginx/certificates/$server_name.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/certificates/$server_name.key;
|
||||||
|
|
||||||
|
# SSL protocol settings
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
|
||||||
|
# Change to your upload limit
|
||||||
|
client_max_body_size 80m;
|
||||||
|
|
||||||
|
# Proxy to Node
|
||||||
|
location / {
|
||||||
|
proxy_pass http://misskey.${HOST}:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
|
# If it's behind another reverse proxy or CDN, remove the following.
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
|
# For WebSocket
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
|
# Cache settings
|
||||||
|
proxy_cache cache1;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
proxy_cache_use_stale updating;
|
||||||
|
proxy_force_ranges on;
|
||||||
|
add_header X-Cache $upstream_cache_status;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
url: https://${HOST}/
|
||||||
|
port: 3000
|
||||||
|
db:
|
||||||
|
host: db.${HOST}
|
||||||
|
port: 5432
|
||||||
|
db: misskey
|
||||||
|
user: postgres
|
||||||
|
pass: postgres
|
||||||
|
dbReplications: false
|
||||||
|
redis:
|
||||||
|
host: redis.test
|
||||||
|
port: 6379
|
||||||
|
id: 'aidx'
|
||||||
|
proxyBypassHosts:
|
||||||
|
- api.deepl.com
|
||||||
|
- api-free.deepl.com
|
||||||
|
- www.recaptcha.net
|
||||||
|
- hcaptcha.com
|
||||||
|
- challenges.cloudflare.com
|
||||||
|
proxyRemoteFiles: true
|
||||||
|
signToActivityPubGet: true
|
||||||
|
allowedPrivateNetworks: [
|
||||||
|
'127.0.0.1/32',
|
||||||
|
'172.20.0.0/16'
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
|
||||||
|
POSTGRES_DB=misskey
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
MK_VERBOSE=true
|
|
@ -0,0 +1,6 @@
|
||||||
|
certificates
|
||||||
|
volumes
|
||||||
|
.env
|
||||||
|
docker.env
|
||||||
|
*.test.conf
|
||||||
|
*.test.default.yml
|
|
@ -0,0 +1,24 @@
|
||||||
|
## test-federation
|
||||||
|
Test federation between two Misskey servers: `a.test` and `b.test`.
|
||||||
|
|
||||||
|
Before testing, you need to build the entire project, and change working directory to here:
|
||||||
|
```sh
|
||||||
|
pnpm build
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
```
|
||||||
|
|
||||||
|
First, you need to start servers by executing following commands:
|
||||||
|
```sh
|
||||||
|
bash ./setup.sh
|
||||||
|
docker compose up --scale tester=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can run all tests by a following command:
|
||||||
|
```sh
|
||||||
|
docker compose run --no-deps --rm tester
|
||||||
|
```
|
||||||
|
|
||||||
|
For testing a specific file, run a following command:
|
||||||
|
```sh
|
||||||
|
docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
|
||||||
|
```
|
|
@ -0,0 +1,64 @@
|
||||||
|
services:
|
||||||
|
a.test:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: nginx
|
||||||
|
depends_on:
|
||||||
|
misskey.a.test:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/a.test.conf
|
||||||
|
target: /etc/nginx/conf.d/a.test.conf
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/a.test.crt
|
||||||
|
target: /etc/nginx/certificates/a.test.crt
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/a.test.key
|
||||||
|
target: /etc/nginx/certificates/a.test.key
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
misskey.a.test:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: misskey
|
||||||
|
depends_on:
|
||||||
|
db.a.test:
|
||||||
|
condition: service_healthy
|
||||||
|
redis.test:
|
||||||
|
condition: service_healthy
|
||||||
|
setup:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/a.test.default.yml
|
||||||
|
target: /misskey/.config/default.yml
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
db.a.test:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: db
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./volumes/db.a
|
||||||
|
target: /var/lib/postgresql/data
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal_network_a:
|
||||||
|
internal: true
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.21.0.0/16
|
||||||
|
ip_range: 172.21.0.0/24
|
|
@ -0,0 +1,64 @@
|
||||||
|
services:
|
||||||
|
b.test:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: nginx
|
||||||
|
depends_on:
|
||||||
|
misskey.b.test:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal_network_b
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/b.test.conf
|
||||||
|
target: /etc/nginx/conf.d/b.test.conf
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/b.test.crt
|
||||||
|
target: /etc/nginx/certificates/b.test.crt
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/b.test.key
|
||||||
|
target: /etc/nginx/certificates/b.test.key
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
misskey.b.test:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: misskey
|
||||||
|
depends_on:
|
||||||
|
db.b.test:
|
||||||
|
condition: service_healthy
|
||||||
|
redis.test:
|
||||||
|
condition: service_healthy
|
||||||
|
setup:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
networks:
|
||||||
|
- internal_network_b
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/b.test.default.yml
|
||||||
|
target: /misskey/.config/default.yml
|
||||||
|
read_only: true
|
||||||
|
|
||||||
|
db.b.test:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: db
|
||||||
|
networks:
|
||||||
|
- internal_network_b
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./volumes/db.b
|
||||||
|
target: /var/lib/postgresql/data
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal_network_b:
|
||||||
|
internal: true
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.22.0.0/16
|
||||||
|
ip_range: 172.22.0.0/24
|
|
@ -0,0 +1,117 @@
|
||||||
|
services:
|
||||||
|
setup:
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: node_modules
|
||||||
|
target: /misskey/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_backend
|
||||||
|
target: /misskey/packages/backend/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-js
|
||||||
|
target: /misskey/packages/misskey-js/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-reversi
|
||||||
|
target: /misskey/packages/misskey-reversi/node_modules
|
||||||
|
|
||||||
|
tester:
|
||||||
|
networks:
|
||||||
|
external_network:
|
||||||
|
internal_network:
|
||||||
|
ipv4_address: 172.20.1.1
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_dev
|
||||||
|
target: /misskey/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_backend_dev
|
||||||
|
target: /misskey/packages/backend/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-js_dev
|
||||||
|
target: /misskey/packages/misskey-js/node_modules
|
||||||
|
|
||||||
|
daemon:
|
||||||
|
networks:
|
||||||
|
- external_network
|
||||||
|
- internal_network_a
|
||||||
|
- internal_network_b
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_dev
|
||||||
|
target: /misskey/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_backend_dev
|
||||||
|
target: /misskey/packages/backend/node_modules
|
||||||
|
|
||||||
|
redis.test:
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
- internal_network_b
|
||||||
|
|
||||||
|
a.test:
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
|
||||||
|
misskey.a.test:
|
||||||
|
networks:
|
||||||
|
- external_network
|
||||||
|
- internal_network
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: node_modules
|
||||||
|
target: /misskey/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_backend
|
||||||
|
target: /misskey/packages/backend/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-js
|
||||||
|
target: /misskey/packages/misskey-js/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-reversi
|
||||||
|
target: /misskey/packages/misskey-reversi/node_modules
|
||||||
|
|
||||||
|
b.test:
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
|
||||||
|
misskey.b.test:
|
||||||
|
networks:
|
||||||
|
- external_network
|
||||||
|
- internal_network
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: node_modules
|
||||||
|
target: /misskey/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_backend
|
||||||
|
target: /misskey/packages/backend/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-js
|
||||||
|
target: /misskey/packages/misskey-js/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_misskey-reversi
|
||||||
|
target: /misskey/packages/misskey-reversi/node_modules
|
||||||
|
|
||||||
|
networks:
|
||||||
|
external_network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.23.0.0/16
|
||||||
|
ip_range: 172.23.0.0/24
|
||||||
|
internal_network:
|
||||||
|
internal: true
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
|
ip_range: 172.20.0.0/24
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node_modules:
|
||||||
|
node_modules_dev:
|
||||||
|
node_modules_backend:
|
||||||
|
node_modules_backend_dev:
|
||||||
|
node_modules_misskey-js:
|
||||||
|
node_modules_misskey-js_dev:
|
||||||
|
node_modules_misskey-reversi:
|
|
@ -0,0 +1,101 @@
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.27
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/rootCA.crt
|
||||||
|
target: /etc/nginx/certificates/rootCA.crt
|
||||||
|
read_only: true
|
||||||
|
healthcheck:
|
||||||
|
test: service nginx status
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
misskey:
|
||||||
|
image: node:20
|
||||||
|
env_file:
|
||||||
|
- ./.config/docker.env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../../../built
|
||||||
|
target: /misskey/built
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../assets
|
||||||
|
target: /misskey/packages/backend/assets
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../built
|
||||||
|
target: /misskey/packages/backend/built
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../migration
|
||||||
|
target: /misskey/packages/backend/migration
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../ormconfig.js
|
||||||
|
target: /misskey/packages/backend/ormconfig.js
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../package.json
|
||||||
|
target: /misskey/packages/backend/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../misskey-js/built
|
||||||
|
target: /misskey/packages/misskey-js/built
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../misskey-js/package.json
|
||||||
|
target: /misskey/packages/misskey-js/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../misskey-reversi/built
|
||||||
|
target: /misskey/packages/misskey-reversi/built
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../misskey-reversi/package.json
|
||||||
|
target: /misskey/packages/misskey-reversi/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../healthcheck.sh
|
||||||
|
target: /misskey/healthcheck.sh
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../package.json
|
||||||
|
target: /misskey/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../pnpm-lock.yaml
|
||||||
|
target: /misskey/pnpm-lock.yaml
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../pnpm-workspace.yaml
|
||||||
|
target: /misskey/pnpm-workspace.yaml
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/rootCA.crt
|
||||||
|
target: /usr/local/share/ca-certificates/rootCA.crt
|
||||||
|
read_only: true
|
||||||
|
working_dir: /misskey
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm -F backend migrate
|
||||||
|
pnpm -F backend start
|
||||||
|
"
|
||||||
|
healthcheck:
|
||||||
|
test: bash /misskey/healthcheck.sh
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
env_file:
|
||||||
|
- ./.config/docker.env
|
||||||
|
volumes:
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
|
@ -0,0 +1,133 @@
|
||||||
|
include:
|
||||||
|
- ./compose.a.yml
|
||||||
|
- ./compose.b.yml
|
||||||
|
|
||||||
|
services:
|
||||||
|
setup:
|
||||||
|
extends:
|
||||||
|
file: ./compose.tpl.yml
|
||||||
|
service: misskey
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm -F backend i
|
||||||
|
pnpm -F misskey-js i
|
||||||
|
pnpm -F misskey-reversi i
|
||||||
|
"
|
||||||
|
|
||||||
|
tester:
|
||||||
|
image: node:20
|
||||||
|
depends_on:
|
||||||
|
a.test:
|
||||||
|
condition: service_healthy
|
||||||
|
b.test:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../package.json
|
||||||
|
target: /misskey/packages/backend/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../test/resources
|
||||||
|
target: /misskey/packages/backend/test/resources
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./test
|
||||||
|
target: /misskey/packages/backend/test-federation/test
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../jest.config.cjs
|
||||||
|
target: /misskey/packages/backend/jest.config.cjs
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../jest.config.fed.cjs
|
||||||
|
target: /misskey/packages/backend/jest.config.fed.cjs
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../misskey-js/built
|
||||||
|
target: /misskey/packages/misskey-js/built
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../misskey-js/package.json
|
||||||
|
target: /misskey/packages/misskey-js/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../package.json
|
||||||
|
target: /misskey/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../pnpm-lock.yaml
|
||||||
|
target: /misskey/pnpm-lock.yaml
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../pnpm-workspace.yaml
|
||||||
|
target: /misskey/pnpm-workspace.yaml
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/rootCA.crt
|
||||||
|
target: /usr/local/share/ca-certificates/rootCA.crt
|
||||||
|
read_only: true
|
||||||
|
working_dir: /misskey
|
||||||
|
entrypoint: >
|
||||||
|
bash -c '
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm -F misskey-js i --frozen-lockfile
|
||||||
|
pnpm -F backend i --frozen-lockfile
|
||||||
|
exec "$0" "$@"
|
||||||
|
'
|
||||||
|
command: pnpm -F backend test:fed
|
||||||
|
|
||||||
|
daemon:
|
||||||
|
image: node:20
|
||||||
|
depends_on:
|
||||||
|
redis.test:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../package.json
|
||||||
|
target: /misskey/packages/backend/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./daemon.ts
|
||||||
|
target: /misskey/packages/backend/test-federation/daemon.ts
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./tsconfig.json
|
||||||
|
target: /misskey/packages/backend/test-federation/tsconfig.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../package.json
|
||||||
|
target: /misskey/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../pnpm-lock.yaml
|
||||||
|
target: /misskey/pnpm-lock.yaml
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ../../../pnpm-workspace.yaml
|
||||||
|
target: /misskey/pnpm-workspace.yaml
|
||||||
|
read_only: true
|
||||||
|
working_dir: /misskey
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm -F backend i --frozen-lockfile
|
||||||
|
pnpm exec tsc -p ./packages/backend/test-federation
|
||||||
|
node ./packages/backend/test-federation/built/daemon.js
|
||||||
|
"
|
||||||
|
|
||||||
|
redis.test:
|
||||||
|
image: redis:7-alpine
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./volumes/redis
|
||||||
|
target: /data
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
healthcheck:
|
||||||
|
test: redis-cli ping
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
|
@ -0,0 +1,38 @@
|
||||||
|
import IPCIDR from 'ip-cidr';
|
||||||
|
import { Redis } from 'ioredis';
|
||||||
|
|
||||||
|
const TESTER_IP_ADDRESS = '172.20.1.1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should be same as {@link file://./../src/misc/get-ip-hash.ts}.
|
||||||
|
*/
|
||||||
|
function getIpHash(ip: string) {
|
||||||
|
const prefix = IPCIDR.createAddress(ip).mask(64);
|
||||||
|
return `ip-${BigInt('0b' + prefix).toString(36)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This prevents hitting rate limit when login.
|
||||||
|
*/
|
||||||
|
export async function purgeLimit(host: string, client: Redis) {
|
||||||
|
const ipHash = getIpHash(TESTER_IP_ADDRESS);
|
||||||
|
const key = `${host}:limit:${ipHash}:signin`;
|
||||||
|
const res = await client.zrange(key, 0, -1);
|
||||||
|
if (res.length !== 0) {
|
||||||
|
console.log(`${key} - ${JSON.stringify(res)}`);
|
||||||
|
await client.del(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Daemon started running');
|
||||||
|
|
||||||
|
{
|
||||||
|
const redisClient = new Redis({
|
||||||
|
host: 'redis.test',
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
purgeLimit('a.test', redisClient);
|
||||||
|
purgeLimit('b.test', redisClient);
|
||||||
|
}, 200);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import globals from 'globals';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import sharedConfig from '../../shared/eslint.config.js';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...sharedConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/bash
|
||||||
|
mkdir certificates
|
||||||
|
|
||||||
|
# rootCA
|
||||||
|
openssl genrsa -des3 \
|
||||||
|
-passout pass:rootCA \
|
||||||
|
-out certificates/rootCA.key 4096
|
||||||
|
openssl req -x509 -new -nodes -batch \
|
||||||
|
-key certificates/rootCA.key \
|
||||||
|
-sha256 \
|
||||||
|
-days 1024 \
|
||||||
|
-passin pass:rootCA \
|
||||||
|
-out certificates/rootCA.crt
|
||||||
|
|
||||||
|
# domain
|
||||||
|
function generate {
|
||||||
|
openssl req -new -newkey rsa:2048 -sha256 -nodes \
|
||||||
|
-keyout certificates/$1.key \
|
||||||
|
-subj "/CN=$1/emailAddress=admin@$1/C=JP/ST=/L=/O=Misskey Tester/OU=Some Unit" \
|
||||||
|
-out certificates/$1.csr
|
||||||
|
openssl x509 -req -sha256 \
|
||||||
|
-in certificates/$1.csr \
|
||||||
|
-CA certificates/rootCA.crt \
|
||||||
|
-CAkey certificates/rootCA.key \
|
||||||
|
-CAcreateserial \
|
||||||
|
-passin pass:rootCA \
|
||||||
|
-out certificates/$1.crt \
|
||||||
|
-days 500
|
||||||
|
if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
|
||||||
|
if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
|
||||||
|
if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate a.test
|
||||||
|
generate b.test
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { rejects, strictEqual } from 'node:assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js';
|
||||||
|
|
||||||
|
describe('Abuse report', () => {
|
||||||
|
describe('Forwarding report', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser, aModerator: LoginUser, bModerator: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[aModerator, bModerator] = await Promise.all([
|
||||||
|
createModerator('a.test'),
|
||||||
|
createModerator('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice reports Bob, moderator in A forwards it, and B moderator receives it', async () => {
|
||||||
|
const comment = crypto.randomUUID();
|
||||||
|
await alice.client.request('users/report-abuse', { userId: bobInA.id, comment });
|
||||||
|
const reports = await aModerator.client.request('admin/abuse-user-reports', {});
|
||||||
|
const report = reports.filter(report => report.comment === comment)[0];
|
||||||
|
await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {});
|
||||||
|
const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0];
|
||||||
|
// NOTE: reporter is not Alice, and is not moderator in A
|
||||||
|
strictEqual(reportInB.reporter.url, 'https://a.test/@instance.actor');
|
||||||
|
strictEqual(reportInB.targetUserId, bob.id);
|
||||||
|
|
||||||
|
// NOTE: cannot forward multiple times
|
||||||
|
await rejects(
|
||||||
|
async () => await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'INTERNAL_ERROR');
|
||||||
|
strictEqual(err.info.e.message, 'The report has already been forwarded.');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,224 @@
|
||||||
|
import { deepStrictEqual, rejects, strictEqual } from 'node:assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
|
||||||
|
|
||||||
|
describe('Block', () => {
|
||||||
|
describe('Check follow', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cannot follow if blocked', async () => {
|
||||||
|
await alice.client.request('blocking/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'BLOCKED');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 0);
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: this is invalid case
|
||||||
|
test('Cannot follow even if unblocked', async () => {
|
||||||
|
// unblock here
|
||||||
|
await alice.client.request('blocking/delete', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// TODO: why still being blocked?
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'BLOCKED');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('Can follow if unblocked', async () => {
|
||||||
|
await alice.client.request('blocking/delete', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 1);
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('Remove follower when block them', async () => {
|
||||||
|
test('before block', async () => {
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 1);
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await alice.client.request('blocking/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
test('after block', async () => {
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 0);
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check reply', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cannot reply if blocked', async () => {
|
||||||
|
await alice.client.request('blocking/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can reply if unblocked', async () => {
|
||||||
|
await alice.client.request('blocking/delete', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const reply = (await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id })).createdNote;
|
||||||
|
|
||||||
|
await resolveRemoteNote('b.test', reply.id, alice);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check reaction', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cannot reaction if blocked', async () => {
|
||||||
|
await alice.client.request('blocking/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: this is invalid case
|
||||||
|
test('Cannot reaction even if unblocked', async () => {
|
||||||
|
// unblock here
|
||||||
|
await alice.client.request('blocking/delete', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
|
||||||
|
// TODO: why still being blocked?
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('Can reaction if unblocked', async () => {
|
||||||
|
await alice.client.request('blocking/delete', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' });
|
||||||
|
|
||||||
|
const _note = await alice.client.request('notes/show', { noteId: note.id });
|
||||||
|
deepStrictEqual(_note.reactions, { '😅': 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check mention', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** NOTE: You should mute the target to stop receiving notifications */
|
||||||
|
test('Can mention and notified even if blocked', async () => {
|
||||||
|
await alice.client.request('blocking/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const text = `@${alice.username}@a.test plz unblock me!`;
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('notes/create', { text }),
|
||||||
|
notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,175 @@
|
||||||
|
import assert, { strictEqual } from 'node:assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
|
||||||
|
|
||||||
|
const bAdmin = await fetchAdmin('b.test');
|
||||||
|
|
||||||
|
describe('Drive', () => {
|
||||||
|
describe('Upload image in a.test and resolve from b.test', () => {
|
||||||
|
let uploader: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
uploader = await createAccount('a.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
let image: Misskey.entities.DriveFile, imageInB: Misskey.entities.DriveFile;
|
||||||
|
|
||||||
|
describe('Upload', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
image = await uploadFile('a.test', uploader);
|
||||||
|
const noteWithImage = (await uploader.client.request('notes/create', { fileIds: [image.id] })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', noteWithImage.id, bAdmin);
|
||||||
|
assert(noteInB.files != null);
|
||||||
|
strictEqual(noteInB.files.length, 1);
|
||||||
|
imageInB = noteInB.files[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency of DriveFile', () => {
|
||||||
|
// console.log(`a.test: ${JSON.stringify(image, null, '\t')}`);
|
||||||
|
// console.log(`b.test: ${JSON.stringify(imageInB, null, '\t')}`);
|
||||||
|
|
||||||
|
deepStrictEqualWithExcludedFields(image, imageInB, [
|
||||||
|
'id',
|
||||||
|
'createdAt',
|
||||||
|
'size',
|
||||||
|
'url',
|
||||||
|
'thumbnailUrl',
|
||||||
|
'userId',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedImage: Misskey.entities.DriveFile, updatedImageInB: Misskey.entities.DriveFile;
|
||||||
|
|
||||||
|
describe('Update', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
updatedImage = await uploader.client.request('drive/files/update', {
|
||||||
|
fileId: image.id,
|
||||||
|
name: 'updated_192.jpg',
|
||||||
|
isSensitive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedImageInB = await bAdmin.client.request('drive/files/show', {
|
||||||
|
fileId: imageInB.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency', () => {
|
||||||
|
// console.log(`a.test: ${JSON.stringify(updatedImage, null, '\t')}`);
|
||||||
|
// console.log(`b.test: ${JSON.stringify(updatedImageInB, null, '\t')}`);
|
||||||
|
|
||||||
|
// FIXME: not updated with `drive/files/update`
|
||||||
|
strictEqual(updatedImage.isSensitive, true);
|
||||||
|
strictEqual(updatedImage.name, 'updated_192.jpg');
|
||||||
|
strictEqual(updatedImageInB.isSensitive, false);
|
||||||
|
strictEqual(updatedImageInB.name, '192.jpg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let reupdatedImageInB: Misskey.entities.DriveFile;
|
||||||
|
|
||||||
|
describe('Re-update with attaching to Note', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const noteWithUpdatedImage = (await uploader.client.request('notes/create', { fileIds: [updatedImage.id] })).createdNote;
|
||||||
|
const noteWithUpdatedImageInB = await resolveRemoteNote('a.test', noteWithUpdatedImage.id, bAdmin);
|
||||||
|
assert(noteWithUpdatedImageInB.files != null);
|
||||||
|
strictEqual(noteWithUpdatedImageInB.files.length, 1);
|
||||||
|
reupdatedImageInB = noteWithUpdatedImageInB.files[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency', () => {
|
||||||
|
// console.log(`b.test: ${JSON.stringify(reupdatedImageInB, null, '\t')}`);
|
||||||
|
|
||||||
|
// `isSensitive` is updated
|
||||||
|
strictEqual(reupdatedImageInB.isSensitive, true);
|
||||||
|
// FIXME: but `name` is not updated
|
||||||
|
strictEqual(reupdatedImageInB.name, '192.jpg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Sensitive flag', () => {
|
||||||
|
describe('isSensitive is federated in delivering to followers', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
|
||||||
|
const file = await uploadFile('a.test', alice);
|
||||||
|
await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
|
||||||
|
await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const notes = await bob.client.request('notes/timeline', {});
|
||||||
|
strictEqual(notes.length, 1);
|
||||||
|
const noteInB = notes[0];
|
||||||
|
assert(noteInB.files != null);
|
||||||
|
strictEqual(noteInB.files.length, 1);
|
||||||
|
strictEqual(noteInB.files[0].isSensitive, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSensitive is federated in resolving', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
|
||||||
|
const file = await uploadFile('a.test', alice);
|
||||||
|
await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] })).createdNote;
|
||||||
|
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
assert(noteInB.files != null);
|
||||||
|
strictEqual(noteInB.files.length, 1);
|
||||||
|
strictEqual(noteInB.files[0].isSensitive, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @see https://github.com/misskey-dev/misskey/issues/12208 */
|
||||||
|
describe('isSensitive is federated in replying', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
|
||||||
|
const bobNote = (await bob.client.request('notes/create', { text: 'I\'m Bob' })).createdNote;
|
||||||
|
|
||||||
|
const file = await uploadFile('a.test', alice);
|
||||||
|
await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
|
||||||
|
const bobNoteInA = await resolveRemoteNote('b.test', bobNote.id, alice);
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id], replyId: bobNoteInA.id })).createdNote;
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
assert(noteInB.files != null);
|
||||||
|
strictEqual(noteInB.files.length, 1);
|
||||||
|
strictEqual(noteInB.files[0].isSensitive, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,97 @@
|
||||||
|
import assert, { deepStrictEqual, strictEqual } from 'assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js';
|
||||||
|
|
||||||
|
describe('Emoji', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Custom emoji are delivered with Note delivery', async () => {
|
||||||
|
const emoji = await addCustomEmoji('a.test');
|
||||||
|
await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const notes = await bob.client.request('notes/timeline', {});
|
||||||
|
const noteInB = notes[0];
|
||||||
|
|
||||||
|
strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
|
||||||
|
assert(noteInB.emojis != null);
|
||||||
|
assert(emoji.name in noteInB.emojis);
|
||||||
|
strictEqual(noteInB.emojis[emoji.name], emoji.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Custom emoji are delivered with Reaction delivery', async () => {
|
||||||
|
const emoji = await addCustomEmoji('a.test');
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const noteInB = (await bob.client.request('notes/timeline', {}))[0];
|
||||||
|
deepStrictEqual(noteInB.reactions[`:${emoji.name}@a.test:`], 1);
|
||||||
|
deepStrictEqual(noteInB.reactionEmojis[`${emoji.name}@a.test`], emoji.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Custom emoji are delivered with Profile delivery', async () => {
|
||||||
|
const emoji = await addCustomEmoji('a.test');
|
||||||
|
const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(renewedaliceInB.name, renewedAlice.name);
|
||||||
|
assert(emoji.name in renewedaliceInB.emojis);
|
||||||
|
strictEqual(renewedaliceInB.emojis[emoji.name], emoji.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Local-only custom emoji aren\'t delivered with Note delivery', async () => {
|
||||||
|
const emoji = await addCustomEmoji('a.test', { localOnly: true });
|
||||||
|
await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const notes = await bob.client.request('notes/timeline', {});
|
||||||
|
const noteInB = notes[0];
|
||||||
|
|
||||||
|
strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
|
||||||
|
// deepStrictEqual(noteInB.emojis, {}); // TODO: this fails (why?)
|
||||||
|
deepStrictEqual({ ...noteInB.emojis }, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Local-only custom emoji aren\'t delivered with Reaction delivery', async () => {
|
||||||
|
const emoji = await addCustomEmoji('a.test', { localOnly: true });
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const noteInB = (await bob.client.request('notes/timeline', {}))[0];
|
||||||
|
deepStrictEqual({ ...noteInB.reactions }, { '❤': 1 });
|
||||||
|
deepStrictEqual({ ...noteInB.reactionEmojis }, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Local-only custom emoji aren\'t delivered with Profile delivery', async () => {
|
||||||
|
const emoji = await addCustomEmoji('a.test', { localOnly: true });
|
||||||
|
const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(renewedaliceInB.name, renewedAlice.name);
|
||||||
|
deepStrictEqual({ ...renewedaliceInB.emojis }, {});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
import assert, { strictEqual } from 'node:assert';
|
||||||
|
import { createAccount, type LoginUser, sleep } from './utils.js';
|
||||||
|
|
||||||
|
describe('Move', () => {
|
||||||
|
test('Minimum move', async () => {
|
||||||
|
const [alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
|
||||||
|
await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @see https://github.com/misskey-dev/misskey/issues/11320 */
|
||||||
|
describe('Following relation is transferred after move', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser, carol: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
carol = await createAccount('a.test');
|
||||||
|
|
||||||
|
// Follow @carol@a.test ==> @alice@a.test
|
||||||
|
await carol.client.request('following/create', { userId: alice.id });
|
||||||
|
|
||||||
|
// Move @alice@a.test ==> @bob@b.test
|
||||||
|
await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
|
||||||
|
await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check from follower', async () => {
|
||||||
|
const following = await carol.client.request('users/following', { userId: carol.id });
|
||||||
|
strictEqual(following.length, 2);
|
||||||
|
const followees = following.map(({ followee }) => followee);
|
||||||
|
assert(followees.every(followee => followee != null));
|
||||||
|
assert(followees.some(({ id, url }) => id === alice.id && url === null));
|
||||||
|
assert(followees.some(({ url }) => url === `https://b.test/@${bob.username}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check from followee', async () => {
|
||||||
|
const followers = await bob.client.request('users/followers', { userId: bob.id });
|
||||||
|
strictEqual(followers.length, 1);
|
||||||
|
const follower = followers[0].follower;
|
||||||
|
assert(follower != null);
|
||||||
|
strictEqual(follower.url, `https://a.test/@${carol.username}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,317 @@
|
||||||
|
import assert, { rejects, strictEqual } from 'node:assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
|
||||||
|
|
||||||
|
describe('Note', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Note content', () => {
|
||||||
|
test('Consistency of Public Note', async () => {
|
||||||
|
const image = await uploadFile('a.test', alice);
|
||||||
|
const note = (await alice.client.request('notes/create', {
|
||||||
|
text: 'I am Alice!',
|
||||||
|
fileIds: [image.id],
|
||||||
|
poll: {
|
||||||
|
choices: ['neko', 'inu'],
|
||||||
|
multiple: false,
|
||||||
|
expiredAfter: 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
})).createdNote;
|
||||||
|
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||||
|
'id',
|
||||||
|
'emojis',
|
||||||
|
/** Consistency of files is checked at {@link file://./drive.test.ts}, so let's skip. */
|
||||||
|
'fileIds',
|
||||||
|
'files',
|
||||||
|
/** @see https://github.com/misskey-dev/misskey/issues/12409 */
|
||||||
|
'reactionAcceptance',
|
||||||
|
'userId',
|
||||||
|
'user',
|
||||||
|
'uri',
|
||||||
|
]);
|
||||||
|
strictEqual(aliceInB.id, resolvedNote.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Consistency of reply', async () => {
|
||||||
|
const _replyedNote = (await alice.client.request('notes/create', {
|
||||||
|
text: 'a',
|
||||||
|
})).createdNote;
|
||||||
|
const note = (await alice.client.request('notes/create', {
|
||||||
|
text: 'b',
|
||||||
|
replyId: _replyedNote.id,
|
||||||
|
})).createdNote;
|
||||||
|
// NOTE: the repliedCount is incremented, so fetch again
|
||||||
|
const replyedNote = await alice.client.request('notes/show', { noteId: _replyedNote.id });
|
||||||
|
strictEqual(replyedNote.repliesCount, 1);
|
||||||
|
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||||
|
'id',
|
||||||
|
'emojis',
|
||||||
|
'reactionAcceptance',
|
||||||
|
'replyId',
|
||||||
|
'reply',
|
||||||
|
'userId',
|
||||||
|
'user',
|
||||||
|
'uri',
|
||||||
|
]);
|
||||||
|
assert(resolvedNote.replyId != null);
|
||||||
|
assert(resolvedNote.reply != null);
|
||||||
|
deepStrictEqualWithExcludedFields(replyedNote, resolvedNote.reply, [
|
||||||
|
'id',
|
||||||
|
// TODO: why clippedCount loses consistency?
|
||||||
|
'clippedCount',
|
||||||
|
'emojis',
|
||||||
|
'userId',
|
||||||
|
'user',
|
||||||
|
'uri',
|
||||||
|
// flaky because this is parallelly incremented, so let's check it below
|
||||||
|
'repliesCount',
|
||||||
|
]);
|
||||||
|
strictEqual(aliceInB.id, resolvedNote.userId);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const resolvedReplyedNote = await bob.client.request('notes/show', { noteId: resolvedNote.replyId });
|
||||||
|
strictEqual(resolvedReplyedNote.repliesCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Consistency of Renote', async () => {
|
||||||
|
// NOTE: the renoteCount is not incremented, so no need to fetch again
|
||||||
|
const renotedNote = (await alice.client.request('notes/create', {
|
||||||
|
text: 'a',
|
||||||
|
})).createdNote;
|
||||||
|
const note = (await alice.client.request('notes/create', {
|
||||||
|
text: 'b',
|
||||||
|
renoteId: renotedNote.id,
|
||||||
|
})).createdNote;
|
||||||
|
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||||
|
'id',
|
||||||
|
'emojis',
|
||||||
|
'reactionAcceptance',
|
||||||
|
'renoteId',
|
||||||
|
'renote',
|
||||||
|
'userId',
|
||||||
|
'user',
|
||||||
|
'uri',
|
||||||
|
]);
|
||||||
|
assert(resolvedNote.renoteId != null);
|
||||||
|
assert(resolvedNote.renote != null);
|
||||||
|
deepStrictEqualWithExcludedFields(renotedNote, resolvedNote.renote, [
|
||||||
|
'id',
|
||||||
|
'emojis',
|
||||||
|
'userId',
|
||||||
|
'user',
|
||||||
|
'uri',
|
||||||
|
]);
|
||||||
|
strictEqual(aliceInB.id, resolvedNote.userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Other props', () => {
|
||||||
|
test('localOnly', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
|
||||||
|
rejects(
|
||||||
|
async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
|
||||||
|
(err: any) => {
|
||||||
|
/**
|
||||||
|
* FIXME: this error is not handled
|
||||||
|
* @see https://github.com/misskey-dev/misskey/issues/12736
|
||||||
|
*/
|
||||||
|
strictEqual(err.code, 'INTERNAL_ERROR');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Deletion', () => {
|
||||||
|
describe('Check Delete consistency', () => {
|
||||||
|
let carol: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
carol = await createAccount('a.test');
|
||||||
|
|
||||||
|
await carol.client.request('following/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete is derivered to followers', async () => {
|
||||||
|
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
|
||||||
|
const noteInA = await resolveRemoteNote('b.test', note.id, carol);
|
||||||
|
await bob.client.request('notes/delete', { noteId: note.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await rejects(
|
||||||
|
async () => await carol.client.request('notes/show', { noteId: noteInA.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_NOTE');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Deletion of remote user\'s note for moderation', () => {
|
||||||
|
let note: Misskey.entities.Note;
|
||||||
|
|
||||||
|
test('Alice post is deleted in B', async () => {
|
||||||
|
note = (await alice.client.request('notes/create', { text: 'Hello' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const bMod = await createModerator('b.test');
|
||||||
|
await bMod.client.request('notes/delete', { noteId: noteInB.id });
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_NOTE');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: implement soft deletion as well as user?
|
||||||
|
* @see https://github.com/misskey-dev/misskey/issues/11437
|
||||||
|
*/
|
||||||
|
test.failing('Not found even if resolve again', async () => {
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_NOTE');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Reaction', () => {
|
||||||
|
describe('Consistency', () => {
|
||||||
|
test('Unicode reaction', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const reaction = '😅';
|
||||||
|
await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
|
||||||
|
strictEqual(reactions.length, 1);
|
||||||
|
strictEqual(reactions[0].type, reaction);
|
||||||
|
strictEqual(reactions[0].user.id, bobInA.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Custom emoji reaction', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const emoji = await addCustomEmoji('b.test');
|
||||||
|
await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
|
||||||
|
strictEqual(reactions.length, 1);
|
||||||
|
strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
|
||||||
|
strictEqual(reactions[0].user.id, bobInA.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Acceptance', () => {
|
||||||
|
test('Even if likeOnly, remote users can react with custom emoji, but it is converted to like', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'likeOnly' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const emoji = await addCustomEmoji('b.test');
|
||||||
|
await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
|
||||||
|
strictEqual(reactions.length, 1);
|
||||||
|
strictEqual(reactions[0].type, '❤');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: this may be unexpected behavior?
|
||||||
|
* @see https://github.com/misskey-dev/misskey/issues/12409
|
||||||
|
*/
|
||||||
|
test('Even if nonSensitiveOnly, remote users can react with sensitive emoji, and it is not converted', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'nonSensitiveOnly' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const emoji = await addCustomEmoji('b.test', { isSensitive: true });
|
||||||
|
await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
|
||||||
|
strictEqual(reactions.length, 1);
|
||||||
|
strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Poll', () => {
|
||||||
|
describe('Any remote user\'s vote is delivered to the author', () => {
|
||||||
|
let carol: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
carol = await createAccount('a.test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob creates poll and receives a vote from Carol', async () => {
|
||||||
|
const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
|
||||||
|
const noteInA = await resolveRemoteNote('b.test', note.id, carol);
|
||||||
|
await carol.client.request('notes/polls/vote', { noteId: noteInA.id, choice: 0 });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const noteAfterVote = await bob.client.request('notes/show', { noteId: note.id });
|
||||||
|
assert(noteAfterVote.poll != null);
|
||||||
|
strictEqual(noteAfterVote.poll.choices[0].votes, 1);
|
||||||
|
strictEqual(noteAfterVote.poll.choices[1].votes, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Local user\'s vote is delivered to the author\'s remote followers', () => {
|
||||||
|
let bobRemoteFollower: LoginUser, localVoter: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[
|
||||||
|
bobRemoteFollower,
|
||||||
|
localVoter,
|
||||||
|
] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await bobRemoteFollower.client.request('following/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A vote in Bob\'s server is delivered to Bob\'s remote followers', async () => {
|
||||||
|
const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
|
||||||
|
// NOTE: resolve before voting
|
||||||
|
const noteInA = await resolveRemoteNote('b.test', note.id, bobRemoteFollower);
|
||||||
|
await localVoter.client.request('notes/polls/vote', { noteId: note.id, choice: 0 });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const noteAfterVote = await bobRemoteFollower.client.request('notes/show', { noteId: noteInA.id });
|
||||||
|
assert(noteAfterVote.poll != null);
|
||||||
|
strictEqual(noteAfterVote.poll.choices[0].votes, 1);
|
||||||
|
strictEqual(noteAfterVote.poll.choices[1].votes, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,107 @@
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
|
||||||
|
|
||||||
|
describe('Notification', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Follow', () => {
|
||||||
|
test('Get notification when follow', async () => {
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'b.test', bob,
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
notification => notification.type === 'followRequestAccepted' && notification.userId === aliceInB.id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await bob.client.request('following/delete', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get notification when get followed', async () => {
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
notification => notification.type === 'follow' && notification.userId === bobInA.id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => await bob.client.request('following/delete', { userId: aliceInB.id }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Note', () => {
|
||||||
|
test('Get notification when get a reaction', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const reaction = '😅';
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction }),
|
||||||
|
notification =>
|
||||||
|
notification.type === 'reaction' && notification.note.id === note.id && notification.userId === bobInA.id && notification.reaction === reaction,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get notification when replied', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const text = crypto.randomUUID();
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('notes/create', { text, replyId: noteInB.id }),
|
||||||
|
notification =>
|
||||||
|
notification.type === 'reply' && notification.note.reply!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get notification when renoted', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('notes/create', { renoteId: noteInB.id }),
|
||||||
|
notification =>
|
||||||
|
notification.type === 'renote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get notification when quoted', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
|
||||||
|
const text = crypto.randomUUID();
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('notes/create', { text, renoteId: noteInB.id }),
|
||||||
|
notification =>
|
||||||
|
notification.type === 'quote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get notification when mentioned', async () => {
|
||||||
|
const text = `@${alice.username}@a.test`;
|
||||||
|
await assertNotificationReceived(
|
||||||
|
'a.test', alice,
|
||||||
|
async () => await bob.client.request('notes/create', { text }),
|
||||||
|
notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,328 @@
|
||||||
|
import { strictEqual } from 'assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js';
|
||||||
|
|
||||||
|
const bAdmin = await fetchAdmin('b.test');
|
||||||
|
|
||||||
|
describe('Timeline', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag');
|
||||||
|
type TimelineEndpoint = keyof Misskey.Endpoints & (`${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag');
|
||||||
|
const timelineMap = new Map<TimelineChannel, TimelineEndpoint>([
|
||||||
|
['antenna', 'antennas/notes'],
|
||||||
|
['globalTimeline', 'notes/global-timeline'],
|
||||||
|
['homeTimeline', 'notes/timeline'],
|
||||||
|
['hybridTimeline', 'notes/hybrid-timeline'],
|
||||||
|
['localTimeline', 'notes/local-timeline'],
|
||||||
|
['roleTimeline', 'roles/notes'],
|
||||||
|
['hashtag', 'notes/search-by-tag'],
|
||||||
|
['userList', 'notes/user-list-timeline'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function postAndCheckReception<C extends TimelineChannel>(
|
||||||
|
timelineChannel: C,
|
||||||
|
expect: boolean,
|
||||||
|
noteParams: Misskey.entities.NotesCreateRequest = {},
|
||||||
|
channelParams: Misskey.Channels[C]['params'] = {},
|
||||||
|
) {
|
||||||
|
let note: Misskey.entities.Note | undefined;
|
||||||
|
const text = noteParams.text ?? crypto.randomUUID();
|
||||||
|
const streamingFired = await isFired(
|
||||||
|
'b.test', bob, timelineChannel,
|
||||||
|
async () => {
|
||||||
|
note = (await alice.client.request('notes/create', { text, ...noteParams })).createdNote;
|
||||||
|
},
|
||||||
|
'note', msg => msg.text === text,
|
||||||
|
channelParams,
|
||||||
|
);
|
||||||
|
strictEqual(streamingFired, expect);
|
||||||
|
|
||||||
|
const endpoint = timelineMap.get(timelineChannel)!;
|
||||||
|
const params: Misskey.Endpoints[typeof endpoint]['req'] =
|
||||||
|
endpoint === 'antennas/notes' ? { antennaId: (channelParams as Misskey.Channels['antenna']['params']).antennaId } :
|
||||||
|
endpoint === 'notes/user-list-timeline' ? { listId: (channelParams as Misskey.Channels['userList']['params']).listId } :
|
||||||
|
endpoint === 'notes/search-by-tag' ? { query: (channelParams as Misskey.Channels['hashtag']['params']).q } :
|
||||||
|
endpoint === 'roles/notes' ? { roleId: (channelParams as Misskey.Channels['roleTimeline']['params']).roleId } :
|
||||||
|
{};
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
const notes = await (bob.client.request as Request)(endpoint, params);
|
||||||
|
const noteInB = notes.filter(({ uri }) => uri === `https://a.test/notes/${note!.id}`).pop();
|
||||||
|
const endpointFired = noteInB != null;
|
||||||
|
strictEqual(endpointFired, expect);
|
||||||
|
|
||||||
|
// Let's check Delete reception
|
||||||
|
if (expect) {
|
||||||
|
const streamingFired = await isNoteUpdatedEventFired(
|
||||||
|
'b.test', bob, noteInB!.id,
|
||||||
|
async () => await alice.client.request('notes/delete', { noteId: note!.id }),
|
||||||
|
msg => msg.type === 'deleted' && msg.id === noteInB!.id,
|
||||||
|
);
|
||||||
|
strictEqual(streamingFired, true);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
const notes = await (bob.client.request as Request)(endpoint, params);
|
||||||
|
const endpointFired = notes.every(({ uri }) => uri !== `https://a.test/notes/${note!.id}`);
|
||||||
|
strictEqual(endpointFired, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('homeTimeline', () => {
|
||||||
|
// NOTE: narrowing scope intentionally to prevent mistakes by copy-and-paste
|
||||||
|
const homeTimeline = 'homeTimeline';
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s home-only Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, true, { visibility: 'home' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s followers-only Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, true, { visibility: 'followers' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s localOnly Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, false, { localOnly: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s invisible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, false, { visibility: 'specified' });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: can receive this
|
||||||
|
* @see https://github.com/misskey-dev/misskey/issues/14083
|
||||||
|
*/
|
||||||
|
test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: cannot receive this
|
||||||
|
* @see https://github.com/misskey-dev/misskey/issues/14084
|
||||||
|
*/
|
||||||
|
test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote;
|
||||||
|
await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localTimeline', () => {
|
||||||
|
const localTimeline = 'localTimeline';
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Don\'t receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(localTimeline, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hybridTimeline', () => {
|
||||||
|
const hybridTimeline = 'hybridTimeline';
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(hybridTimeline, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s home-only Note', async () => {
|
||||||
|
await postAndCheckReception(hybridTimeline, true, { visibility: 'home' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s followers-only Note', async () => {
|
||||||
|
await postAndCheckReception(hybridTimeline, true, { visibility: 'followers' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(hybridTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('globalTimeline', () => {
|
||||||
|
const globalTimeline = 'globalTimeline';
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(globalTimeline, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s home-only Note', async () => {
|
||||||
|
await postAndCheckReception(globalTimeline, false, { visibility: 'home' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s followers-only Note', async () => {
|
||||||
|
await postAndCheckReception(globalTimeline, false, { visibility: 'followers' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(globalTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('userList', () => {
|
||||||
|
const userList = 'userList';
|
||||||
|
|
||||||
|
let list: Misskey.entities.UserList;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
list = await bob.client.request('users/lists/create', { name: 'Bob\'s List' });
|
||||||
|
await bob.client.request('users/lists/push', { listId: list.id, userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(userList, true, {}, { listId: list.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s home-only Note', async () => {
|
||||||
|
await postAndCheckReception(userList, true, { visibility: 'home' }, { listId: list.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s followers-only Note', async () => {
|
||||||
|
await postAndCheckReception(userList, true, { visibility: 'followers' }, { listId: list.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(userList, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { listId: list.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hashtag', () => {
|
||||||
|
const hashtag = 'hashtag';
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
const tag = crypto.randomUUID();
|
||||||
|
await postAndCheckReception(hashtag, true, { text: `#${tag}` }, { q: [[tag]] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s home-only Note', async () => {
|
||||||
|
const tag = crypto.randomUUID();
|
||||||
|
await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'home' }, { q: [[tag]] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s followers-only Note', async () => {
|
||||||
|
const tag = crypto.randomUUID();
|
||||||
|
await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'followers' }, { q: [[tag]] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
const tag = crypto.randomUUID();
|
||||||
|
await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'specified', visibleUserIds: [bobInA.id] }, { q: [[tag]] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('roleTimeline', () => {
|
||||||
|
const roleTimeline = 'roleTimeline';
|
||||||
|
|
||||||
|
let role: Misskey.entities.Role;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
role = await createRole('b.test', {
|
||||||
|
name: 'Remote Users',
|
||||||
|
description: 'Remote users are assigned to this role.',
|
||||||
|
condFormula: {
|
||||||
|
/** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
|
||||||
|
type: 'isRemote' as never,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(roleTimeline, true, {}, { roleId: role.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s home-only Note', async () => {
|
||||||
|
await postAndCheckReception(roleTimeline, false, { visibility: 'home' }, { roleId: role.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s followers-only Note', async () => {
|
||||||
|
await postAndCheckReception(roleTimeline, false, { visibility: 'followers' }, { roleId: role.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(roleTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { roleId: role.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await bAdmin.client.request('admin/roles/delete', { roleId: role.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Cannot test
|
||||||
|
describe.skip('antenna', () => {
|
||||||
|
const antenna = 'antenna';
|
||||||
|
|
||||||
|
let bobAntenna: Misskey.entities.Antenna;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
bobAntenna = await bob.client.request('antennas/create', {
|
||||||
|
name: 'Bob\'s Egosurfing Antenna',
|
||||||
|
src: 'all',
|
||||||
|
keywords: [['Bob']],
|
||||||
|
excludeKeywords: [],
|
||||||
|
users: [],
|
||||||
|
caseSensitive: false,
|
||||||
|
localOnly: false,
|
||||||
|
withReplies: true,
|
||||||
|
withFile: true,
|
||||||
|
});
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check reception of remote followee\'s Note', () => {
|
||||||
|
test('Receive remote followee\'s Note', async () => {
|
||||||
|
await postAndCheckReception(antenna, true, { text: 'I love Bob (1)' }, { antennaId: bobAntenna.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s home-only Note', async () => {
|
||||||
|
await postAndCheckReception(antenna, false, { text: 'I love Bob (2)', visibility: 'home' }, { antennaId: bobAntenna.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s followers-only Note', async () => {
|
||||||
|
await postAndCheckReception(antenna, false, { text: 'I love Bob (3)', visibility: 'followers' }, { antennaId: bobAntenna.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
|
||||||
|
await postAndCheckReception(antenna, false, { text: 'I love Bob (4)', visibility: 'specified', visibleUserIds: [bobInA.id] }, { antennaId: bobAntenna.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await bob.client.request('antennas/delete', { antennaId: bobAntenna.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,560 @@
|
||||||
|
import assert, { rejects, strictEqual } from 'node:assert';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
|
||||||
|
|
||||||
|
const [aAdmin, bAdmin] = await Promise.all([
|
||||||
|
fetchAdmin('a.test'),
|
||||||
|
fetchAdmin('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe('User', () => {
|
||||||
|
describe('Profile', () => {
|
||||||
|
describe('Consistency of profile', () => {
|
||||||
|
let alice: LoginUser;
|
||||||
|
let aliceWatcher: LoginUser;
|
||||||
|
let aliceWatcherInB: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
alice = await createAccount('a.test');
|
||||||
|
[
|
||||||
|
aliceWatcher,
|
||||||
|
aliceWatcherInB,
|
||||||
|
] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency', async () => {
|
||||||
|
const aliceInA = await aliceWatcher.client.request('users/show', { userId: alice.id });
|
||||||
|
const resolved = await resolveRemoteUser('a.test', aliceInA.id, aliceWatcherInB);
|
||||||
|
const aliceInB = await aliceWatcherInB.client.request('users/show', { userId: resolved.id });
|
||||||
|
|
||||||
|
// console.log(`a.test: ${JSON.stringify(aliceInA, null, '\t')}`);
|
||||||
|
// console.log(`b.test: ${JSON.stringify(aliceInB, null, '\t')}`);
|
||||||
|
|
||||||
|
deepStrictEqualWithExcludedFields(aliceInA, aliceInB, [
|
||||||
|
'id',
|
||||||
|
'host',
|
||||||
|
'avatarUrl',
|
||||||
|
'instance',
|
||||||
|
'badgeRoles',
|
||||||
|
'url',
|
||||||
|
'uri',
|
||||||
|
'createdAt',
|
||||||
|
'lastFetchedAt',
|
||||||
|
'publicReactions',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ffVisibility is federated', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// NOTE: follow each other
|
||||||
|
await Promise.all([
|
||||||
|
alice.client.request('following/create', { userId: bobInA.id }),
|
||||||
|
bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
]);
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Visibility set public by default', async () => {
|
||||||
|
for (const user of await Promise.all([
|
||||||
|
alice.client.request('users/show', { userId: bobInA.id }),
|
||||||
|
bob.client.request('users/show', { userId: aliceInB.id }),
|
||||||
|
])) {
|
||||||
|
strictEqual(user.followersVisibility, 'public');
|
||||||
|
strictEqual(user.followingVisibility, 'public');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** FIXME: not working */
|
||||||
|
test.skip('Setting private for followersVisibility is federated', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
alice.client.request('i/update', { followersVisibility: 'private' }),
|
||||||
|
bob.client.request('i/update', { followersVisibility: 'private' }),
|
||||||
|
]);
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
for (const user of await Promise.all([
|
||||||
|
alice.client.request('users/show', { userId: bobInA.id }),
|
||||||
|
bob.client.request('users/show', { userId: aliceInB.id }),
|
||||||
|
])) {
|
||||||
|
strictEqual(user.followersVisibility, 'private');
|
||||||
|
strictEqual(user.followingVisibility, 'public');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('Setting private for followingVisibility is federated', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
alice.client.request('i/update', { followingVisibility: 'private' }),
|
||||||
|
bob.client.request('i/update', { followingVisibility: 'private' }),
|
||||||
|
]);
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
for (const user of await Promise.all([
|
||||||
|
alice.client.request('users/show', { userId: bobInA.id }),
|
||||||
|
bob.client.request('users/show', { userId: aliceInB.id }),
|
||||||
|
])) {
|
||||||
|
strictEqual(user.followersVisibility, 'private');
|
||||||
|
strictEqual(user.followingVisibility, 'private');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isCat is federated', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Not isCat for default', () => {
|
||||||
|
strictEqual(aliceInB.isCat, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Becoming a cat is sent to their followers', async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await alice.client.request('i/update', { isCat: true });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const res = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(res.isCat, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Pinning Notes', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
aliceInB = await resolveRemoteUser('a.test', alice.id, bob);
|
||||||
|
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pinning localOnly Note is not delivered', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
|
||||||
|
await alice.client.request('i/pin', { noteId: note.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(_aliceInB.pinnedNoteIds.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pinning followers-only Note is not delivered', async () => {
|
||||||
|
const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'followers' })).createdNote;
|
||||||
|
await alice.client.request('i/pin', { noteId: note.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(_aliceInB.pinnedNoteIds.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
let pinnedNote: Misskey.entities.Note;
|
||||||
|
|
||||||
|
test('Pinning normal Note is delivered', async () => {
|
||||||
|
pinnedNote = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
|
||||||
|
await alice.client.request('i/pin', { noteId: pinnedNote.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(_aliceInB.pinnedNoteIds.length, 1);
|
||||||
|
const pinnedNoteInB = await resolveRemoteNote('a.test', pinnedNote.id, bob);
|
||||||
|
strictEqual(_aliceInB.pinnedNotes[0].id, pinnedNoteInB.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unpinning normal Note is delivered', async () => {
|
||||||
|
await alice.client.request('i/unpin', { noteId: pinnedNote.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
strictEqual(_aliceInB.pinnedNoteIds.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Follow / Unfollow', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Follow a.test ==> b.test', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await alice.client.request('following/create', { userId: bobInA.id });
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
strictEqual(
|
||||||
|
(await alice.client.request('users/following', { userId: alice.id }))
|
||||||
|
.some(v => v.followeeId === bobInA.id),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
strictEqual(
|
||||||
|
(await bob.client.request('users/followers', { userId: bob.id }))
|
||||||
|
.some(v => v.followerId === aliceInB.id),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Unfollow a.test ==> b.test', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await alice.client.request('following/delete', { userId: bobInA.id });
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
strictEqual(
|
||||||
|
(await alice.client.request('users/following', { userId: alice.id }))
|
||||||
|
.some(v => v.followeeId === bobInA.id),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
strictEqual(
|
||||||
|
(await bob.client.request('users/followers', { userId: bob.id }))
|
||||||
|
.some(v => v.followerId === aliceInB.id),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Follow requests', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await alice.client.request('i/update', { isLocked: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Send follow request from Bob to Alice and cancel', () => {
|
||||||
|
describe('Bob sends follow request to Alice', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice should have a request', async () => {
|
||||||
|
const requests = await alice.client.request('following/requests/list', {});
|
||||||
|
strictEqual(requests.length, 1);
|
||||||
|
strictEqual(requests[0].followee.id, alice.id);
|
||||||
|
strictEqual(requests[0].follower.id, bobInA.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Alice cancels it', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await bob.client.request('following/requests/cancel', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice should have no requests', async () => {
|
||||||
|
const requests = await alice.client.request('following/requests/list', {});
|
||||||
|
strictEqual(requests.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Send follow request from Bob to Alice and reject', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await alice.client.request('following/requests/reject', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob should have no requests', async () => {
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/requests/cancel', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'FOLLOW_REQUEST_NOT_FOUND');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob doesn\'t follow Alice', async () => {
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Send follow request from Bob to Alice and accept', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
await alice.client.request('following/requests/accept', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob follows Alice', async () => {
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 1);
|
||||||
|
strictEqual(following[0].followeeId, aliceInB.id);
|
||||||
|
strictEqual(following[0].followerId, bob.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Deletion', () => {
|
||||||
|
describe('Check Delete consistency', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob follows Alice, and Alice deleted themself', async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
|
await alice.client.request('i/delete-account', { password: alice.password });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 0); // no following relation
|
||||||
|
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_USER');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Deletion of remote user for moderation', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob follows Alice, then Alice gets deleted in B server', async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
|
await bAdmin.client.request('admin/delete-account', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: remote account is not deleted!
|
||||||
|
* @see https://github.com/misskey-dev/misskey/issues/14728
|
||||||
|
*/
|
||||||
|
const deletedAlice = await bob.client.request('users/show', { userId: aliceInB.id });
|
||||||
|
assert(deletedAlice.id, aliceInB.id);
|
||||||
|
|
||||||
|
// TODO: why still following relation?
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 1);
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'ALREADY_FOLLOWING');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice tries to follow Bob, but it is not processed', async () => {
|
||||||
|
await alice.client.request('following/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const following = await alice.client.request('users/following', { userId: alice.id });
|
||||||
|
strictEqual(following.length, 0); // Not following Bob because B server doesn't return Accept
|
||||||
|
|
||||||
|
const followers = await bob.client.request('users/followers', { userId: bob.id });
|
||||||
|
strictEqual(followers.length, 0); // Alice's Follow is not processed
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Suspension', () => {
|
||||||
|
describe('Check suspend/unsuspend consistency', () => {
|
||||||
|
let alice: LoginUser, bob: LoginUser;
|
||||||
|
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[alice, bob] = await Promise.all([
|
||||||
|
createAccount('a.test'),
|
||||||
|
createAccount('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[bobInA, aliceInB] = await Promise.all([
|
||||||
|
resolveRemoteUser('b.test', bob.id, alice),
|
||||||
|
resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bob follows Alice, and Alice gets suspended, there is no following relation, and Bob fails to follow again', async () => {
|
||||||
|
await bob.client.request('following/create', { userId: aliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
|
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 0); // no following relation
|
||||||
|
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_USER');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alice gets unsuspended, Bob succeeds in following Alice', async () => {
|
||||||
|
await aAdmin.client.request('admin/unsuspend-user', { userId: alice.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const followers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(followers.length, 1); // FIXME: followers are not deleted??
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: still rejected!
|
||||||
|
* seems to can't process Undo Delete activity because it is not implemented
|
||||||
|
* related @see https://github.com/misskey-dev/misskey/issues/13273
|
||||||
|
*/
|
||||||
|
await rejects(
|
||||||
|
async () => await bob.client.request('following/create', { userId: aliceInB.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_USER');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// FIXME: resolving also fails
|
||||||
|
await rejects(
|
||||||
|
async () => await resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'INTERNAL_ERROR');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* instead of simple unsuspension, let's tell existence by following from Alice
|
||||||
|
*/
|
||||||
|
test('Alice can follow Bob', async () => {
|
||||||
|
await alice.client.request('following/create', { userId: bobInA.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const bobFollowers = await bob.client.request('users/followers', { userId: bob.id });
|
||||||
|
strictEqual(bobFollowers.length, 1); // followed by Alice
|
||||||
|
assert(bobFollowers[0].follower != null);
|
||||||
|
const renewedaliceInB = bobFollowers[0].follower;
|
||||||
|
assert(aliceInB.username === renewedaliceInB.username);
|
||||||
|
assert(aliceInB.host === renewedaliceInB.host);
|
||||||
|
assert(aliceInB.id !== renewedaliceInB.id); // TODO: Same username and host, but their ids are different! Is it OK?
|
||||||
|
|
||||||
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
|
strictEqual(following.length, 0); // following are deleted
|
||||||
|
|
||||||
|
// Bob tries to follow Alice
|
||||||
|
await bob.client.request('following/create', { userId: renewedaliceInB.id });
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const aliceFollowers = await alice.client.request('users/followers', { userId: alice.id });
|
||||||
|
strictEqual(aliceFollowers.length, 1);
|
||||||
|
|
||||||
|
// FIXME: but resolving still fails ...
|
||||||
|
await rejects(
|
||||||
|
async () => await resolveRemoteUser('a.test', alice.id, bob),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'INTERNAL_ERROR');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,309 @@
|
||||||
|
import { deepStrictEqual, strictEqual } from 'assert';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { WebSocket } from 'ws';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export const ADMIN_PARAMS = { username: 'admin', password: 'admin' };
|
||||||
|
const ADMIN_CACHE = new Map<Host, SigninResponse>();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fetchAdmin('a.test'),
|
||||||
|
fetchAdmin('b.test'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
type SigninResponse = Omit<Misskey.entities.SigninFlowResponse & { finished: true }, 'finished'>;
|
||||||
|
|
||||||
|
export type LoginUser = SigninResponse & {
|
||||||
|
client: Misskey.api.APIClient;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** used for avoiding overload and some endpoints */
|
||||||
|
export type Request = <
|
||||||
|
E extends keyof Misskey.Endpoints,
|
||||||
|
P extends Misskey.Endpoints[E]['req'],
|
||||||
|
>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
type Host = 'a.test' | 'b.test';
|
||||||
|
|
||||||
|
export async function sleep(ms = 200): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signin(
|
||||||
|
host: Host,
|
||||||
|
params: Misskey.entities.SigninFlowRequest,
|
||||||
|
): Promise<SigninResponse> {
|
||||||
|
// wait for a second to prevent hit rate limit
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
return await (new Misskey.api.APIClient({ origin: `https://${host}` }).request as Request)('signin-flow', params)
|
||||||
|
.then(res => {
|
||||||
|
strictEqual(res.finished, true);
|
||||||
|
if (params.username === ADMIN_PARAMS.username) ADMIN_CACHE.set(host, res);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.then(({ id, i }) => ({ id, i }))
|
||||||
|
.catch(async err => {
|
||||||
|
if (err.code === 'TOO_MANY_AUTHENTICATION_FAILURES') {
|
||||||
|
await sleep(Math.random() * 2000);
|
||||||
|
return await signin(host, params);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse | undefined> {
|
||||||
|
const client = new Misskey.api.APIClient({ origin: `https://${host}` });
|
||||||
|
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
||||||
|
ADMIN_CACHE.set(host, {
|
||||||
|
id: res.id,
|
||||||
|
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
|
||||||
|
i: res.token,
|
||||||
|
});
|
||||||
|
return res as Misskey.entities.SignupResponse;
|
||||||
|
}).then(async res => {
|
||||||
|
await client.request('admin/roles/update-default-policies', {
|
||||||
|
policies: {
|
||||||
|
/** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
|
||||||
|
rateLimitFactor: 0 as never,
|
||||||
|
},
|
||||||
|
}, res.token);
|
||||||
|
return res;
|
||||||
|
}).catch(err => {
|
||||||
|
if (err.info.e.message === 'access denied') return undefined;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAdmin(host: Host): Promise<LoginUser> {
|
||||||
|
const admin = ADMIN_CACHE.get(host) ?? await signin(host, ADMIN_PARAMS)
|
||||||
|
.catch(async err => {
|
||||||
|
if (err.id === '6cc579cc-885d-43d8-95c2-b8c7fc963280') {
|
||||||
|
await createAdmin(host);
|
||||||
|
return await signin(host, ADMIN_PARAMS);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...admin,
|
||||||
|
client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: admin.i }),
|
||||||
|
...ADMIN_PARAMS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAccount(host: Host): Promise<LoginUser> {
|
||||||
|
const username = crypto.randomUUID().replaceAll('-', '').substring(0, 20);
|
||||||
|
const password = crypto.randomUUID().replaceAll('-', '');
|
||||||
|
const admin = await fetchAdmin(host);
|
||||||
|
await admin.client.request('admin/accounts/create', { username, password });
|
||||||
|
const signinRes = await signin(host, { username, password });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...signinRes,
|
||||||
|
client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: signinRes.i }),
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createModerator(host: Host): Promise<LoginUser> {
|
||||||
|
const user = await createAccount(host);
|
||||||
|
const role = await createRole(host, {
|
||||||
|
name: 'Moderator',
|
||||||
|
isModerator: true,
|
||||||
|
});
|
||||||
|
const admin = await fetchAdmin(host);
|
||||||
|
await admin.client.request('admin/roles/assign', { roleId: role.id, userId: user.id });
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRole(
|
||||||
|
host: Host,
|
||||||
|
params: Partial<Misskey.entities.AdminRolesCreateRequest> = {},
|
||||||
|
): Promise<Misskey.entities.Role> {
|
||||||
|
const admin = await fetchAdmin(host);
|
||||||
|
return await admin.client.request('admin/roles/create', {
|
||||||
|
name: 'Some role',
|
||||||
|
description: 'Role for testing',
|
||||||
|
color: null,
|
||||||
|
iconUrl: null,
|
||||||
|
target: 'conditional',
|
||||||
|
condFormula: {},
|
||||||
|
isPublic: true,
|
||||||
|
isModerator: false,
|
||||||
|
isAdministrator: false,
|
||||||
|
isExplorable: true,
|
||||||
|
asBadge: false,
|
||||||
|
canEditMembersByModerator: false,
|
||||||
|
displayOrder: 0,
|
||||||
|
policies: {},
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveRemoteUser(
|
||||||
|
host: Host,
|
||||||
|
id: string,
|
||||||
|
from: LoginUser,
|
||||||
|
): Promise<Misskey.entities.UserDetailedNotMe> {
|
||||||
|
const uri = `https://${host}/users/${id}`;
|
||||||
|
return await from.client.request('ap/show', { uri })
|
||||||
|
.then(res => {
|
||||||
|
strictEqual(res.type, 'User');
|
||||||
|
strictEqual(res.object.uri, uri);
|
||||||
|
return res.object;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveRemoteNote(
|
||||||
|
host: Host,
|
||||||
|
id: string,
|
||||||
|
from: LoginUser,
|
||||||
|
): Promise<Misskey.entities.Note> {
|
||||||
|
const uri = `https://${host}/notes/${id}`;
|
||||||
|
return await from.client.request('ap/show', { uri })
|
||||||
|
.then(res => {
|
||||||
|
strictEqual(res.type, 'Note');
|
||||||
|
strictEqual(res.object.uri, uri);
|
||||||
|
return res.object;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(
|
||||||
|
host: Host,
|
||||||
|
user: { i: string },
|
||||||
|
path = '../../test/resources/192.jpg',
|
||||||
|
): Promise<Misskey.entities.DriveFile> {
|
||||||
|
const filename = path.split('/').pop() ?? 'untitled';
|
||||||
|
const blob = new Blob([await readFile(join(__dirname, path))]);
|
||||||
|
|
||||||
|
const body = new FormData();
|
||||||
|
body.append('i', user.i);
|
||||||
|
body.append('force', 'true');
|
||||||
|
body.append('file', blob);
|
||||||
|
body.append('name', filename);
|
||||||
|
|
||||||
|
return await fetch(`https://${host}/api/drive/files/create`, { method: 'POST', body })
|
||||||
|
.then(async res => await res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addCustomEmoji(
|
||||||
|
host: Host,
|
||||||
|
param?: Partial<Misskey.entities.AdminEmojiAddRequest>,
|
||||||
|
path?: string,
|
||||||
|
): Promise<Misskey.entities.EmojiDetailed> {
|
||||||
|
const admin = await fetchAdmin(host);
|
||||||
|
const name = crypto.randomUUID().replaceAll('-', '');
|
||||||
|
const file = await uploadFile(host, admin, path);
|
||||||
|
return await admin.client.request('admin/emoji/add', { name, fileId: file.id, ...param });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deepStrictEqualWithExcludedFields<T>(actual: T, expected: T, excludedFields: (keyof T)[]) {
|
||||||
|
const _actual = structuredClone(actual);
|
||||||
|
const _expected = structuredClone(expected);
|
||||||
|
for (const obj of [_actual, _expected]) {
|
||||||
|
for (const field of excludedFields) {
|
||||||
|
delete obj[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deepStrictEqual(_actual, _expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isFired<C extends keyof Misskey.Channels, T extends keyof Misskey.Channels[C]['events']>(
|
||||||
|
host: Host,
|
||||||
|
user: { i: string },
|
||||||
|
channel: C,
|
||||||
|
trigger: () => Promise<unknown>,
|
||||||
|
type: T,
|
||||||
|
// @ts-expect-error TODO: why getting error here?
|
||||||
|
cond: (msg: Parameters<Misskey.Channels[C]['events'][T]>[0]) => boolean,
|
||||||
|
params?: Misskey.Channels[C]['params'],
|
||||||
|
): Promise<boolean> {
|
||||||
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
|
// @ts-expect-error TODO: why?
|
||||||
|
const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
|
||||||
|
const connection = stream.useChannel(channel, params);
|
||||||
|
connection.on(type as any, ((msg: any) => {
|
||||||
|
if (cond(msg)) {
|
||||||
|
stream.close();
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
await trigger().then(() => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
stream.close();
|
||||||
|
resolve(false);
|
||||||
|
}, 500);
|
||||||
|
}).catch(err => {
|
||||||
|
stream.close();
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function isNoteUpdatedEventFired(
|
||||||
|
host: Host,
|
||||||
|
user: { i: string },
|
||||||
|
noteId: string,
|
||||||
|
trigger: () => Promise<unknown>,
|
||||||
|
cond: (msg: Parameters<Misskey.StreamEvents['noteUpdated']>[0]) => boolean,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
|
// @ts-expect-error TODO: why?
|
||||||
|
const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
|
||||||
|
stream.send('s', { id: noteId });
|
||||||
|
stream.on('noteUpdated', msg => {
|
||||||
|
if (cond(msg)) {
|
||||||
|
stream.close();
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
await trigger().then(() => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
stream.close();
|
||||||
|
resolve(false);
|
||||||
|
}, 500);
|
||||||
|
}).catch(err => {
|
||||||
|
stream.close();
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function assertNotificationReceived(
|
||||||
|
receiverHost: Host,
|
||||||
|
receiver: LoginUser,
|
||||||
|
trigger: () => Promise<unknown>,
|
||||||
|
cond: (notification: Misskey.entities.Notification) => boolean,
|
||||||
|
expect: boolean,
|
||||||
|
) {
|
||||||
|
const streamingFired = await isFired(receiverHost, receiver, 'main', trigger, 'notification', cond);
|
||||||
|
strictEqual(streamingFired, expect);
|
||||||
|
|
||||||
|
const endpointFired = await receiver.client.request('i/notifications', {})
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
.then(([notification]) => notification != null ? cond(notification) : false);
|
||||||
|
strictEqual(endpointFired, expect);
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "NodeNext", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./built", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"daemon.ts",
|
||||||
|
"./test/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ export default [
|
||||||
{
|
{
|
||||||
files: ['**/*.cjs'],
|
files: ['**/*.cjs'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
sourceType: 'commonjs',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'commonjs',
|
sourceType: 'commonjs',
|
||||||
},
|
},
|
||||||
|
@ -25,4 +26,10 @@ export default [
|
||||||
globals: globals.node,
|
globals: globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.js', '**/*.cjs'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue