test(backend): add federation test
This commit is contained in:
parent
ceb4640669
commit
9cb0727880
|
@ -0,0 +1,55 @@
|
||||||
|
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
|
||||||
|
- 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 -F misskey-js i --frozen-lockfile
|
||||||
|
pnpm -F misskey-js build
|
||||||
|
pnpm -F misskey-reversi i --frozen-lockfile
|
||||||
|
pnpm -F misskey-reversi build
|
||||||
|
pnpm -F backend i --frozen-lockfile
|
||||||
|
pnpm -F backend build
|
||||||
|
pnpm -F backend build:fed
|
||||||
|
- name: Setup
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
cp ./.env.example ./.env
|
||||||
|
bash ./generate_certificates.sh
|
||||||
|
sudo chmod 644 ./certificates/*.local.key
|
||||||
|
- name: Start servers
|
||||||
|
# https://github.com/docker/compose/issues/1294#issuecomment-374847206
|
||||||
|
run: docker compose up -d --scale tester=0
|
||||||
|
- name: Test
|
||||||
|
run: docker compose run tester
|
||||||
|
- name: Stop servers
|
||||||
|
run: 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
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
||||||
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
||||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||||
|
"build:fed": "cd test-federation && tsc -p tsconfig.json",
|
||||||
"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",
|
||||||
|
|
|
@ -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 a.local;
|
||||||
|
|
||||||
|
# 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 a.local;
|
||||||
|
|
||||||
|
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.a.local: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,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 b.local;
|
||||||
|
|
||||||
|
# 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 b.local;
|
||||||
|
|
||||||
|
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.b.local: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://a.local/
|
||||||
|
port: 3000
|
||||||
|
db:
|
||||||
|
host: db.a.local
|
||||||
|
port: 5432
|
||||||
|
db: misskey
|
||||||
|
user: postgres
|
||||||
|
pass: postgres
|
||||||
|
dbReplications: false
|
||||||
|
redis:
|
||||||
|
host: redis.local
|
||||||
|
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,25 @@
|
||||||
|
url: https://b.local/
|
||||||
|
port: 3000
|
||||||
|
db:
|
||||||
|
host: db.b.local
|
||||||
|
port: 5432
|
||||||
|
db: misskey
|
||||||
|
user: postgres
|
||||||
|
pass: postgres
|
||||||
|
dbReplications: false
|
||||||
|
redis:
|
||||||
|
host: redis.local
|
||||||
|
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,4 @@
|
||||||
|
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
|
||||||
|
POSTGRES_DB=misskey
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
|
@ -0,0 +1 @@
|
||||||
|
TESTER_IP_ADDRESS=172.20.1.1
|
|
@ -0,0 +1,3 @@
|
||||||
|
certificates
|
||||||
|
volumes
|
||||||
|
.env
|
|
@ -0,0 +1,7 @@
|
||||||
|
Execute following commands:
|
||||||
|
```sh
|
||||||
|
cp ./.env.example ./.env
|
||||||
|
bash ./generate_certificates.sh
|
||||||
|
pnpm build:fed
|
||||||
|
docker compose up
|
||||||
|
```
|
|
@ -0,0 +1,148 @@
|
||||||
|
services:
|
||||||
|
a.local:
|
||||||
|
image: nginx:1.27
|
||||||
|
depends_on:
|
||||||
|
misskey.a.local:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/a.local.conf
|
||||||
|
target: /etc/nginx/conf.d/a.local.conf
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/a.local.crt
|
||||||
|
target: /etc/nginx/certificates/a.local.crt
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/a.local.key
|
||||||
|
target: /etc/nginx/certificates/a.local.key
|
||||||
|
read_only: true
|
||||||
|
- 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.a.local:
|
||||||
|
image: node:20
|
||||||
|
depends_on:
|
||||||
|
db.a.local:
|
||||||
|
condition: service_healthy
|
||||||
|
redis.local:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
env_file:
|
||||||
|
- ./.config/docker.env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/default.a.yml
|
||||||
|
target: /misskey/.config/default.yml
|
||||||
|
read_only: true
|
||||||
|
- 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 i
|
||||||
|
pnpm -F misskey-js i
|
||||||
|
pnpm -F misskey-reversi i
|
||||||
|
pnpm -F backend migrate
|
||||||
|
pnpm -F backend start
|
||||||
|
"
|
||||||
|
healthcheck:
|
||||||
|
test: bash /misskey/healthcheck.sh
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
db.a.local:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
env_file:
|
||||||
|
- ./.config/docker.env
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./volumes/db.a
|
||||||
|
target: /var/lib/postgresql/data
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
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,151 @@
|
||||||
|
services:
|
||||||
|
b.local:
|
||||||
|
image: nginx:1.27
|
||||||
|
depends_on:
|
||||||
|
misskey.b.local:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal_network_b
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/b.local.conf
|
||||||
|
target: /etc/nginx/conf.d/b.local.conf
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/b.local.crt
|
||||||
|
target: /etc/nginx/certificates/b.local.crt
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./certificates/b.local.key
|
||||||
|
target: /etc/nginx/certificates/b.local.key
|
||||||
|
read_only: true
|
||||||
|
- 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.b.local:
|
||||||
|
image: node:20
|
||||||
|
depends_on:
|
||||||
|
db.b.local:
|
||||||
|
condition: service_healthy
|
||||||
|
redis.local:
|
||||||
|
condition: service_healthy
|
||||||
|
# avoid conflict for installing dependencies
|
||||||
|
misskey.a.local:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal_network_b
|
||||||
|
env_file:
|
||||||
|
- ./.config/docker.env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./.config/default.b.yml
|
||||||
|
target: /misskey/.config/default.yml
|
||||||
|
read_only: true
|
||||||
|
- 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 i
|
||||||
|
pnpm -F misskey-js i
|
||||||
|
pnpm -F misskey-reversi i
|
||||||
|
pnpm -F backend migrate
|
||||||
|
pnpm -F backend start
|
||||||
|
"
|
||||||
|
healthcheck:
|
||||||
|
test: bash /misskey/healthcheck.sh
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
db.b.local:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
networks:
|
||||||
|
- internal_network_b
|
||||||
|
env_file:
|
||||||
|
- ./.config/docker.env
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./volumes/db.b
|
||||||
|
target: /var/lib/postgresql/data
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
healthcheck:
|
||||||
|
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
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,99 @@
|
||||||
|
services:
|
||||||
|
tester:
|
||||||
|
networks:
|
||||||
|
external_network:
|
||||||
|
internal_network:
|
||||||
|
ipv4_address: $TESTER_IP_ADDRESS
|
||||||
|
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
|
||||||
|
|
||||||
|
daemon:
|
||||||
|
networks:
|
||||||
|
- external_network
|
||||||
|
- internal_network_a
|
||||||
|
- internal_network_b
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: node_modules
|
||||||
|
target: /misskey/node_modules
|
||||||
|
- type: volume
|
||||||
|
source: node_modules_backend
|
||||||
|
target: /misskey/packages/backend/node_modules
|
||||||
|
|
||||||
|
redis.local:
|
||||||
|
networks:
|
||||||
|
- internal_network_a
|
||||||
|
- internal_network_b
|
||||||
|
|
||||||
|
a.local:
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
|
||||||
|
misskey.a.local:
|
||||||
|
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.local:
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
|
||||||
|
misskey.b.local:
|
||||||
|
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_backend:
|
||||||
|
node_modules_misskey-js:
|
||||||
|
node_modules_misskey-reversi:
|
|
@ -0,0 +1,111 @@
|
||||||
|
include:
|
||||||
|
- ./compose.a.yml
|
||||||
|
- ./compose.b.yml
|
||||||
|
|
||||||
|
services:
|
||||||
|
tester:
|
||||||
|
image: node:20
|
||||||
|
depends_on:
|
||||||
|
a.local:
|
||||||
|
condition: service_healthy
|
||||||
|
b.local:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- 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: ./built
|
||||||
|
target: /misskey/packages/backend/test-federation
|
||||||
|
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
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm -F backend i
|
||||||
|
node --test "./packages/backend/test-federation/test/*.test.js"
|
||||||
|
"
|
||||||
|
|
||||||
|
daemon:
|
||||||
|
image: node:20
|
||||||
|
depends_on:
|
||||||
|
misskey.a.local:
|
||||||
|
condition: service_healthy
|
||||||
|
misskey.b.local:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- TESTER_IP_ADDRESS=$TESTER_IP_ADDRESS
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../package.json
|
||||||
|
target: /misskey/packages/backend/package.json
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: ./built
|
||||||
|
target: /misskey/packages/backend/test-federation
|
||||||
|
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
|
||||||
|
node ./packages/backend/test-federation/daemon.js
|
||||||
|
"
|
||||||
|
|
||||||
|
redis.local:
|
||||||
|
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,36 @@
|
||||||
|
import IPCIDR from 'ip-cidr';
|
||||||
|
import { Redis } from 'ioredis';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(process.env.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.local',
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
purgeLimit('a.local', redisClient);
|
||||||
|
purgeLimit('b.local', redisClient);
|
||||||
|
}, 1000);
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
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,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/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
|
||||||
|
}
|
||||||
|
|
||||||
|
generate a.local
|
||||||
|
generate b.local
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert';
|
||||||
|
import test, { describe } from 'node:test';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, fetchAdmin, uploadFile } from './utils.js';
|
||||||
|
|
||||||
|
const [
|
||||||
|
[, aAdminClient],
|
||||||
|
[, bAdminClient],
|
||||||
|
] = await Promise.all([
|
||||||
|
fetchAdmin('a.local'),
|
||||||
|
fetchAdmin('b.local'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe('Drive', () => {
|
||||||
|
describe('Upload image in a.local and resolve from b.local', async () => {
|
||||||
|
const [uploader, uploaderClient] = await createAccount('a.local', aAdminClient);
|
||||||
|
|
||||||
|
const image = await uploadFile('a.local', '../../test/resources/192.jpg', uploader.i);
|
||||||
|
const noteWithImage = (await uploaderClient.request('notes/create', { fileIds: [image.id] })).createdNote;
|
||||||
|
const uri = `https://a.local/notes/${noteWithImage.id}`;
|
||||||
|
const noteInBServer = await (async (): Promise<Misskey.entities.ApShowResponse & { type: 'Note' }> => {
|
||||||
|
const resolved = await bAdminClient.request('ap/show', { uri });
|
||||||
|
strictEqual(resolved.type, 'Note');
|
||||||
|
return resolved;
|
||||||
|
})();
|
||||||
|
deepEqual(noteInBServer.object.uri, uri);
|
||||||
|
deepEqual(noteInBServer.object.files != null, true);
|
||||||
|
deepEqual(noteInBServer.object.files!.length, 1);
|
||||||
|
const imageInBServer = noteInBServer.object.files![0];
|
||||||
|
|
||||||
|
await test('Check consistency of DriveFile', () => {
|
||||||
|
// console.log(`a.local: ${JSON.stringify(image, null, '\t')}`);
|
||||||
|
// console.log(`b.local: ${JSON.stringify(imageInBServer, null, '\t')}`);
|
||||||
|
|
||||||
|
const toBeDeleted: (keyof Misskey.entities.DriveFile)[] = [
|
||||||
|
'id',
|
||||||
|
'createdAt',
|
||||||
|
'size',
|
||||||
|
'url',
|
||||||
|
'thumbnailUrl',
|
||||||
|
'userId',
|
||||||
|
];
|
||||||
|
const _Image: Partial<Misskey.entities.DriveFile> = structuredClone(image);
|
||||||
|
const _ImageInBServer: Partial<Misskey.entities.DriveFile> = structuredClone(imageInBServer);
|
||||||
|
|
||||||
|
for (const image of [_Image, _ImageInBServer]) {
|
||||||
|
for (const field of toBeDeleted) {
|
||||||
|
delete image[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deepStrictEqual(_Image, _ImageInBServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedImage = await uploaderClient.request('drive/files/update', {
|
||||||
|
fileId: image.id,
|
||||||
|
name: 'updated_192.jpg',
|
||||||
|
isSensitive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedImageInBServer = await bAdminClient.request('drive/files/show', {
|
||||||
|
fileId: imageInBServer.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Update', async () => {
|
||||||
|
// console.log(`a.local: ${JSON.stringify(updatedImage, null, '\t')}`);
|
||||||
|
// console.log(`b.local: ${JSON.stringify(updatedImageInBServer, null, '\t')}`);
|
||||||
|
|
||||||
|
// FIXME: not updated with `drive/files/update`
|
||||||
|
deepEqual(updatedImage.isSensitive, true);
|
||||||
|
deepEqual(updatedImage.name, 'updated_192.jpg');
|
||||||
|
deepEqual(updatedImageInBServer.isSensitive, false);
|
||||||
|
deepEqual(updatedImageInBServer.name, '192.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
const noteWithUpdatedImage = (await uploaderClient.request('notes/create', { fileIds: [updatedImage.id] })).createdNote;
|
||||||
|
const uriUpdated = `https://a.local/notes/${noteWithUpdatedImage.id}`;
|
||||||
|
const noteWithUpdatedImageInBServer = await (async (): Promise<Misskey.entities.ApShowResponse & { type: 'Note' }> => {
|
||||||
|
const resolved = await bAdminClient.request('ap/show', { uri: uriUpdated });
|
||||||
|
strictEqual(resolved.type, 'Note');
|
||||||
|
return resolved;
|
||||||
|
})();
|
||||||
|
deepEqual(noteWithUpdatedImageInBServer.object.uri, uriUpdated);
|
||||||
|
deepEqual(noteWithUpdatedImageInBServer.object.files != null, true);
|
||||||
|
deepEqual(noteWithUpdatedImageInBServer.object.files!.length, 1);
|
||||||
|
const reupdatedImageInBServer = noteWithUpdatedImageInBServer.object.files![0];
|
||||||
|
|
||||||
|
await test('Re-update with attaching to Note', async () => {
|
||||||
|
// console.log(`b.local: ${JSON.stringify(reupdatedImageInBServer, null, '\t')}`);
|
||||||
|
|
||||||
|
// `isSensitive` is updated
|
||||||
|
deepEqual(reupdatedImageInBServer.isSensitive, true);
|
||||||
|
// FIXME: but `name` is not updated
|
||||||
|
deepEqual(reupdatedImageInBServer.name, '192.jpg');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { deepEqual, deepStrictEqual, strictEqual } from 'node:assert';
|
||||||
|
import test, { before, describe } from 'node:test';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { createAccount, fetchAdmin, resolveRemoteAccount } from './utils.js';
|
||||||
|
|
||||||
|
const [
|
||||||
|
[, aAdminClient],
|
||||||
|
[, bAdminClient],
|
||||||
|
] = await Promise.all([
|
||||||
|
fetchAdmin('a.local'),
|
||||||
|
fetchAdmin('b.local'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe('User', () => {
|
||||||
|
describe('Profile', async () => {
|
||||||
|
describe('Consistency of profile', async () => {
|
||||||
|
const [alice] = await createAccount('a.local', aAdminClient);
|
||||||
|
const [
|
||||||
|
[, aliceWatcherClient],
|
||||||
|
[, aliceWatcherInBServerClient],
|
||||||
|
] = await Promise.all([
|
||||||
|
createAccount('a.local', aAdminClient),
|
||||||
|
createAccount('b.local', bAdminClient),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const aliceInAServer = await aliceWatcherClient.request('users/show', { userId: alice.id });
|
||||||
|
|
||||||
|
const resolved = await (async (): Promise<Misskey.entities.ApShowResponse & { type: 'User' }> => {
|
||||||
|
const resolved = await aliceWatcherInBServerClient.request('ap/show', {
|
||||||
|
uri: `https://a.local/@${aliceInAServer.username}`,
|
||||||
|
});
|
||||||
|
strictEqual(resolved.type, 'User');
|
||||||
|
return resolved;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const aliceInBServer = await aliceWatcherInBServerClient.request('users/show', { userId: resolved.object.id });
|
||||||
|
|
||||||
|
// console.log(`a.local: ${JSON.stringify(aliceInAServer, null, '\t')}`);
|
||||||
|
// console.log(`b.local: ${JSON.stringify(aliceInBServer, null, '\t')}`);
|
||||||
|
|
||||||
|
const toBeDeleted: (keyof Misskey.entities.UserDetailedNotMe)[] = [
|
||||||
|
'id',
|
||||||
|
'host',
|
||||||
|
'avatarUrl',
|
||||||
|
'instance',
|
||||||
|
'badgeRoles',
|
||||||
|
'url',
|
||||||
|
'uri',
|
||||||
|
'createdAt',
|
||||||
|
'lastFetchedAt',
|
||||||
|
'publicReactions',
|
||||||
|
];
|
||||||
|
const _aliceInAServer: Partial<Misskey.entities.UserDetailedNotMe> = structuredClone(aliceInAServer);
|
||||||
|
const _aliceInBServer: Partial<Misskey.entities.UserDetailedNotMe> = structuredClone(aliceInBServer);
|
||||||
|
for (const alice of [_aliceInAServer, _aliceInBServer]) {
|
||||||
|
for (const field of toBeDeleted) {
|
||||||
|
delete alice[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deepStrictEqual(_aliceInAServer, _aliceInBServer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Follow / Unfollow', async () => {
|
||||||
|
const [alice, aliceClient, { username: aliceUsername }] = await createAccount('a.local', aAdminClient);
|
||||||
|
const [bob, bobClient, { username: bobUsername }] = await createAccount('b.local', bAdminClient);
|
||||||
|
|
||||||
|
const aliceAcct = `@${aliceUsername}@a.local`;
|
||||||
|
const bobAcct = `@${bobUsername}@b.local`;
|
||||||
|
|
||||||
|
const [bobInAServer, aliceInBServer] = await Promise.all([
|
||||||
|
resolveRemoteAccount(aliceAcct, bobAcct, aliceClient),
|
||||||
|
resolveRemoteAccount(bobAcct, aliceAcct, bobClient),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await describe('Follow a.local ==> b.local', async () => {
|
||||||
|
before(async () => {
|
||||||
|
console.log(`Following ${bobAcct} from ${aliceAcct} ...`);
|
||||||
|
await aliceClient.request('following/create', { userId: bobInAServer.object.id });
|
||||||
|
console.log(`Followed ${bobAcct} from ${aliceAcct}`);
|
||||||
|
|
||||||
|
// wait for 1 secound
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
deepEqual(
|
||||||
|
(await aliceClient.request('users/following', { userId: alice.id }))
|
||||||
|
.some(v => v.followeeId === bobInAServer.object.id),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
deepEqual(
|
||||||
|
(await bobClient.request('users/followers', { userId: bob.id }))
|
||||||
|
.some(v => v.followerId === aliceInBServer.object.id),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await describe('Unfollow a.local ==> b.local', async () => {
|
||||||
|
before(async () => {
|
||||||
|
console.log(`Unfollowing ${bobAcct} from ${aliceAcct} ...`);
|
||||||
|
await aliceClient.request('following/delete', { userId: bobInAServer.object.id });
|
||||||
|
console.log(`Unfollowed ${bobAcct} from ${aliceAcct}`);
|
||||||
|
|
||||||
|
// wait for 1 secound
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
deepEqual(
|
||||||
|
(await aliceClient.request('users/following', { userId: alice.id }))
|
||||||
|
.some(v => v.followeeId === bobInAServer.object.id),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
deepEqual(
|
||||||
|
(await bobClient.request('users/followers', { userId: bob.id }))
|
||||||
|
.some(v => v.followerId === aliceInBServer.object.id),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { strictEqual } from 'assert';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { SwitchCaseResponseType } from 'misskey-js/api.types.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
/** used for avoiding overload and some endpoints */
|
||||||
|
type Request = <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
|
||||||
|
endpoint: E, params: P, credential?: string | null
|
||||||
|
) => Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
export const ADMIN_PARAMS = { username: 'admin', password: 'admin' };
|
||||||
|
|
||||||
|
export async function signin(host: string, params: Misskey.entities.SigninRequest): Promise<Misskey.entities.SigninResponse> {
|
||||||
|
// wait for a second to prevent hit rate limit
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
console.log(`Sign in to @${params.username}@${host} ...`);
|
||||||
|
return await (new Misskey.api.APIClient({
|
||||||
|
origin: `https://${host}`,
|
||||||
|
fetch: (input, init) => fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...init?.headers,
|
||||||
|
'Content-Type': init?.headers['Content-Type'] != null ? init.headers['Content-Type'] : 'application/json',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).request as Request)('signin', params)
|
||||||
|
.then(res => {
|
||||||
|
console.log(`Signed in to @${params.username}@${host}`);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(async err => {
|
||||||
|
if (err.id === '22d05606-fbcf-421a-a2db-b32610dcfd1b') {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, Math.random() * 5000));
|
||||||
|
return await signin(host, params);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAdmin(host: string): 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 => {
|
||||||
|
console.log(`Successfully created admin account: @${ADMIN_PARAMS.username}@${host}`);
|
||||||
|
return res as Misskey.entities.SignupResponse;
|
||||||
|
}).then(async res => {
|
||||||
|
await client.request('admin/roles/update-default-policies', {
|
||||||
|
policies: {
|
||||||
|
rateLimitFactor: 0 as never,
|
||||||
|
},
|
||||||
|
}, res.token);
|
||||||
|
return res;
|
||||||
|
}).catch(err => {
|
||||||
|
if (err.info.e.message === 'access denied') {
|
||||||
|
console.log(`Admin account already exists: @${ADMIN_PARAMS.username}@${host}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAdmin(host: string): Promise<[Misskey.entities.SigninResponse, Misskey.api.APIClient]> {
|
||||||
|
const admin = 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);
|
||||||
|
} else if (err.id === '22d05606-fbcf-421a-a2db-b32610dcfd1b') {
|
||||||
|
return await signin(host, ADMIN_PARAMS);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [admin, new Misskey.api.APIClient({ origin: `https://${host}`, credential: admin.i })];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAccount(host: string, adminClient: Misskey.api.APIClient): Promise<[Misskey.entities.SigninResponse, Misskey.api.APIClient, { username: string; password: string }]> {
|
||||||
|
const username = crypto.randomUUID().replaceAll('-', '').substring(0, 20);
|
||||||
|
const password = crypto.randomUUID().replaceAll('-', '');
|
||||||
|
await adminClient.request('admin/accounts/create', { username, password });
|
||||||
|
console.log(`Created an account: @${username}@${host}`);
|
||||||
|
const signinRes = await signin(host, { username, password });
|
||||||
|
|
||||||
|
return [
|
||||||
|
signinRes,
|
||||||
|
new Misskey.api.APIClient({ origin: `https://${host}`, credential: signinRes.i }),
|
||||||
|
{ username, password },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAcct(acct: string): { username: string; host: string | null } {
|
||||||
|
const split = (acct.startsWith('@') ? acct.substring(1) : acct).split('@', 2);
|
||||||
|
return { username: split[0], host: split[1] ?? null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveRemoteAccount(from_acct: string, to_acct: string, fromClient?: Misskey.api.APIClient): Promise<Misskey.entities.ApShowResponse & { type: 'User' }> {
|
||||||
|
const [from, to] = [parseAcct(from_acct), parseAcct(to_acct)];
|
||||||
|
const fromAdminClient: Misskey.api.APIClient = fromClient ?? (await fetchAdmin(from.username))[1];
|
||||||
|
|
||||||
|
return new Promise<Misskey.entities.ApShowResponse & { type: 'User' }>((resolve, reject) => {
|
||||||
|
console.log(`Resolving @${to.username}@${to.host} from @${from.username}@${from.host} ...`);
|
||||||
|
fromAdminClient.request('ap/show', { uri: `https://${to.host}/@${to.username}` })
|
||||||
|
.then(res => {
|
||||||
|
console.log(`Resolved @${to.username}@${to.host} from @${from.username}@${from.host}`);
|
||||||
|
strictEqual(res.type, 'User');
|
||||||
|
strictEqual(res.object.url, `https://${to.host}/@${to.username}`);
|
||||||
|
resolve(res);
|
||||||
|
})
|
||||||
|
.catch(err => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(host: string, path: string, token: string): 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', token);
|
||||||
|
body.append('force', 'true');
|
||||||
|
body.append('file', blob);
|
||||||
|
body.append('name', filename);
|
||||||
|
|
||||||
|
return new Promise<Misskey.entities.DriveFile>((resolve, reject) => {
|
||||||
|
fetch(`https://${host}/api/drive/files/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}).then(async res => {
|
||||||
|
resolve(await res.json());
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue