diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml new file mode 100644 index 0000000000..9ca28fe328 --- /dev/null +++ b/.github/workflows/test-federation.yml @@ -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 diff --git a/.gitignore b/.gitignore index b270d5cb3a..5b8a798ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ coverage !/.config/docker_example.env !/.config/cypress-devcontainer.yml docker-compose.yml -compose.yml +./compose.yml .devcontainer/compose.yml !/.devcontainer/compose.yml diff --git a/packages/backend/package.json b/packages/backend/package.json index a06fd9156b..55452d6539 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -16,6 +16,7 @@ "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", "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", "restart": "pnpm build && pnpm start", "dev": "node ./scripts/dev.mjs", diff --git a/packages/backend/test-federation/.config/a.local.conf b/packages/backend/test-federation/.config/a.local.conf new file mode 100644 index 0000000000..cbb65b6e05 --- /dev/null +++ b/packages/backend/test-federation/.config/a.local.conf @@ -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; + } +} diff --git a/packages/backend/test-federation/.config/b.local.conf b/packages/backend/test-federation/.config/b.local.conf new file mode 100644 index 0000000000..380b2ceef3 --- /dev/null +++ b/packages/backend/test-federation/.config/b.local.conf @@ -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; + } +} diff --git a/packages/backend/test-federation/.config/default.a.yml b/packages/backend/test-federation/.config/default.a.yml new file mode 100644 index 0000000000..9a1456afde --- /dev/null +++ b/packages/backend/test-federation/.config/default.a.yml @@ -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' +] diff --git a/packages/backend/test-federation/.config/default.b.yml b/packages/backend/test-federation/.config/default.b.yml new file mode 100644 index 0000000000..0785cd7391 --- /dev/null +++ b/packages/backend/test-federation/.config/default.b.yml @@ -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' +] diff --git a/packages/backend/test-federation/.config/docker.env b/packages/backend/test-federation/.config/docker.env new file mode 100644 index 0000000000..b2a0177c84 --- /dev/null +++ b/packages/backend/test-federation/.config/docker.env @@ -0,0 +1,4 @@ +NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt +POSTGRES_DB=misskey +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres diff --git a/packages/backend/test-federation/.env.example b/packages/backend/test-federation/.env.example new file mode 100644 index 0000000000..97ec4d3594 --- /dev/null +++ b/packages/backend/test-federation/.env.example @@ -0,0 +1 @@ +TESTER_IP_ADDRESS=172.20.1.1 diff --git a/packages/backend/test-federation/.gitignore b/packages/backend/test-federation/.gitignore new file mode 100644 index 0000000000..dd03bf2cab --- /dev/null +++ b/packages/backend/test-federation/.gitignore @@ -0,0 +1,3 @@ +certificates +volumes +.env diff --git a/packages/backend/test-federation/README.md b/packages/backend/test-federation/README.md new file mode 100644 index 0000000000..c69a0c611f --- /dev/null +++ b/packages/backend/test-federation/README.md @@ -0,0 +1,7 @@ +Execute following commands: +```sh +cp ./.env.example ./.env +bash ./generate_certificates.sh +pnpm build:fed +docker compose up +``` diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml new file mode 100644 index 0000000000..3046c166df --- /dev/null +++ b/packages/backend/test-federation/compose.a.yml @@ -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 diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml new file mode 100644 index 0000000000..7dddac905c --- /dev/null +++ b/packages/backend/test-federation/compose.b.yml @@ -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 diff --git a/packages/backend/test-federation/compose.override.yaml b/packages/backend/test-federation/compose.override.yaml new file mode 100644 index 0000000000..a35b540ca7 --- /dev/null +++ b/packages/backend/test-federation/compose.override.yaml @@ -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: diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml new file mode 100644 index 0000000000..27a018847f --- /dev/null +++ b/packages/backend/test-federation/compose.yml @@ -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 diff --git a/packages/backend/test-federation/daemon.ts b/packages/backend/test-federation/daemon.ts new file mode 100644 index 0000000000..8eb275d4c3 --- /dev/null +++ b/packages/backend/test-federation/daemon.ts @@ -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); +} diff --git a/packages/backend/test-federation/eslint.config.js b/packages/backend/test-federation/eslint.config.js new file mode 100644 index 0000000000..a0f43babad --- /dev/null +++ b/packages/backend/test-federation/eslint.config.js @@ -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, + }, + }, + }, +]; diff --git a/packages/backend/test-federation/generate_certificates.sh b/packages/backend/test-federation/generate_certificates.sh new file mode 100644 index 0000000000..eae68ba839 --- /dev/null +++ b/packages/backend/test-federation/generate_certificates.sh @@ -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 diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts new file mode 100644 index 0000000000..892d75488b --- /dev/null +++ b/packages/backend/test-federation/test/drive.test.ts @@ -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 => { + 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 = structuredClone(image); + const _ImageInBServer: Partial = 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 => { + 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'); + }); + }); +}); diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts new file mode 100644 index 0000000000..1e691d1852 --- /dev/null +++ b/packages/backend/test-federation/test/user.test.ts @@ -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 => { + 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 = structuredClone(aliceInAServer); + const _aliceInBServer: Partial = 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, + ), + ]); + }); + }); + }); +}); diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts new file mode 100644 index 0000000000..77a915ae35 --- /dev/null +++ b/packages/backend/test-federation/test/utils.ts @@ -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 = ( + endpoint: E, params: P, credential?: string | null +) => Promise>; + +export const ADMIN_PARAMS = { username: 'admin', password: 'admin' }; + +export async function signin(host: string, params: Misskey.entities.SigninRequest): Promise { + // 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 { + 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 { + const [from, to] = [parseAcct(from_acct), parseAcct(to_acct)]; + const fromAdminClient: Misskey.api.APIClient = fromClient ?? (await fetchAdmin(from.username))[1]; + + return new Promise((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 { + 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((resolve, reject) => { + fetch(`https://${host}/api/drive/files/create`, { + method: 'POST', + body, + }).then(async res => { + resolve(await res.json()); + }).catch(err => { + reject(err); + }); + }); +} diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json new file mode 100644 index 0000000000..3a1cb3b9f3 --- /dev/null +++ b/packages/backend/test-federation/tsconfig.json @@ -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 ''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" + ] +}