From eb1a21265f6c93f07369bdd586a608359ccf0614 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Fri, 1 Sep 2023 14:41:29 -0500 Subject: [PATCH] refactor(server): use ava (#4120) --- apps/server/package.json | 29 +- apps/server/scripts/run-test.ts | 73 --- apps/server/src/app.ts | 3 +- apps/server/src/index.ts | 2 + apps/server/src/modules/auth/index.ts | 2 + apps/server/src/modules/auth/resolver.ts | 2 +- apps/server/src/modules/index.ts | 13 +- apps/server/src/tests/app.e2e.ts | 183 ++++--- apps/server/src/tests/auth.spec.ts | 14 +- apps/server/src/tests/config.spec.ts | 15 +- apps/server/src/tests/doc.spec.ts | 263 +++++----- apps/server/src/tests/mailer.spec.ts | 139 +++--- .../src/tests/prometheus-metrics.spec.ts | 12 +- apps/server/src/tests/session.spec.ts | 14 +- apps/server/src/tests/user.spec.ts | 103 ++-- apps/server/src/tests/workspace-blobs.spec.ts | 224 ++++----- .../server/src/tests/workspace-invite.spec.ts | 299 ++++++------ apps/server/src/tests/workspace.spec.ts | 425 ++++++++--------- yarn.lock | 450 +++++++++++++++++- 19 files changed, 1313 insertions(+), 952 deletions(-) delete mode 100755 apps/server/scripts/run-test.ts diff --git a/apps/server/package.json b/apps/server/package.json index b7b6cffe17..4787a45a99 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -11,11 +11,8 @@ "build": "tsc", "start": "node --loader ts-node/esm.mjs --es-module-specifier-resolution node ./src/index.ts", "dev": "nodemon ./src/index.ts", - "test": "yarn exec ts-node-esm ./scripts/run-test.ts all", - "test:select": "yarn exec ts-node-esm ./scripts/run-test.ts", - "test:watch": "yarn exec ts-node-esm ./scripts/run-test.ts all --watch", - "test:select:watch": "yarn exec ts-node-esm ./scripts/run-test.ts --watch", - "test:coverage": "c8 yarn exec ts-node-esm ./scripts/run-test.ts all", + "test": "ava --concurrency 1 --serial", + "test:coverage": "c8 ava --concurrency 1 --serial", "postinstall": "prisma generate" }, "dependencies": { @@ -90,6 +87,7 @@ "@types/sinon": "^10.0.16", "@types/supertest": "^2.0.12", "@types/ws": "^8.5.5", + "ava": "^5.3.1", "c8": "^8.0.1", "nodemon": "^3.0.1", "sinon": "^15.2.0", @@ -97,6 +95,27 @@ "ts-node": "^10.9.1", "typescript": "^5.2.2" }, + "ava": { + "extensions": { + "ts": "module" + }, + "nodeArguments": [ + "--loader", + "ts-node/esm.mjs", + "--es-module-specifier-resolution", + "node" + ], + "files": [ + "src/**/*.spec.ts" + ], + "require": [ + "./src/prelude.ts" + ], + "environmentVariables": { + "TS_NODE_PROJECT": "./tsconfig.json", + "NODE_ENV": "test" + } + }, "nodemonConfig": { "exec": "node", "script": "./src/index.ts", diff --git a/apps/server/scripts/run-test.ts b/apps/server/scripts/run-test.ts deleted file mode 100755 index b732d5feaa..0000000000 --- a/apps/server/scripts/run-test.ts +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env ts-node-esm -import { resolve } from 'node:path'; - -import * as p from '@clack/prompts'; -import { spawn, spawnSync } from 'child_process'; -import { readdir } from 'fs/promises'; -import * as process from 'process'; -import { fileURLToPath } from 'url'; - -import pkg from '../package.json' assert { type: 'json' }; - -const root = fileURLToPath(new URL('..', import.meta.url)); -const testDir = resolve(root, 'src', 'tests'); -const files = await readdir(testDir); - -const watchMode = process.argv.includes('--watch'); - -const sharedArgs = [ - ...pkg.nodemonConfig.nodeArgs, - '--test', - watchMode ? '--watch' : '', -]; - -const env = { - PATH: process.env.PATH, - NODE_ENV: 'test', - DATABASE_URL: process.env.DATABASE_URL, - NODE_NO_WARNINGS: '1', -}; - -if (process.argv[2] === 'all') { - files.forEach(file => { - const path = resolve(testDir, file); - spawnSync('node', [...sharedArgs, path], { - cwd: root, - env, - stdio: 'inherit', - shell: true, - }); - }); -} else { - const result = await p.group({ - file: () => - p.select({ - message: 'Select a file to run', - options: files.map(file => ({ - label: file, - value: file as any, - })), - }), - }); - - const target = resolve(testDir, result.file); - - const cp = spawn( - 'node', - [ - ...sharedArgs, - '--test-reporter=spec', - '--test-reporter-destination=stdout', - target, - ], - { - cwd: root, - env, - stdio: 'inherit', - shell: true, - } - ); - cp.on('exit', code => { - process.exit(code ?? 0); - }); -} diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 822e559d3c..25cecb3a88 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -3,7 +3,7 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { ConfigModule } from './config'; import { MetricsModule } from './metrics'; -import { BusinessModules, Providers } from './modules'; +import { BusinessModules } from './modules'; import { PrismaModule } from './prisma'; import { SessionModule } from './session'; import { StorageModule } from './storage'; @@ -19,7 +19,6 @@ import { RateLimiterModule } from './throttler'; RateLimiterModule, ...BusinessModules, ], - providers: Providers, controllers: [AppController], }) export class AppModule {} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index c8fc02bd2e..02e041bd86 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -24,6 +24,7 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from './app'; import { Config } from './config'; +import { ExceptionLogger } from './middleware/exception-logger'; import { serverTimingAndCache } from './middleware/timing'; import { RedisIoAdapter } from './modules/sync/redis-adapter'; @@ -75,6 +76,7 @@ app.use( }) ); +app.useGlobalFilters(new ExceptionLogger()); app.use(cookieParser()); const config = app.get(Config); diff --git a/apps/server/src/modules/auth/index.ts b/apps/server/src/modules/auth/index.ts index f8c284d870..ca10b40c0f 100644 --- a/apps/server/src/modules/auth/index.ts +++ b/apps/server/src/modules/auth/index.ts @@ -1,5 +1,6 @@ import { Global, Module } from '@nestjs/common'; +import { SessionService } from '../../session'; import { MAILER, MailService } from './mailer'; import { NextAuthController } from './next-auth.controller'; import { NextAuthOptionsProvider } from './next-auth-options'; @@ -10,6 +11,7 @@ import { AuthService } from './service'; @Module({ providers: [ AuthService, + SessionService, AuthResolver, NextAuthOptionsProvider, MAILER, diff --git a/apps/server/src/modules/auth/resolver.ts b/apps/server/src/modules/auth/resolver.ts index 55bd32be1b..9b85929e20 100644 --- a/apps/server/src/modules/auth/resolver.ts +++ b/apps/server/src/modules/auth/resolver.ts @@ -43,7 +43,7 @@ export class TokenType { export class AuthResolver { constructor( private readonly config: Config, - private auth: AuthService, + private readonly auth: AuthService, private readonly session: SessionService ) {} diff --git a/apps/server/src/modules/index.ts b/apps/server/src/modules/index.ts index 10086a6944..c11e3428b1 100644 --- a/apps/server/src/modules/index.ts +++ b/apps/server/src/modules/index.ts @@ -1,8 +1,6 @@ -import { DynamicModule, Provider, Type } from '@nestjs/common'; -import { APP_FILTER } from '@nestjs/core'; +import { DynamicModule, Type } from '@nestjs/common'; import { GqlModule } from '../graphql.module'; -import { ExceptionLogger } from '../middleware/exception-logger'; import { AuthModule } from './auth'; import { DocModule } from './doc'; import { SyncModule } from './sync'; @@ -39,11 +37,4 @@ switch (SERVER_FLAVOR) { break; } -const Providers: Provider[] = [ - { - provide: APP_FILTER, - useClass: ExceptionLogger, - }, -]; - -export { BusinessModules, Providers }; +export { BusinessModules }; diff --git a/apps/server/src/tests/app.e2e.ts b/apps/server/src/tests/app.e2e.ts index ba07aca88a..4232eb4b1c 100644 --- a/apps/server/src/tests/app.e2e.ts +++ b/apps/server/src/tests/app.e2e.ts @@ -1,11 +1,11 @@ import { equal, ok } from 'node:assert'; -import { afterEach, beforeEach, describe, test } from 'node:test'; import { Transformer } from '@napi-rs/image'; import type { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { hash } from '@node-rs/argon2'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; import { Express } from 'express'; // @ts-expect-error graphql-upload is not typed import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; @@ -15,83 +15,82 @@ import { AppModule } from '../app'; const gql = '/graphql'; -describe('AppModule', async () => { - let app: INestApplication; +let app: INestApplication; - // cleanup database before each test - beforeEach(async () => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.user.create({ - data: { - name: 'Alex Yang', - email: 'alex.yang@example.org', - password: await hash('123456'), - }, - }); - await client.$disconnect(); +// cleanup database before each test +test.beforeEach(async () => { + const client = new PrismaClient(); + await client.$connect(); + await client.user.deleteMany({}); + await client.user.create({ + data: { + name: 'Alex Yang', + email: 'alex.yang@example.org', + password: await hash('123456'), + }, }); + await client.$disconnect(); +}); - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = module.createNestApplication({ - cors: true, - bodyParser: true, - }); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); +test.beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication({ + cors: true, + bodyParser: true, }); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); +}); - afterEach(async () => { - await app.close(); - }); +test.afterEach(async () => { + await app.close(); +}); - await test('should init app', async () => { - ok(typeof app === 'object'); - await request(app.getHttpServer()) - .post(gql) - .send({ - query: ` +test('should init app', async () => { + ok(typeof app === 'object'); + await request(app.getHttpServer()) + .post(gql) + .send({ + query: ` query { error } `, - }) - .expect(400); + }) + .expect(400); - const { token } = await createToken(app); + const { token } = await createToken(app); - await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .send({ - query: ` + await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .send({ + query: ` query { __typename } `, - }) - .expect(200) - .expect(res => { - ok(res.body.data.__typename === 'Query'); - }); - }); + }) + .expect(200) + .expect(res => { + ok(res.body.data.__typename === 'Query'); + }); +}); - await test('should find default user', async () => { - const { token } = await createToken(app); - await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .send({ - query: ` +test('should find default user', async () => { + const { token } = await createToken(app); + await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .send({ + query: ` query { user(email: "alex.yang@example.org") { email @@ -99,29 +98,29 @@ describe('AppModule', async () => { } } `, - }) - .expect(200) - .expect(res => { - equal(res.body.data.user.email, 'alex.yang@example.org'); - }); - }); + }) + .expect(200) + .expect(res => { + equal(res.body.data.user.email, 'alex.yang@example.org'); + }); +}); - await test('should be able to upload avatar', async () => { - const { token, id } = await createToken(app); - const png = await Transformer.fromRgbaPixels( - Buffer.alloc(400 * 400 * 4).fill(255), - 400, - 400 - ).png(); +test('should be able to upload avatar', async () => { + const { token, id } = await createToken(app); + const png = await Transformer.fromRgbaPixels( + Buffer.alloc(400 * 400 * 4).fill(255), + 400, + 400 + ).png(); - await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .field( - 'operations', - JSON.stringify({ - name: 'uploadAvatar', - query: `mutation uploadAvatar($id: String!, $avatar: Upload!) { + await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .field( + 'operations', + JSON.stringify({ + name: 'uploadAvatar', + query: `mutation uploadAvatar($id: String!, $avatar: Upload!) { uploadAvatar(id: $id, avatar: $avatar) { id name @@ -130,17 +129,15 @@ describe('AppModule', async () => { } } `, - variables: { id, avatar: null }, - }) - ) - - .field('map', JSON.stringify({ '0': ['variables.avatar'] })) - .attach('0', png, 'avatar.png') - .expect(200) - .expect(res => { - equal(res.body.data.uploadAvatar.id, id); - }); - }); + variables: { id, avatar: null }, + }) + ) + .field('map', JSON.stringify({ '0': ['variables.avatar'] })) + .attach('0', png, 'avatar.png') + .expect(200) + .expect(res => { + equal(res.body.data.uploadAvatar.id, id); + }); }); async function createToken(app: INestApplication): Promise<{ diff --git a/apps/server/src/tests/auth.spec.ts b/apps/server/src/tests/auth.spec.ts index f3a700d4a5..7215b5f19c 100644 --- a/apps/server/src/tests/auth.spec.ts +++ b/apps/server/src/tests/auth.spec.ts @@ -1,9 +1,9 @@ /// import { equal } from 'node:assert'; -import { afterEach, beforeEach, test } from 'node:test'; import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; import { ConfigModule } from '../config'; import { GqlModule } from '../graphql.module'; @@ -17,13 +17,13 @@ let auth: AuthService; let module: TestingModule; // cleanup database before each test -beforeEach(async () => { +test.beforeEach(async () => { const client = new PrismaClient(); await client.$connect(); await client.user.deleteMany({}); }); -beforeEach(async () => { +test.beforeEach(async () => { module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ @@ -43,16 +43,17 @@ beforeEach(async () => { auth = module.get(AuthService); }); -afterEach(async () => { +test.afterEach(async () => { await module.close(); }); -test('should be able to register and signIn', async () => { +test('should be able to register and signIn', async t => { await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); await auth.signIn('alexyang@example.org', '123456'); + t.pass(); }); -test('should be able to verify', async () => { +test('should be able to verify', async t => { await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); await auth.signIn('alexyang@example.org', '123456'); const date = new Date(); @@ -83,4 +84,5 @@ test('should be able to verify', async () => { equal(claim.emailVerified?.toISOString(), date.toISOString()); equal(claim.createdAt.toISOString(), date.toISOString()); } + t.pass(); }); diff --git a/apps/server/src/tests/config.spec.ts b/apps/server/src/tests/config.spec.ts index 106c5c81df..ddc76573d8 100644 --- a/apps/server/src/tests/config.spec.ts +++ b/apps/server/src/tests/config.spec.ts @@ -1,24 +1,24 @@ -import { equal, ok } from 'node:assert'; -import { beforeEach, test } from 'node:test'; +import { ok } from 'node:assert'; import { Test } from '@nestjs/testing'; +import test from 'ava'; import { Config, ConfigModule } from '../config'; let config: Config; -beforeEach(async () => { +test.beforeEach(async () => { const module = await Test.createTestingModule({ imports: [ConfigModule.forRoot()], }).compile(); config = module.get(Config); }); -test('should be able to get config', () => { - ok(typeof config.host === 'string'); - equal(config.env, 'test'); +test('should be able to get config', t => { + t.true(typeof config.host === 'string'); + t.is(config.env, 'test'); }); -test('should be able to override config', async () => { +test('should be able to override config', async t => { const module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ @@ -29,4 +29,5 @@ test('should be able to override config', async () => { const config = module.get(Config); ok(config.host, 'testing'); + t.pass(); }); diff --git a/apps/server/src/tests/doc.spec.ts b/apps/server/src/tests/doc.spec.ts index 00865c921b..bcf339677d 100644 --- a/apps/server/src/tests/doc.spec.ts +++ b/apps/server/src/tests/doc.spec.ts @@ -1,8 +1,9 @@ import { deepEqual, equal, ok } from 'node:assert'; -import { afterEach, beforeEach, mock, test } from 'node:test'; +import { mock } from 'node:test'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import test from 'ava'; import { register } from 'prom-client'; import * as Sinon from 'sinon'; import { Doc as YDoc, encodeStateAsUpdate } from 'yjs'; @@ -24,135 +25,137 @@ const createModule = () => { }).compile(); }; -test('Doc Module', async t => { - let app: INestApplication; - let m: TestingModule; - let timer: Sinon.SinonFakeTimers; +let app: INestApplication; +let m: TestingModule; +let timer: Sinon.SinonFakeTimers; - // cleanup database before each test - beforeEach(async () => { - timer = Sinon.useFakeTimers({ - toFake: ['setInterval'], - }); - await flushDB(); - m = await createModule(); - app = m.createNestApplication(); - app.enableShutdownHooks(); - await app.init(); - }); - - afterEach(async () => { - await app.close(); - timer.restore(); - }); - - await t.test('should setup update poll interval', async () => { - register.clear(); - const m = await createModule(); - const manager = m.get(DocManager); - const fake = mock.method(manager, 'setup'); - - await m.createNestApplication().init(); - - equal(fake.mock.callCount(), 1); - // @ts-expect-error private member - ok(manager.job); - }); - - await t.test('should be able to stop poll', async () => { - const manager = m.get(DocManager); - const fake = mock.method(manager, 'destroy'); - - await app.close(); - - equal(fake.mock.callCount(), 1); - // @ts-expect-error private member - equal(manager.job, null); - }); - - await t.test('should poll when intervel due', async () => { - const manager = m.get(DocManager); - const interval = m.get(Config).doc.manager.updatePollInterval; - - let resolve: any; - const fake = mock.method(manager, 'apply', () => { - return new Promise(_resolve => { - resolve = _resolve; - }); - }); - - timer.tick(interval); - equal(fake.mock.callCount(), 1); - - // busy - timer.tick(interval); - // @ts-expect-error private member - equal(manager.busy, true); - equal(fake.mock.callCount(), 1); - - resolve(); - await timer.tickAsync(1); - - // @ts-expect-error private member - equal(manager.busy, false); - timer.tick(interval); - equal(fake.mock.callCount(), 2); - }); - - await t.test('should merge update when intervel due', async () => { - const db = m.get(PrismaService); - const manager = m.get(DocManager); - - const doc = new YDoc(); - const text = doc.getText('content'); - text.insert(0, 'hello'); - const update = encodeStateAsUpdate(doc); - - const ws = await db.workspace.create({ - data: { - id: '1', - public: false, - }, - }); - - await db.update.createMany({ - data: [ - { - id: '1', - workspaceId: '1', - blob: Buffer.from([0, 0]), - }, - { - id: '1', - workspaceId: '1', - blob: Buffer.from(update), - }, - ], - }); - - await manager.apply(); - - deepEqual(await manager.getLatestUpdate(ws.id, '1'), update); - - let appendUpdate = Buffer.from([]); - doc.on('update', update => { - appendUpdate = Buffer.from(update); - }); - text.insert(5, 'world'); - - await db.update.create({ - data: { - workspaceId: ws.id, - id: '1', - blob: appendUpdate, - }, - }); - - await manager.apply(); - - deepEqual( - await manager.getLatestUpdate(ws.id, '1'), - encodeStateAsUpdate(doc) - ); +// cleanup database before each test +test.beforeEach(async () => { + timer = Sinon.useFakeTimers({ + toFake: ['setInterval'], }); + await flushDB(); + m = await createModule(); + app = m.createNestApplication(); + app.enableShutdownHooks(); + await app.init(); +}); + +test.afterEach(async () => { + await app.close(); + timer.restore(); +}); + +test('should setup update poll interval', async t => { + register.clear(); + const m = await createModule(); + const manager = m.get(DocManager); + const fake = mock.method(manager, 'setup'); + + await m.createNestApplication().init(); + + equal(fake.mock.callCount(), 1); + // @ts-expect-error private member + ok(manager.job); + t.pass(); +}); + +test('should be able to stop poll', async t => { + const manager = m.get(DocManager); + const fake = mock.method(manager, 'destroy'); + + await app.close(); + + equal(fake.mock.callCount(), 1); + // @ts-expect-error private member + equal(manager.job, null); + t.pass(); +}); + +test('should poll when intervel due', async t => { + const manager = m.get(DocManager); + const interval = m.get(Config).doc.manager.updatePollInterval; + + let resolve: any; + const fake = mock.method(manager, 'apply', () => { + return new Promise(_resolve => { + resolve = _resolve; + }); + }); + + timer.tick(interval); + equal(fake.mock.callCount(), 1); + + // busy + timer.tick(interval); + // @ts-expect-error private member + equal(manager.busy, true); + equal(fake.mock.callCount(), 1); + + resolve(); + await timer.tickAsync(1); + + // @ts-expect-error private member + equal(manager.busy, false); + timer.tick(interval); + equal(fake.mock.callCount(), 2); + t.pass(); +}); + +test('should merge update when intervel due', async t => { + const db = m.get(PrismaService); + const manager = m.get(DocManager); + + const doc = new YDoc(); + const text = doc.getText('content'); + text.insert(0, 'hello'); + const update = encodeStateAsUpdate(doc); + + const ws = await db.workspace.create({ + data: { + id: '1', + public: false, + }, + }); + + await db.update.createMany({ + data: [ + { + id: '1', + workspaceId: '1', + blob: Buffer.from([0, 0]), + }, + { + id: '1', + workspaceId: '1', + blob: Buffer.from(update), + }, + ], + }); + + await manager.apply(); + + deepEqual(await manager.getLatestUpdate(ws.id, '1'), update); + + let appendUpdate = Buffer.from([]); + doc.on('update', update => { + appendUpdate = Buffer.from(update); + }); + text.insert(5, 'world'); + + await db.update.create({ + data: { + workspaceId: ws.id, + id: '1', + blob: appendUpdate, + }, + }); + + await manager.apply(); + + deepEqual( + await manager.getLatestUpdate(ws.id, '1'), + encodeStateAsUpdate(doc) + ); + t.pass(); }); diff --git a/apps/server/src/tests/mailer.spec.ts b/apps/server/src/tests/mailer.spec.ts index f641f69fa7..87e76526a4 100644 --- a/apps/server/src/tests/mailer.spec.ts +++ b/apps/server/src/tests/mailer.spec.ts @@ -1,9 +1,9 @@ import { ok } from 'node:assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import type { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; // @ts-expect-error graphql-upload is not typed import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; @@ -11,76 +11,75 @@ import { AppModule } from '../app'; import { MailService } from '../modules/auth/mailer'; import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils'; -describe('Mail Module', () => { - let app: INestApplication; +let app: INestApplication; - const client = new PrismaClient(); +const client = new PrismaClient(); - let mail: MailService; +let mail: MailService; - // cleanup database before each test - beforeEach(async () => { - await client.$connect(); - await client.user.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.update.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - }); - - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - - mail = module.get(MailService); - }); - - afterEach(async () => { - await app.close(); - }); - - it('should send invite email', async () => { - if (mail.hasConfigured()) { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - const inviteId = await inviteUser( - app, - u1.token.token, - workspace.id, - u2.email, - 'Admin' - ); - - const inviteInfo = await getInviteInfo(app, u1.token.token, inviteId); - - const resp = await mail.sendInviteEmail( - 'production@toeverything.info', - inviteId, - { - workspace: { - id: inviteInfo.workspace.id, - name: inviteInfo.workspace.name, - avatar: '', - }, - user: { - avatar: inviteInfo.user?.avatarUrl || '', - name: inviteInfo.user?.name || '', - }, - } - ); - - ok(resp.accepted.length === 1, 'failed to send invite email'); - } - }); +// cleanup database before each test +test.beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.update.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); +}); + +test.beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); + + mail = module.get(MailService); +}); + +test.afterEach(async () => { + await app.close(); +}); + +test('should send invite email', async t => { + if (mail.hasConfigured()) { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + const inviteId = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + + const inviteInfo = await getInviteInfo(app, u1.token.token, inviteId); + + const resp = await mail.sendInviteEmail( + 'production@toeverything.info', + inviteId, + { + workspace: { + id: inviteInfo.workspace.id, + name: inviteInfo.workspace.name, + avatar: '', + }, + user: { + avatar: inviteInfo.user?.avatarUrl || '', + name: inviteInfo.user?.name || '', + }, + } + ); + + ok(resp.accepted.length === 1, 'failed to send invite email'); + } + t.pass(); }); diff --git a/apps/server/src/tests/prometheus-metrics.spec.ts b/apps/server/src/tests/prometheus-metrics.spec.ts index 3eb0b094c1..59a069dbb8 100644 --- a/apps/server/src/tests/prometheus-metrics.spec.ts +++ b/apps/server/src/tests/prometheus-metrics.spec.ts @@ -1,7 +1,7 @@ import { ok } from 'node:assert'; -import { afterEach, beforeEach, test } from 'node:test'; import { Test, TestingModule } from '@nestjs/testing'; +import test from 'ava'; import { register } from 'prom-client'; import { MetricsModule } from '../metrics'; @@ -11,7 +11,7 @@ import { PrismaModule } from '../prisma'; let metrics: Metrics; let module: TestingModule; -beforeEach(async () => { +test.beforeEach(async () => { module = await Test.createTestingModule({ imports: [MetricsModule, PrismaModule], }).compile(); @@ -19,11 +19,11 @@ beforeEach(async () => { metrics = module.get(Metrics); }); -afterEach(async () => { +test.afterEach(async () => { await module.close(); }); -test('should be able to increment counter', async () => { +test('should be able to increment counter', async t => { metrics.socketIOEventCounter(1, { event: 'client-handshake' }); const socketIOCounterMetric = await register.getSingleMetric('socket_io_counter'); @@ -33,9 +33,10 @@ test('should be able to increment counter', async () => { JSON.stringify((await socketIOCounterMetric.get()).values) === '[{"value":1,"labels":{"event":"client-handshake"}}]' ); + t.pass(); }); -test('should be able to timer', async () => { +test('should be able to timer', async t => { let minimum: number; { const endTimer = metrics.socketIOEventTimer({ event: 'client-handshake' }); @@ -76,4 +77,5 @@ test('should be able to timer', async () => { ); } } + t.pass(); }); diff --git a/apps/server/src/tests/session.spec.ts b/apps/server/src/tests/session.spec.ts index 8fd674b41e..aed6380540 100644 --- a/apps/server/src/tests/session.spec.ts +++ b/apps/server/src/tests/session.spec.ts @@ -1,9 +1,9 @@ /// import { equal } from 'node:assert'; -import { afterEach, beforeEach, test } from 'node:test'; import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; import { ConfigModule } from '../config'; import { SessionModule, SessionService } from '../session'; @@ -12,31 +12,33 @@ let session: SessionService; let module: TestingModule; // cleanup database before each test -beforeEach(async () => { +test.beforeEach(async () => { const client = new PrismaClient(); await client.$connect(); await client.user.deleteMany({}); }); -beforeEach(async () => { +test.beforeEach(async () => { module = await Test.createTestingModule({ imports: [ConfigModule.forRoot(), SessionModule], }).compile(); session = module.get(SessionService); }); -afterEach(async () => { +test.afterEach(async () => { await module.close(); }); -test('should be able to set session', async () => { +test('should be able to set session', async t => { await session.set('test', 'value'); equal(await session.get('test'), 'value'); + t.pass(); }); -test('should be expired by ttl', async () => { +test('should be expired by ttl', async t => { await session.set('test', 'value', 100); equal(await session.get('test'), 'value'); await new Promise(resolve => setTimeout(resolve, 500)); equal(await session.get('test'), undefined); + t.pass(); }); diff --git a/apps/server/src/tests/user.spec.ts b/apps/server/src/tests/user.spec.ts index dd55c1d8ad..5883aa368d 100644 --- a/apps/server/src/tests/user.spec.ts +++ b/apps/server/src/tests/user.spec.ts @@ -1,9 +1,9 @@ import { ok, rejects } from 'node:assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import type { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; // @ts-expect-error graphql-upload is not typed import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; @@ -11,66 +11,67 @@ import request from 'supertest'; import { AppModule } from '../app'; import { currentUser, signUp } from './utils'; -describe('User Module', () => { - let app: INestApplication; +let app: INestApplication; - // cleanup database before each test - beforeEach(async () => { - const client = new PrismaClient(); - await client.$connect(); - await client.user.deleteMany({}); - await client.$disconnect(); - }); +// cleanup database before each test +test.beforeEach(async () => { + const client = new PrismaClient(); + await client.$connect(); + await client.user.deleteMany({}); + await client.$disconnect(); +}); - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - }); +test.beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); +}); - afterEach(async () => { - await app.close(); - }); +test.afterEach(async () => { + await app.close(); +}); - it('should register a user', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); - ok(typeof user.id === 'string', 'user.id is not a string'); - ok(user.name === 'u1', 'user.name is not valid'); - ok(user.email === 'u1@affine.pro', 'user.email is not valid'); - }); +test('should register a user', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + ok(typeof user.id === 'string', 'user.id is not a string'); + ok(user.name === 'u1', 'user.name is not valid'); + ok(user.email === 'u1@affine.pro', 'user.email is not valid'); + t.pass(); +}); - it('should get current user', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); - const currUser = await currentUser(app, user.token.token); - ok(currUser.id === user.id, 'user.id is not valid'); - ok(currUser.name === user.name, 'user.name is not valid'); - ok(currUser.email === user.email, 'user.email is not valid'); - ok(currUser.hasPassword, 'currUser.hasPassword is not valid'); - }); +test('should get current user', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + const currUser = await currentUser(app, user.token.token); + ok(currUser.id === user.id, 'user.id is not valid'); + ok(currUser.name === user.name, 'user.name is not valid'); + ok(currUser.email === user.email, 'user.email is not valid'); + ok(currUser.hasPassword, 'currUser.hasPassword is not valid'); + t.pass(); +}); - it('should be able to delete user', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); - await request(app.getHttpServer()) - .post('/graphql') - .auth(user.token.token, { type: 'bearer' }) - .send({ - query: ` +test('should be able to delete user', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + await request(app.getHttpServer()) + .post('/graphql') + .auth(user.token.token, { type: 'bearer' }) + .send({ + query: ` mutation { deleteAccount { success } } `, - }) - .expect(200); - rejects(currentUser(app, user.token.token)); - }); + }) + .expect(200); + await rejects(currentUser(app, user.token.token)); + t.pass(); }); diff --git a/apps/server/src/tests/workspace-blobs.spec.ts b/apps/server/src/tests/workspace-blobs.spec.ts index 272d7569ca..e552150491 100644 --- a/apps/server/src/tests/workspace-blobs.spec.ts +++ b/apps/server/src/tests/workspace-blobs.spec.ts @@ -1,9 +1,9 @@ import { deepEqual, ok } from 'node:assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import type { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; // @ts-expect-error graphql-upload is not typed import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; @@ -18,116 +18,118 @@ import { signUp, } from './utils'; -describe('Workspace Module - Blobs', () => { - let app: INestApplication; +let app: INestApplication; - const client = new PrismaClient(); +const client = new PrismaClient(); - // cleanup database before each test - beforeEach(async () => { - await client.$connect(); - await client.user.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.update.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - }); - - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - }); - - afterEach(async () => { - await app.close(); - }); - - it('should set blobs', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - - const buffer1 = Buffer.from([0, 0]); - const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1); - const buffer2 = Buffer.from([0, 1]); - const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2); - - const server = app.getHttpServer(); - - const response1 = await request(server) - .get(`/api/workspaces/${workspace.id}/blobs/${hash1}`) - .auth(u1.token.token, { type: 'bearer' }) - .buffer(); - - deepEqual(response1.body, buffer1, 'failed to get blob'); - - const response2 = await request(server) - .get(`/api/workspaces/${workspace.id}/blobs/${hash2}`) - .auth(u1.token.token, { type: 'bearer' }) - .buffer(); - - deepEqual(response2.body, buffer2, 'failed to get blob'); - }); - - it('should list blobs', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - const blobs = await listBlobs(app, u1.token.token, workspace.id); - ok(blobs.length === 0, 'failed to list blobs'); - - const buffer1 = Buffer.from([0, 0]); - const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1); - const buffer2 = Buffer.from([0, 1]); - const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2); - - const ret = await listBlobs(app, u1.token.token, workspace.id); - ok(ret.length === 2, 'failed to list blobs'); - ok(ret[0] === hash1, 'failed to list blobs'); - ok(ret[1] === hash2, 'failed to list blobs'); - }); - - it('should calc blobs size', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - - const buffer1 = Buffer.from([0, 0]); - await setBlob(app, u1.token.token, workspace.id, buffer1); - const buffer2 = Buffer.from([0, 1]); - await setBlob(app, u1.token.token, workspace.id, buffer2); - - const size = await collectBlobSizes(app, u1.token.token, workspace.id); - ok(size === 4, 'failed to collect blob sizes'); - }); - - it('should calc all blobs size', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - - const workspace1 = await createWorkspace(app, u1.token.token); - - const buffer1 = Buffer.from([0, 0]); - await setBlob(app, u1.token.token, workspace1.id, buffer1); - const buffer2 = Buffer.from([0, 1]); - await setBlob(app, u1.token.token, workspace1.id, buffer2); - - const workspace2 = await createWorkspace(app, u1.token.token); - - const buffer3 = Buffer.from([0, 0]); - await setBlob(app, u1.token.token, workspace2.id, buffer3); - const buffer4 = Buffer.from([0, 1]); - await setBlob(app, u1.token.token, workspace2.id, buffer4); - - const size = await collectAllBlobSizes(app, u1.token.token); - ok(size === 8, 'failed to collect all blob sizes'); - }); +// cleanup database before each test +test.beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.update.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); +}); + +test.beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); +}); + +test.afterEach(async () => { + await app.close(); +}); + +test('should set blobs', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const buffer1 = Buffer.from([0, 0]); + const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1); + const buffer2 = Buffer.from([0, 1]); + const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2); + + const server = app.getHttpServer(); + + const response1 = await request(server) + .get(`/api/workspaces/${workspace.id}/blobs/${hash1}`) + .auth(u1.token.token, { type: 'bearer' }) + .buffer(); + + deepEqual(response1.body, buffer1, 'failed to get blob'); + + const response2 = await request(server) + .get(`/api/workspaces/${workspace.id}/blobs/${hash2}`) + .auth(u1.token.token, { type: 'bearer' }) + .buffer(); + + deepEqual(response2.body, buffer2, 'failed to get blob'); + t.pass(); +}); + +test('should list blobs', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + const blobs = await listBlobs(app, u1.token.token, workspace.id); + ok(blobs.length === 0, 'failed to list blobs'); + + const buffer1 = Buffer.from([0, 0]); + const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1); + const buffer2 = Buffer.from([0, 1]); + const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2); + + const ret = await listBlobs(app, u1.token.token, workspace.id); + ok(ret.length === 2, 'failed to list blobs'); + ok(ret[0] === hash1, 'failed to list blobs'); + ok(ret[1] === hash2, 'failed to list blobs'); + t.pass(); +}); + +test('should calc blobs size', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const buffer1 = Buffer.from([0, 0]); + await setBlob(app, u1.token.token, workspace.id, buffer1); + const buffer2 = Buffer.from([0, 1]); + await setBlob(app, u1.token.token, workspace.id, buffer2); + + const size = await collectBlobSizes(app, u1.token.token, workspace.id); + ok(size === 4, 'failed to collect blob sizes'); + t.pass(); +}); + +test('should calc all blobs size', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace1 = await createWorkspace(app, u1.token.token); + + const buffer1 = Buffer.from([0, 0]); + await setBlob(app, u1.token.token, workspace1.id, buffer1); + const buffer2 = Buffer.from([0, 1]); + await setBlob(app, u1.token.token, workspace1.id, buffer2); + + const workspace2 = await createWorkspace(app, u1.token.token); + + const buffer3 = Buffer.from([0, 0]); + await setBlob(app, u1.token.token, workspace2.id, buffer3); + const buffer4 = Buffer.from([0, 1]); + await setBlob(app, u1.token.token, workspace2.id, buffer4); + + const size = await collectAllBlobSizes(app, u1.token.token); + ok(size === 8, 'failed to collect all blob sizes'); + t.pass(); }); diff --git a/apps/server/src/tests/workspace-invite.spec.ts b/apps/server/src/tests/workspace-invite.spec.ts index 010239d76f..32506bc74a 100644 --- a/apps/server/src/tests/workspace-invite.spec.ts +++ b/apps/server/src/tests/workspace-invite.spec.ts @@ -1,9 +1,9 @@ import { ok } from 'node:assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import type { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; // @ts-expect-error graphql-upload is not typed import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; @@ -21,169 +21,168 @@ import { signUp, } from './utils'; -describe('Workspace Module - invite', () => { - let app: INestApplication; +let app: INestApplication; - const client = new PrismaClient(); +const client = new PrismaClient(); - let auth: AuthService; - let mail: MailService; +let auth: AuthService; +let mail: MailService; - // cleanup database before each test - beforeEach(async () => { - await client.$connect(); - await client.user.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.update.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - }); +// cleanup database before each test +test.beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.update.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); +}); - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); +test.beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); - auth = module.get(AuthService); - mail = module.get(MailService); - }); + auth = module.get(AuthService); + mail = module.get(MailService); +}); - afterEach(async () => { - await app.close(); - }); +test.afterEach(async () => { + await app.close(); +}); - it('should invite a user', async () => { +test('should invite a user', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const invite = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + ok(!!invite, 'failed to invite user'); + t.pass(); +}); + +test('should accept an invite', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + + const accept = await acceptInvite(app, u2.token.token, workspace.id); + ok(accept === true, 'failed to accept invite'); + + const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); + const currMember = currWorkspace.members.find(u => u.email === u2.email); + ok(currMember !== undefined, 'failed to invite user'); + ok(currMember.id === u2.id, 'failed to invite user'); + ok(!currMember.accepted, 'failed to invite user'); + t.pass(); +}); + +test('should leave a workspace', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + await acceptInvite(app, u2.token.token, workspace.id); + + const leave = await leaveWorkspace(app, u2.token.token, workspace.id); + ok(leave === true, 'failed to leave workspace'); + t.pass(); +}); + +test('should revoke a user', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + + const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); + ok(currWorkspace.members.length === 2, 'failed to invite user'); + + const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id); + ok(revoke === true, 'failed to revoke user'); + t.pass(); +}); + +test('should create user if not exist', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro', 'Admin'); + + const user = await auth.getUserByEmail('u2@affine.pro'); + ok(user !== undefined, 'failed to create user'); + ok(user?.name === 'Unnamed', 'failed to create user'); + t.pass(); +}); + +test('should invite a user by link', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const invite = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + + const accept = await acceptInviteById(app, workspace.id, invite); + ok(accept === true, 'failed to accept invite'); + + const invite1 = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + + ok(invite === invite1, 'repeat the invitation must return same id'); + + const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); + const currMember = currWorkspace.members.find(u => u.email === u2.email); + ok(currMember !== undefined, 'failed to invite user'); + ok(currMember.inviteId === invite, 'failed to check invite id'); + t.pass(); +}); + +test('should send invite email', async t => { + if (mail.hasConfigured()) { const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + const u2 = await signUp(app, 'test', 'production@toeverything.info', '1'); const workspace = await createWorkspace(app, u1.token.token); - - const invite = await inviteUser( - app, - u1.token.token, - workspace.id, - u2.email, - 'Admin' - ); - ok(!!invite, 'failed to invite user'); - }); - - it('should accept an invite', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); - - const accept = await acceptInvite(app, u2.token.token, workspace.id); - ok(accept === true, 'failed to accept invite'); - - const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); - const currMember = currWorkspace.members.find(u => u.email === u2.email); - ok(currMember !== undefined, 'failed to invite user'); - ok(currMember.id === u2.id, 'failed to invite user'); - ok(!currMember.accepted, 'failed to invite user'); - }); - - it('should leave a workspace', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); - await acceptInvite(app, u2.token.token, workspace.id); - - const leave = await leaveWorkspace(app, u2.token.token, workspace.id); - ok(leave === true, 'failed to leave workspace'); - }); - - it('should revoke a user', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); - - const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); - ok(currWorkspace.members.length === 2, 'failed to invite user'); - - const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id); - ok(revoke === true, 'failed to revoke user'); - }); - - it('should create user if not exist', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - await inviteUser( - app, - u1.token.token, - workspace.id, - 'u2@affine.pro', - 'Admin' - ); - - const user = await auth.getUserByEmail('u2@affine.pro'); - ok(user !== undefined, 'failed to create user'); - ok(user?.name === 'Unnamed', 'failed to create user'); - }); - - it('should invite a user by link', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - - const invite = await inviteUser( app, u1.token.token, workspace.id, u2.email, - 'Admin' + 'Admin', + true ); - - const accept = await acceptInviteById(app, workspace.id, invite); - ok(accept === true, 'failed to accept invite'); - - const invite1 = await inviteUser( - app, - u1.token.token, - workspace.id, - u2.email, - 'Admin' - ); - - ok(invite === invite1, 'repeat the invitation must return same id'); - - const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); - const currMember = currWorkspace.members.find(u => u.email === u2.email); - ok(currMember !== undefined, 'failed to invite user'); - ok(currMember.inviteId === invite, 'failed to check invite id'); - }); - - it('should send invite email', async () => { - if (mail.hasConfigured()) { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'test', 'production@toeverything.info', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - await inviteUser( - app, - u1.token.token, - workspace.id, - u2.email, - 'Admin', - true - ); - } - }); + } + t.pass(); }); diff --git a/apps/server/src/tests/workspace.spec.ts b/apps/server/src/tests/workspace.spec.ts index 817eea16c6..500c3ce045 100644 --- a/apps/server/src/tests/workspace.spec.ts +++ b/apps/server/src/tests/workspace.spec.ts @@ -1,9 +1,9 @@ import { deepEqual, ok, rejects } from 'node:assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import type { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; +import test from 'ava'; // @ts-expect-error graphql-upload is not typed import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; @@ -22,222 +22,213 @@ import { updateWorkspace, } from './utils'; -describe('Workspace Module', () => { - let app: INestApplication; +let app: INestApplication; - const client = new PrismaClient(); +const client = new PrismaClient(); - // cleanup database before each test - beforeEach(async () => { - await client.$connect(); - await client.user.deleteMany({}); - await client.update.deleteMany({}); - await client.snapshot.deleteMany({}); - await client.workspace.deleteMany({}); - await client.$disconnect(); - }); - - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - }); - - afterEach(async () => { - await app.close(); - }); - - it('should register a user', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); - ok(typeof user.id === 'string', 'user.id is not a string'); - ok(user.name === 'u1', 'user.name is not valid'); - ok(user.email === 'u1@affine.pro', 'user.email is not valid'); - }); - - it.skip('should be throttled at call signUp', async () => { - let token = ''; - for (let i = 0; i < 10; i++) { - token = (await signUp(app, `u${i}`, `u${i}@affine.pro`, `${i}`)).token - .token; - // throttles are applied to each endpoint separately - await currentUser(app, token); - } - await rejects(signUp(app, 'u11', 'u11@affine.pro', '11')); - await rejects(currentUser(app, token)); - }); - - it('should create a workspace', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); - - const workspace = await createWorkspace(app, user.token.token); - ok(typeof workspace.id === 'string', 'workspace.id is not a string'); - }); - - it('should can publish workspace', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const workspace = await createWorkspace(app, user.token.token); - - const isPublic = await updateWorkspace( - app, - user.token.token, - workspace.id, - true - ); - ok(isPublic === true, 'failed to publish workspace'); - - const isPrivate = await updateWorkspace( - app, - user.token.token, - workspace.id, - false - ); - ok(isPrivate === false, 'failed to unpublish workspace'); - }); - - it('should can read published workspace', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const workspace = await createWorkspace(app, user.token.token); - - await rejects( - getPublicWorkspace(app, 'not_exists_ws'), - 'must not get not exists workspace' - ); - await rejects( - getPublicWorkspace(app, workspace.id), - 'must not get private workspace' - ); - - await updateWorkspace(app, user.token.token, workspace.id, true); - - const publicWorkspace = await getPublicWorkspace(app, workspace.id); - ok(publicWorkspace.id === workspace.id, 'failed to get public workspace'); - }); - - it('should share a page', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - - const workspace = await createWorkspace(app, u1.token.token); - - const share = await sharePage(app, u1.token.token, workspace.id, 'page1'); - ok(share === true, 'failed to share page'); - const pages = await getWorkspaceSharedPages( - app, - u1.token.token, - workspace.id - ); - ok(pages.length === 1, 'failed to get shared pages'); - ok(pages[0] === 'page1', 'failed to get shared page: page1'); - - const msg1 = await sharePage(app, u2.token.token, workspace.id, 'page2'); - ok(msg1 === 'Permission denied', 'unauthorized user can share page'); - const msg2 = await revokePage( - app, - u2.token.token, - 'not_exists_ws', - 'page2' - ); - ok(msg2 === 'Permission denied', 'unauthorized user can share page'); - - await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); - await acceptInvite(app, u2.token.token, workspace.id); - const invited = await sharePage(app, u2.token.token, workspace.id, 'page2'); - ok(invited === true, 'failed to share page'); - - const revoke = await revokePage(app, u1.token.token, workspace.id, 'page1'); - ok(revoke === true, 'failed to revoke page'); - const pages2 = await getWorkspaceSharedPages( - app, - u1.token.token, - workspace.id - ); - ok(pages2.length === 1, 'failed to get shared pages'); - ok(pages2[0] === 'page2', 'failed to get shared page: page2'); - - const msg3 = await revokePage(app, u1.token.token, workspace.id, 'page3'); - ok(msg3 === false, 'can revoke non-exists page'); - - const msg4 = await revokePage(app, u1.token.token, workspace.id, 'page2'); - ok(msg4 === true, 'failed to revoke page'); - const page3 = await getWorkspaceSharedPages( - app, - u1.token.token, - workspace.id - ); - ok(page3.length === 0, 'failed to get shared pages'); - }); - - it('should can get workspace doc', async () => { - const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2'); - const workspace = await createWorkspace(app, u1.token.token); - - const res1 = await request(app.getHttpServer()) - .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) - .auth(u1.token.token, { type: 'bearer' }) - .expect(200) - .type('application/octet-stream'); - - deepEqual( - res1.body, - Buffer.from([0, 0]), - 'failed to get doc with u1 token' - ); - - await request(app.getHttpServer()) - .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) - .expect(403); - await request(app.getHttpServer()) - .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) - .auth(u2.token.token, { type: 'bearer' }) - .expect(403); - - await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); - await request(app.getHttpServer()) - .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) - .auth(u2.token.token, { type: 'bearer' }) - .expect(403); - - await acceptInvite(app, u2.token.token, workspace.id); - const res2 = await request(app.getHttpServer()) - .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) - .auth(u2.token.token, { type: 'bearer' }) - .expect(200) - .type('application/octet-stream'); - - deepEqual( - res2.body, - Buffer.from([0, 0]), - 'failed to get doc with u2 token' - ); - }); - - it('should be able to get public workspace doc', async () => { - const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const workspace = await createWorkspace(app, user.token.token); - - const isPublic = await updateWorkspace( - app, - user.token.token, - workspace.id, - true - ); - - ok(isPublic === true, 'failed to publish workspace'); - - const res = await request(app.getHttpServer()) - .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) - .expect(200) - .type('application/octet-stream'); - - deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc'); - }); +// cleanup database before each test +test.beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.update.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); +}); + +test.beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); +}); + +test.afterEach(async () => { + await app.close(); +}); + +test('should register a user', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + t.true(typeof user.id === 'string', 'user.id is not a string'); + t.true(user.name === 'u1', 'user.name is not valid'); + t.true(user.email === 'u1@affine.pro', 'user.email is not valid'); +}); + +test.skip('should be throttled at call signUp', async t => { + let token = ''; + for (let i = 0; i < 10; i++) { + token = (await signUp(app, `u${i}`, `u${i}@affine.pro`, `${i}`)).token + .token; + // throttles are applied to each endpoint separately + await currentUser(app, token); + } + await rejects(signUp(app, 'u11', 'u11@affine.pro', '11')); + await rejects(currentUser(app, token)); + t.pass(); +}); + +test('should create a workspace', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, user.token.token); + ok(typeof workspace.id === 'string', 'workspace.id is not a string'); + t.pass(); +}); + +test('should can publish workspace', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const workspace = await createWorkspace(app, user.token.token); + + const isPublic = await updateWorkspace( + app, + user.token.token, + workspace.id, + true + ); + ok(isPublic === true, 'failed to publish workspace'); + + const isPrivate = await updateWorkspace( + app, + user.token.token, + workspace.id, + false + ); + ok(isPrivate === false, 'failed to unpublish workspace'); + t.pass(); +}); + +test('should can read published workspace', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const workspace = await createWorkspace(app, user.token.token); + + await rejects( + getPublicWorkspace(app, 'not_exists_ws'), + 'must not get not exists workspace' + ); + await rejects( + getPublicWorkspace(app, workspace.id), + 'must not get private workspace' + ); + + await updateWorkspace(app, user.token.token, workspace.id, true); + + const publicWorkspace = await getPublicWorkspace(app, workspace.id); + ok(publicWorkspace.id === workspace.id, 'failed to get public workspace'); + t.pass(); +}); + +test('should share a page', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const share = await sharePage(app, u1.token.token, workspace.id, 'page1'); + t.true(share === true, 'failed to share page'); + const pages = await getWorkspaceSharedPages( + app, + u1.token.token, + workspace.id + ); + t.true(pages.length === 1, 'failed to get shared pages'); + t.true(pages[0] === 'page1', 'failed to get shared page: page1'); + + const msg1 = await sharePage(app, u2.token.token, workspace.id, 'page2'); + t.true(msg1 === 'Permission denied', 'unauthorized user can share page'); + const msg2 = await revokePage(app, u2.token.token, 'not_exists_ws', 'page2'); + t.true(msg2 === 'Permission denied', 'unauthorized user can share page'); + + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + await acceptInvite(app, u2.token.token, workspace.id); + const invited = await sharePage(app, u2.token.token, workspace.id, 'page2'); + t.true(invited === true, 'failed to share page'); + + const revoke = await revokePage(app, u1.token.token, workspace.id, 'page1'); + t.true(revoke === true, 'failed to revoke page'); + const pages2 = await getWorkspaceSharedPages( + app, + u1.token.token, + workspace.id + ); + t.true(pages2.length === 1, 'failed to get shared pages'); + t.true(pages2[0] === 'page2', 'failed to get shared page: page2'); + + const msg3 = await revokePage(app, u1.token.token, workspace.id, 'page3'); + t.true(msg3 === false, 'can revoke non-exists page'); + + const msg4 = await revokePage(app, u1.token.token, workspace.id, 'page2'); + t.true(msg4 === true, 'failed to revoke page'); + const page3 = await getWorkspaceSharedPages( + app, + u1.token.token, + workspace.id + ); + t.true(page3.length === 0, 'failed to get shared pages'); +}); + +test('should can get workspace doc', async t => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2'); + const workspace = await createWorkspace(app, u1.token.token); + + const res1 = await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u1.token.token, { type: 'bearer' }) + .expect(200) + .type('application/octet-stream'); + + deepEqual(res1.body, Buffer.from([0, 0]), 'failed to get doc with u1 token'); + + await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .expect(403); + await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u2.token.token, { type: 'bearer' }) + .expect(403); + + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u2.token.token, { type: 'bearer' }) + .expect(403); + + await acceptInvite(app, u2.token.token, workspace.id); + const res2 = await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u2.token.token, { type: 'bearer' }) + .expect(200) + .type('application/octet-stream'); + + deepEqual(res2.body, Buffer.from([0, 0]), 'failed to get doc with u2 token'); + t.pass(); +}); + +test('should be able to get public workspace doc', async t => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const workspace = await createWorkspace(app, user.token.token); + + const isPublic = await updateWorkspace( + app, + user.token.token, + workspace.id, + true + ); + + ok(isPublic === true, 'failed to publish workspace'); + + const res = await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .expect(200) + .type('application/octet-stream'); + + deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc'); + t.pass(); }); diff --git a/yarn.lock b/yarn.lock index 23f7679278..c035c2a8dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -699,6 +699,7 @@ __metadata: "@types/sinon": ^10.0.16 "@types/supertest": ^2.0.12 "@types/ws": ^8.5.5 + ava: ^5.3.1 c8: ^8.0.1 cookie-parser: ^1.4.6 dotenv: ^16.3.1 @@ -14762,6 +14763,16 @@ __metadata: languageName: node linkType: hard +"aggregate-error@npm:^4.0.0": + version: 4.0.1 + resolution: "aggregate-error@npm:4.0.1" + dependencies: + clean-stack: ^4.0.0 + indent-string: ^5.0.0 + checksum: bb3ffdfd13447800fff237c2cba752c59868ee669104bb995dfbbe0b8320e967d679e683dabb640feb32e4882d60258165cde0baafc4cd467cc7d275a13ad6b5 + languageName: node + linkType: hard + "ahocorasick@npm:1.0.2": version: 1.0.2 resolution: "ahocorasick@npm:1.0.2" @@ -14944,7 +14955,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 @@ -15086,6 +15097,13 @@ __metadata: languageName: node linkType: hard +"array-find-index@npm:^1.0.1": + version: 1.0.2 + resolution: "array-find-index@npm:1.0.2" + checksum: aac128bf369e1ac6c06ff0bb330788371c0e256f71279fb92d745e26fb4b9db8920e485b4ec25e841c93146bf71a34dcdbcefa115e7e0f96927a214d237b7081 + languageName: node + linkType: hard + "array-flatten@npm:1.1.1": version: 1.1.1 resolution: "array-flatten@npm:1.1.1" @@ -15150,6 +15168,13 @@ __metadata: languageName: node linkType: hard +"arrgv@npm:^1.0.2": + version: 1.0.2 + resolution: "arrgv@npm:1.0.2" + checksum: 470bbb406ea3b34810dd8b03c0b33282617a42d9fce0ab45d58596efefd042fc548eda49161fa8e3f607cbe9df90e7a67003a09043ab9081eff70f97c63dd0e2 + languageName: node + linkType: hard + "arrify@npm:^1.0.1": version: 1.0.1 resolution: "arrify@npm:1.0.1" @@ -15164,6 +15189,13 @@ __metadata: languageName: node linkType: hard +"arrify@npm:^3.0.0": + version: 3.0.0 + resolution: "arrify@npm:3.0.0" + checksum: d6c6f3dad9571234f320e130d57fddb2cc283c87f2ac7df6c7005dffc5161b7bb9376f4be655ed257050330336e84afc4f3020d77696ad231ff580a94ae5aba6 + languageName: node + linkType: hard + "as-table@npm:^1.0.36": version: 1.0.55 resolution: "as-table@npm:1.0.55" @@ -15366,6 +15398,64 @@ __metadata: languageName: node linkType: hard +"ava@npm:^5.3.1": + version: 5.3.1 + resolution: "ava@npm:5.3.1" + dependencies: + acorn: ^8.8.2 + acorn-walk: ^8.2.0 + ansi-styles: ^6.2.1 + arrgv: ^1.0.2 + arrify: ^3.0.0 + callsites: ^4.0.0 + cbor: ^8.1.0 + chalk: ^5.2.0 + chokidar: ^3.5.3 + chunkd: ^2.0.1 + ci-info: ^3.8.0 + ci-parallel-vars: ^1.0.1 + clean-yaml-object: ^0.1.0 + cli-truncate: ^3.1.0 + code-excerpt: ^4.0.0 + common-path-prefix: ^3.0.0 + concordance: ^5.0.4 + currently-unhandled: ^0.4.1 + debug: ^4.3.4 + emittery: ^1.0.1 + figures: ^5.0.0 + globby: ^13.1.4 + ignore-by-default: ^2.1.0 + indent-string: ^5.0.0 + is-error: ^2.2.2 + is-plain-object: ^5.0.0 + is-promise: ^4.0.0 + matcher: ^5.0.0 + mem: ^9.0.2 + ms: ^2.1.3 + p-event: ^5.0.1 + p-map: ^5.5.0 + picomatch: ^2.3.1 + pkg-conf: ^4.0.0 + plur: ^5.1.0 + pretty-ms: ^8.0.0 + resolve-cwd: ^3.0.0 + stack-utils: ^2.0.6 + strip-ansi: ^7.0.1 + supertap: ^3.0.1 + temp-dir: ^3.0.0 + write-file-atomic: ^5.0.1 + yargs: ^17.7.2 + peerDependencies: + "@ava/typescript": "*" + peerDependenciesMeta: + "@ava/typescript": + optional: true + bin: + ava: entrypoints/cli.mjs + checksum: 126a5932baef74eccd8bec992bd522e25c05b6ee4985dde87c20cece76c2377f0bf9448f242f3f9cd2abbf7a5ac932fe4e4abde2a23792d6271a6088e5a1984e + languageName: node + linkType: hard + "axios@npm:1.1.3": version: 1.1.3 resolution: "axios@npm:1.1.3" @@ -15791,6 +15881,13 @@ __metadata: languageName: node linkType: hard +"blueimp-md5@npm:^2.10.0": + version: 2.19.0 + resolution: "blueimp-md5@npm:2.19.0" + checksum: 28095dcbd2c67152a2938006e8d7c74c3406ba6556071298f872505432feb2c13241b0476644160ee0a5220383ba94cb8ccdac0053b51f68d168728f9c382530 + languageName: node + linkType: hard + "body-parser@npm:1.20.1": version: 1.20.1 resolution: "body-parser@npm:1.20.1" @@ -16194,6 +16291,13 @@ __metadata: languageName: node linkType: hard +"callsites@npm:^4.0.0": + version: 4.1.0 + resolution: "callsites@npm:4.1.0" + checksum: 4ad31de7b7615fa25bdab9c2373865209d2d5190f895cdf2e2f518bd1dafa7ebcda2e6e9cc9640f2dfde6b3893d82fa4359a78ffc27baad2503227553c6882fa + languageName: node + linkType: hard + "camel-case@npm:^4.1.2": version: 4.1.2 resolution: "camel-case@npm:4.1.2" @@ -16292,6 +16396,15 @@ __metadata: languageName: node linkType: hard +"cbor@npm:^8.1.0": + version: 8.1.0 + resolution: "cbor@npm:8.1.0" + dependencies: + nofilter: ^3.1.0 + checksum: a90338435dc7b45cc01461af979e3bb6ddd4f2a08584c437586039cd5f2235014c06e49d664295debbfb3514d87b2f06728092ab6aa6175e2e85e9cd7dc0c1fd + languageName: node + linkType: hard + "chai@npm:^4.3.7": version: 4.3.8 resolution: "chai@npm:4.3.8" @@ -16550,6 +16663,13 @@ __metadata: languageName: node linkType: hard +"chunkd@npm:^2.0.1": + version: 2.0.1 + resolution: "chunkd@npm:2.0.1" + checksum: bab8cc08c752a3648984385dc6f61d751e89dbeef648d22a3b661e1d470eaa0f5182f0b4303710f13ae83d2f85144f8eb2dde7a975861d9021b5c56b881f457b + languageName: node + linkType: hard + "ci-info@npm:^3.2.0, ci-info@npm:^3.8.0": version: 3.8.0 resolution: "ci-info@npm:3.8.0" @@ -16557,6 +16677,13 @@ __metadata: languageName: node linkType: hard +"ci-parallel-vars@npm:^1.0.1": + version: 1.0.1 + resolution: "ci-parallel-vars@npm:1.0.1" + checksum: ae859831f7e8e3585db731b8306c336616e37bd709dad1d7775ea4c0731aefd94741dabb48201edc6827d000008fd7fb72cb977967614ee2d99d6b499f0c35fe + languageName: node + linkType: hard + "cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2": version: 1.2.3 resolution: "cjs-module-lexer@npm:1.2.3" @@ -16596,6 +16723,22 @@ __metadata: languageName: node linkType: hard +"clean-stack@npm:^4.0.0": + version: 4.2.0 + resolution: "clean-stack@npm:4.2.0" + dependencies: + escape-string-regexp: 5.0.0 + checksum: 373f656a31face5c615c0839213b9b542a0a48057abfb1df66900eab4dc2a5c6097628e4a0b5aa559cdfc4e66f8a14ea47be9681773165a44470ef5fb8ccc172 + languageName: node + linkType: hard + +"clean-yaml-object@npm:^0.1.0": + version: 0.1.0 + resolution: "clean-yaml-object@npm:0.1.0" + checksum: 0374ad2f1fbd4984ecf56ebc62200092f6372b9ccf1b7971bb979c328fb12fe76e759fb1e8adc491c80b7b1861f9f00c7f19813dd2a0f49c88231422c70451f4 + languageName: node + linkType: hard + "cli-boxes@npm:^3.0.0": version: 3.0.0 resolution: "cli-boxes@npm:3.0.0" @@ -16812,6 +16955,15 @@ __metadata: languageName: node linkType: hard +"code-excerpt@npm:^4.0.0": + version: 4.0.0 + resolution: "code-excerpt@npm:4.0.0" + dependencies: + convert-to-spaces: ^2.0.1 + checksum: d57137d8f4825879283a828cc02a1115b56858dc54ed06c625c8f67d6685d1becd2fbaa7f0ab19ecca1f5cca03f8c97bbc1f013cab40261e4d3275032e65efe9 + languageName: node + linkType: hard + "collect-v8-coverage@npm:^1.0.0": version: 1.0.2 resolution: "collect-v8-coverage@npm:1.0.2" @@ -17089,6 +17241,22 @@ __metadata: languageName: node linkType: hard +"concordance@npm:^5.0.4": + version: 5.0.4 + resolution: "concordance@npm:5.0.4" + dependencies: + date-time: ^3.1.0 + esutils: ^2.0.3 + fast-diff: ^1.2.0 + js-string-escape: ^1.0.1 + lodash: ^4.17.15 + md5-hex: ^3.0.1 + semver: ^7.3.2 + well-known-symbols: ^2.0.0 + checksum: 749153ba711492feb7c3d2f5bb04c107157440b3e39509bd5dd19ee7b3ac751d1e4cd75796d9f702e0a713312dbc661421c68aa4a2c34d5f6d91f47e3a1c64a6 + languageName: node + linkType: hard + "concurrently@npm:^8.2.1": version: 8.2.1 resolution: "concurrently@npm:8.2.1" @@ -17210,6 +17378,13 @@ __metadata: languageName: node linkType: hard +"convert-to-spaces@npm:^2.0.1": + version: 2.0.1 + resolution: "convert-to-spaces@npm:2.0.1" + checksum: bbb324e5916fe9866f65c0ff5f9c1ea933764d0bdb09fccaf59542e40545ed483db6b2339c6d9eb56a11965a58f1a6038f3174f0e2fb7601343c7107ca5e2751 + languageName: node + linkType: hard + "cookie-parser@npm:^1.4.6": version: 1.4.6 resolution: "cookie-parser@npm:1.4.6" @@ -17651,6 +17826,15 @@ __metadata: languageName: node linkType: hard +"currently-unhandled@npm:^0.4.1": + version: 0.4.1 + resolution: "currently-unhandled@npm:0.4.1" + dependencies: + array-find-index: ^1.0.1 + checksum: 1f59fe10b5339b54b1a1eee110022f663f3495cf7cf2f480686e89edc7fa8bfe42dbab4b54f85034bc8b092a76cc7becbc2dad4f9adad332ab5831bec39ad540 + languageName: node + linkType: hard + "cwd@npm:^0.10.0": version: 0.10.0 resolution: "cwd@npm:0.10.0" @@ -17698,6 +17882,15 @@ __metadata: languageName: node linkType: hard +"date-time@npm:^3.1.0": + version: 3.1.0 + resolution: "date-time@npm:3.1.0" + dependencies: + time-zone: ^1.0.0 + checksum: f9cfcd1b15dfeabab15c0b9d18eb9e4e2d9d4371713564178d46a8f91ad577a290b5178b80050718d02d9c0cf646f8a875011e12d1ed05871e9f72c72c8a8fe6 + languageName: node + linkType: hard + "dayjs@npm:^1.11.3, dayjs@npm:^1.11.9": version: 1.11.9 resolution: "dayjs@npm:1.11.9" @@ -18843,6 +19036,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^1.0.1": + version: 1.0.1 + resolution: "emittery@npm:1.0.1" + checksum: d95faee6ffb2e023cadaa6804265fea5298c53d079f170112af8dfae3e141761363ea4510966128259346418e3ec7639310fd75059ecce2423bf8afd07004226 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -19415,6 +19615,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:5.0.0, escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -19436,13 +19643,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^5.0.0": - version: 5.0.0 - resolution: "escape-string-regexp@npm:5.0.0" - checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e - languageName: node - linkType: hard - "escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" @@ -19809,7 +20009,7 @@ __metadata: languageName: node linkType: hard -"esutils@npm:^2.0.2": +"esutils@npm:^2.0.2, esutils@npm:^2.0.3": version: 2.0.3 resolution: "esutils@npm:2.0.3" checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 @@ -20123,7 +20323,7 @@ __metadata: languageName: node linkType: hard -"fast-diff@npm:^1.1.2": +"fast-diff@npm:^1.1.2, fast-diff@npm:^1.2.0": version: 1.3.0 resolution: "fast-diff@npm:1.3.0" checksum: d22d371b994fdc8cce9ff510d7b8dc4da70ac327bcba20df607dd5b9cae9f908f4d1028f5fe467650f058d1e7270235ae0b8230809a262b4df587a3b3aa216c3 @@ -20571,6 +20771,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^6.0.0": + version: 6.3.0 + resolution: "find-up@npm:6.3.0" + dependencies: + locate-path: ^7.1.0 + path-exists: ^5.0.0 + checksum: 9a21b7f9244a420e54c6df95b4f6fc3941efd3c3e5476f8274eb452f6a85706e7a6a90de71353ee4f091fcb4593271a6f92810a324ec542650398f928783c280 + languageName: node + linkType: hard + "flat-cache@npm:^3.0.4": version: 3.1.0 resolution: "flat-cache@npm:3.1.0" @@ -22325,6 +22535,13 @@ __metadata: languageName: node linkType: hard +"ignore-by-default@npm:^2.1.0": + version: 2.1.0 + resolution: "ignore-by-default@npm:2.1.0" + checksum: 2b2df4622b6a07a3e91893987be8f060dc553f7736b67e72aa2312041c450a6fa8371733d03c42f45a02e47ec824e961c2fba63a3d94fc59cbd669220a5b0d7a + languageName: node + linkType: hard + "ignore@npm:^5.0.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4": version: 5.2.4 resolution: "ignore@npm:5.2.4" @@ -22415,6 +22632,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^5.0.0": + version: 5.0.0 + resolution: "indent-string@npm:5.0.0" + checksum: e466c27b6373440e6d84fbc19e750219ce25865cb82d578e41a6053d727e5520dc5725217d6eb1cc76005a1bb1696a0f106d84ce7ebda3033b963a38583fb3b3 + languageName: node + linkType: hard + "indexes-of@npm:^1.0.1": version: 1.0.1 resolution: "indexes-of@npm:1.0.1" @@ -22560,6 +22784,13 @@ __metadata: languageName: node linkType: hard +"irregular-plurals@npm:^3.3.0": + version: 3.5.0 + resolution: "irregular-plurals@npm:3.5.0" + checksum: 5b663091dc89155df7b2e9d053e8fb11941a0c4be95c4b6549ed3ea020489fdf4f75ea586c915b5b543704252679a5a6e8c6c3587da5ac3fc57b12da90a9aee7 + languageName: node + linkType: hard + "is-absolute-url@npm:^3.0.0": version: 3.0.3 resolution: "is-absolute-url@npm:3.0.3" @@ -22688,6 +22919,13 @@ __metadata: languageName: node linkType: hard +"is-error@npm:^2.2.2": + version: 2.2.2 + resolution: "is-error@npm:2.2.2" + checksum: a97b39587150f0d38f9f93f64699807fe3020fe5edbd63548f234dc2ba96fd7c776d66c062bf031dfeb93c7f48db563ff6bde588418ca041da37c659a416f055 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -22869,6 +23107,13 @@ __metadata: languageName: node linkType: hard +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 0b46517ad47b00b6358fd6553c83ec1f6ba9acd7ffb3d30a0bf519c5c69e7147c132430452351b8a9fc198f8dd6c4f76f8e6f5a7f100f8c77d57d9e0f4261a8a + languageName: node + linkType: hard + "is-regexp@npm:^1.0.0": version: 1.0.0 resolution: "is-regexp@npm:1.0.0" @@ -23940,6 +24185,13 @@ __metadata: languageName: node linkType: hard +"js-string-escape@npm:^1.0.1": + version: 1.0.1 + resolution: "js-string-escape@npm:1.0.1" + checksum: f11e0991bf57e0c183b55c547acec85bd2445f043efc9ea5aa68b41bd2a3e7d3ce94636cb233ae0d84064ba4c1a505d32e969813c5b13f81e7d4be12c59256fe + languageName: node + linkType: hard + "js-tiktoken@npm:^1.0.7": version: 1.0.7 resolution: "js-tiktoken@npm:1.0.7" @@ -23967,7 +24219,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1": +"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" dependencies: @@ -24802,6 +25054,13 @@ __metadata: languageName: node linkType: hard +"load-json-file@npm:^7.0.0": + version: 7.0.1 + resolution: "load-json-file@npm:7.0.1" + checksum: a560288da6891778321ef993e4bdbdf05374a4f3a3aeedd5ba6b64672798c830d748cfc59a2ec9891a3db30e78b3d04172e0dcb0d4828168289a393147ca0e74 + languageName: node + linkType: hard + "loader-runner@npm:^4.1.0, loader-runner@npm:^4.2.0": version: 4.3.0 resolution: "loader-runner@npm:4.3.0" @@ -24865,6 +25124,15 @@ __metadata: languageName: node linkType: hard +"locate-path@npm:^7.1.0": + version: 7.2.0 + resolution: "locate-path@npm:7.2.0" + dependencies: + p-locate: ^6.0.0 + checksum: c1b653bdf29beaecb3d307dfb7c44d98a2a98a02ebe353c9ad055d1ac45d6ed4e1142563d222df9b9efebc2bcb7d4c792b507fad9e7150a04c29530b7db570f8 + languageName: node + linkType: hard + "lodash-es@npm:^4.17.21": version: 4.17.21 resolution: "lodash-es@npm:4.17.21" @@ -25374,7 +25642,7 @@ __metadata: languageName: node linkType: hard -"map-age-cleaner@npm:^0.1.1": +"map-age-cleaner@npm:^0.1.1, map-age-cleaner@npm:^0.1.3": version: 0.1.3 resolution: "map-age-cleaner@npm:0.1.3" dependencies: @@ -25474,6 +25742,24 @@ __metadata: languageName: node linkType: hard +"matcher@npm:^5.0.0": + version: 5.0.0 + resolution: "matcher@npm:5.0.0" + dependencies: + escape-string-regexp: ^5.0.0 + checksum: 28f191c2d23fee0f6f32fd0181d9fe173b0ab815a919edba55605438a2f9fa40372e002574a1b17add981b0a8669c75bc6194318d065ed2dceffd8b160c38118 + languageName: node + linkType: hard + +"md5-hex@npm:^3.0.1": + version: 3.0.1 + resolution: "md5-hex@npm:3.0.1" + dependencies: + blueimp-md5: ^2.10.0 + checksum: 6799a19e8bdd3e0c2861b94c1d4d858a89220488d7885c1fa236797e367d0c2e5f2b789e05309307083503f85be3603a9686a5915568a473137d6b4117419cc2 + languageName: node + linkType: hard + "md5@npm:^2.2.1, md5@npm:^2.3.0": version: 2.3.0 resolution: "md5@npm:2.3.0" @@ -25542,6 +25828,16 @@ __metadata: languageName: node linkType: hard +"mem@npm:^9.0.2": + version: 9.0.2 + resolution: "mem@npm:9.0.2" + dependencies: + map-age-cleaner: ^0.1.3 + mimic-fn: ^4.0.0 + checksum: 07829bb182af0e3ecf748dc2edb1c3b10a256ef10458f7e24d06561a2adc2b3ef34d14abe81678bbcedb46faa477e7370223f118b1a5e1252da5fe43496f3967 + languageName: node + linkType: hard + "memfs@npm:^3.4.3": version: 3.5.3 resolution: "memfs@npm:3.5.3" @@ -26108,7 +26404,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -26598,6 +26894,13 @@ __metadata: languageName: node linkType: hard +"nofilter@npm:^3.1.0": + version: 3.1.0 + resolution: "nofilter@npm:3.1.0" + checksum: 58aa85a5b4b35cbb6e42de8a8591c5e338061edc9f3e7286f2c335e9e9b9b8fa7c335ae45daa8a1f3433164dc0b9a3d187fa96f9516e04a17a1f9ce722becc4f + languageName: node + linkType: hard + "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -27185,6 +27488,15 @@ __metadata: languageName: node linkType: hard +"p-event@npm:^5.0.1": + version: 5.0.1 + resolution: "p-event@npm:5.0.1" + dependencies: + p-timeout: ^5.0.2 + checksum: 3bdd8df6092e6b149f25e9c2eb1c0843b3b4279b07be2a2c72c02b65b267a8908c2040fefd606f2497b0f2bcefcd214f8ca5a74f0c883515d400ccf1d88d5683 + languageName: node + linkType: hard + "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0" @@ -27271,6 +27583,15 @@ __metadata: languageName: node linkType: hard +"p-locate@npm:^6.0.0": + version: 6.0.0 + resolution: "p-locate@npm:6.0.0" + dependencies: + p-limit: ^4.0.0 + checksum: 2bfe5234efa5e7a4e74b30a5479a193fdd9236f8f6b4d2f3f69e3d286d9a7d7ab0c118a2a50142efcf4e41625def635bd9332d6cbf9cc65d85eb0718c579ab38 + languageName: node + linkType: hard + "p-map@npm:^3.0.0": version: 3.0.0 resolution: "p-map@npm:3.0.0" @@ -27289,6 +27610,15 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^5.5.0": + version: 5.5.0 + resolution: "p-map@npm:5.5.0" + dependencies: + aggregate-error: ^4.0.0 + checksum: 065cb6fca6b78afbd070dd9224ff160dc23eea96e57863c09a0c8ea7ce921043f76854be7ee0abc295cff1ac9adcf700e79a1fbe3b80b625081087be58e7effb + languageName: node + linkType: hard + "p-queue@npm:^6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" @@ -27318,6 +27648,13 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:^5.0.2": + version: 5.1.0 + resolution: "p-timeout@npm:5.1.0" + checksum: f5cd4e17301ff1ff1d8dbf2817df0ad88c6bba99349fc24d8d181827176ad4f8aca649190b8a5b1a428dfd6ddc091af4606835d3e0cb0656e04045da5c9e270c + languageName: node + linkType: hard + "p-try@npm:^1.0.0": version: 1.0.0 resolution: "p-try@npm:1.0.0" @@ -27446,6 +27783,13 @@ __metadata: languageName: node linkType: hard +"parse-ms@npm:^3.0.0": + version: 3.0.0 + resolution: "parse-ms@npm:3.0.0" + checksum: fc602bba093835562321a67a9d6c8c9687ca4f26a09459a77e07ebd7efddd1a5766725ec60eb0c83a2abe67f7a23808f7deb1c1226727776eaf7f9607ae09db2 + languageName: node + linkType: hard + "parse-passwd@npm:^1.0.0": version: 1.0.0 resolution: "parse-passwd@npm:1.0.0" @@ -27513,6 +27857,13 @@ __metadata: languageName: node linkType: hard +"path-exists@npm:^5.0.0": + version: 5.0.0 + resolution: "path-exists@npm:5.0.0" + checksum: 8ca842868cab09423994596eb2c5ec2a971c17d1a3cb36dbf060592c730c725cd524b9067d7d2a1e031fef9ba7bd2ac6dc5ec9fb92aa693265f7be3987045254 + languageName: node + linkType: hard + "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -27745,6 +28096,16 @@ __metadata: languageName: node linkType: hard +"pkg-conf@npm:^4.0.0": + version: 4.0.0 + resolution: "pkg-conf@npm:4.0.0" + dependencies: + find-up: ^6.0.0 + load-json-file: ^7.0.0 + checksum: 6da0c064a74f6c7ae80d7d68c5853e14f7e762a2a80c6ca9e0aa827002b90b69c86fefe3bac830b10a6f1739e7f96a1f728637f2a141e50b0fdafe92a2c3eab6 + languageName: node + linkType: hard + "pkg-dir@npm:^3.0.0": version: 3.0.0 resolution: "pkg-dir@npm:3.0.0" @@ -27814,6 +28175,15 @@ __metadata: languageName: node linkType: hard +"plur@npm:^5.1.0": + version: 5.1.0 + resolution: "plur@npm:5.1.0" + dependencies: + irregular-plurals: ^3.3.0 + checksum: 57e400dc4b926768fb0abab7f8688fe17e85673712134546e7beaaee188bae7e0504976e847d7e41d0d6103ff2fd61204095f03c2a45de19a8bad15aecb45cc1 + languageName: node + linkType: hard + "pluralize@npm:^8.0.0": version: 8.0.0 resolution: "pluralize@npm:8.0.0" @@ -28519,6 +28889,15 @@ __metadata: languageName: node linkType: hard +"pretty-ms@npm:^8.0.0": + version: 8.0.0 + resolution: "pretty-ms@npm:8.0.0" + dependencies: + parse-ms: ^3.0.0 + checksum: b7d2a8182887af0e5ab93f9df331f10db9b8eda86855e2de115eb01a6c501bde5631a8813b1b0abdd7d045e79b08ae875369a8fd279a3dacd6d9e572bdd3bfa6 + languageName: node + linkType: hard + "pretty-time@npm:^1.1.0": version: 1.1.0 resolution: "pretty-time@npm:1.1.0" @@ -31184,7 +31563,7 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": +"stack-utils@npm:^2.0.3, stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" dependencies: @@ -31725,6 +32104,18 @@ __metadata: languageName: node linkType: hard +"supertap@npm:^3.0.1": + version: 3.0.1 + resolution: "supertap@npm:3.0.1" + dependencies: + indent-string: ^5.0.0 + js-yaml: ^3.14.1 + serialize-error: ^7.0.1 + strip-ansi: ^7.0.1 + checksum: ee3d71c1d25f7f15d4a849e72b0c5f430df7cd8f702cf082fdbec5642a9546be6557766745655fa3a3e9c88f7c7eed849f2d74457b5b72cb9d94a779c0c8a948 + languageName: node + linkType: hard + "supertest@npm:^6.3.3": version: 6.3.3 resolution: "supertest@npm:6.3.3" @@ -32001,6 +32392,13 @@ __metadata: languageName: node linkType: hard +"temp-dir@npm:^3.0.0": + version: 3.0.0 + resolution: "temp-dir@npm:3.0.0" + checksum: 577211e995d1d584dd60f1469351d45e8a5b4524e4a9e42d3bdd12cfde1d0bb8f5898311bef24e02aaafb69514c1feb58c7b4c33dcec7129da3b0861a4ca935b + languageName: node + linkType: hard + "temp@npm:^0.8.4": version: 0.8.4 resolution: "temp@npm:0.8.4" @@ -32201,6 +32599,13 @@ __metadata: languageName: node linkType: hard +"time-zone@npm:^1.0.0": + version: 1.0.0 + resolution: "time-zone@npm:1.0.0" + checksum: e46f5a69b8c236dcd8e91e29d40d4e7a3495ed4f59888c3f84ce1d9678e20461421a6ba41233509d47dd94bc18f1a4377764838b21b584663f942b3426dcbce8 + languageName: node + linkType: hard + "tiny-each-async@npm:2.0.3": version: 2.0.3 resolution: "tiny-each-async@npm:2.0.3" @@ -34039,6 +34444,13 @@ __metadata: languageName: node linkType: hard +"well-known-symbols@npm:^2.0.0": + version: 2.0.0 + resolution: "well-known-symbols@npm:2.0.0" + checksum: 4f54bbc3012371cb4d228f436891b8e7536d34ac61a57541890257e96788608e096231e0121ac24d08ef2f908b3eb2dc0adba35023eaeb2a7df655da91415402 + languageName: node + linkType: hard + "whatwg-encoding@npm:^2.0.0": version: 2.0.0 resolution: "whatwg-encoding@npm:2.0.0" @@ -34313,6 +34725,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^4.0.1 + checksum: 8dbb0e2512c2f72ccc20ccedab9986c7d02d04039ed6e8780c987dc4940b793339c50172a1008eed7747001bfacc0ca47562668a069a7506c46c77d7ba3926a9 + languageName: node + linkType: hard + "ws@npm:8.13.0, ws@npm:^8.11.0, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.2.3": version: 8.13.0 resolution: "ws@npm:8.13.0"