refactor(server): use ava (#4120)

This commit is contained in:
Alex Yang 2023-09-01 14:41:29 -05:00 committed by GitHub
parent 8845bb9b4b
commit eb1a21265f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1313 additions and 952 deletions

View File

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

View File

@ -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);
});
}

View File

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

View File

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

View File

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

View File

@ -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
) {}

View File

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

View File

@ -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<Express>): Promise<{

View File

@ -1,9 +1,9 @@
/// <reference types="../global.d.ts" />
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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -1,9 +1,9 @@
/// <reference types="../global.d.ts" />
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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

450
yarn.lock
View File

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