mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 10:06:17 +08:00
refactor(server): config system (#11081)
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { buildAppModule } from '../../app.module';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
test.before('start app', async t => {
|
||||
// @ts-expect-error override
|
||||
AFFiNE.flavor = {
|
||||
type: 'doc',
|
||||
doc: true,
|
||||
} as typeof AFFiNE.flavor;
|
||||
const app = await createTestingApp({
|
||||
imports: [buildAppModule()],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should init app', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/info')
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.flavor, 'doc');
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import GraphQLUpload, {
|
||||
type FileUpload,
|
||||
} from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { buildAppModule } from '../../app.module';
|
||||
import { Public } from '../../core/auth';
|
||||
import { createTestingApp, TestingApp } from '../utils';
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: TestingApp;
|
||||
}>;
|
||||
|
||||
@Resolver(() => String)
|
||||
class TestResolver {
|
||||
@Public()
|
||||
@Mutation(() => Number)
|
||||
async upload(
|
||||
@Args({ name: 'body', type: () => GraphQLUpload })
|
||||
body: FileUpload
|
||||
): Promise<number> {
|
||||
const size = await new Promise<number>((resolve, reject) => {
|
||||
const stream = body.createReadStream();
|
||||
let size = 0;
|
||||
stream.on('data', chunk => (size += chunk.length));
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => resolve(size));
|
||||
});
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
test.before('start app', async t => {
|
||||
// @ts-expect-error override
|
||||
AFFiNE.flavor = {
|
||||
type: 'graphql',
|
||||
graphql: true,
|
||||
} as typeof AFFiNE.flavor;
|
||||
const app = await createTestingApp({
|
||||
imports: [buildAppModule()],
|
||||
providers: [TestResolver],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should init app', async t => {
|
||||
await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
error
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
const response = await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `query {
|
||||
serverConfig {
|
||||
name
|
||||
version
|
||||
type
|
||||
features
|
||||
}
|
||||
}`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const config = response.body.data.serverConfig;
|
||||
|
||||
t.is(config.type, 'Affine');
|
||||
t.true(Array.isArray(config.features));
|
||||
// make sure the request id is set
|
||||
t.truthy(response.headers['x-request-id']);
|
||||
});
|
||||
|
||||
test('should return 404 for unknown path', async t => {
|
||||
await request(t.context.app.getHttpServer()).get('/unknown').expect(404);
|
||||
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should be able to call apis', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/info')
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.flavor, 'graphql');
|
||||
// make sure the request id is set
|
||||
t.truthy(res.headers['x-request-id']);
|
||||
});
|
||||
|
||||
test('should not throw internal error when graphql call with invalid params', async t => {
|
||||
await t.throwsAsync(t.context.app.gql(`query { workspace("1") }`), {
|
||||
message: /Failed to execute gql: query { workspace\("1"\) \}, status: 400/,
|
||||
});
|
||||
});
|
||||
|
||||
test('should can send maximum size of body', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const body = Buffer.from('a'.repeat(10 * 1024 * 1024 - 1));
|
||||
const res = await app
|
||||
.POST('/graphql')
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'upload',
|
||||
query: `mutation upload($body: Upload!) { upload(body: $body) }`,
|
||||
variables: { body: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.body'] }))
|
||||
.attach(
|
||||
'0',
|
||||
body,
|
||||
`body-${Math.random().toString(16).substring(2, 10)}.data`
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
t.is(Number(res.body.data.upload), body.length);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { buildAppModule } from '../../app.module';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
test.before('start app', async t => {
|
||||
// @ts-expect-error override
|
||||
AFFiNE.flavor = {
|
||||
type: 'renderer',
|
||||
renderer: true,
|
||||
} as typeof AFFiNE.flavor;
|
||||
const app = await createTestingApp({
|
||||
imports: [buildAppModule()],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should init app', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/info')
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.flavor, 'renderer');
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { buildAppModule } from '../../app.module';
|
||||
import { Config } from '../../base';
|
||||
import { Public } from '../../core/auth';
|
||||
import { ServerService } from '../../core/config';
|
||||
import { createTestingApp, type TestingApp } from '../utils';
|
||||
@@ -49,18 +48,16 @@ export class TestResolver {
|
||||
|
||||
test.before('init selfhost server', async t => {
|
||||
// @ts-expect-error override
|
||||
AFFiNE.isSelfhosted = true;
|
||||
AFFiNE.flavor.renderer = true;
|
||||
globalThis.env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
const app = await createTestingApp({
|
||||
imports: [buildAppModule()],
|
||||
imports: [buildAppModule(globalThis.env)],
|
||||
controllers: [TestResolver],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
t.context.db = t.context.app.get(PrismaClient);
|
||||
const config = app.get(Config);
|
||||
|
||||
const staticPath = path.join(config.projectRoot, 'static');
|
||||
const staticPath = path.join(env.projectRoot, 'static');
|
||||
initTestStaticFiles(staticPath);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { buildAppModule } from '../../app.module';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
test.before('start app', async t => {
|
||||
// @ts-expect-error override
|
||||
AFFiNE.flavor = {
|
||||
type: 'sync',
|
||||
sync: true,
|
||||
} as typeof AFFiNE.flavor;
|
||||
const app = await createTestingApp({
|
||||
imports: [buildAppModule()],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should init app', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/info')
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.flavor, 'sync');
|
||||
});
|
||||
@@ -5,10 +5,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { AuthModule } from '../../core/auth';
|
||||
import { AuthService } from '../../core/auth/service';
|
||||
import { FeatureModule } from '../../core/features';
|
||||
import { UserModule } from '../../core/user';
|
||||
import {
|
||||
createTestingApp,
|
||||
currentUser,
|
||||
@@ -23,9 +20,7 @@ const test = ava as TestFn<{
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [FeatureModule, UserModule, AuthModule],
|
||||
});
|
||||
const app = await createTestingApp();
|
||||
|
||||
t.context.auth = app.get(AuthService);
|
||||
t.context.db = app.get(PrismaClient);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import test from 'ava';
|
||||
|
||||
import { Config, ConfigModule } from '../base/config';
|
||||
import { createTestingModule } from './utils';
|
||||
|
||||
let config: Config;
|
||||
let module: TestingModule;
|
||||
test.beforeEach(async () => {
|
||||
module = await createTestingModule({}, false);
|
||||
config = module.get(Config);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be able to get config', t => {
|
||||
t.true(typeof config.server.host === 'string');
|
||||
t.is(config.projectRoot, process.cwd());
|
||||
t.is(config.NODE_ENV, 'test');
|
||||
});
|
||||
|
||||
test('should be able to override config', async t => {
|
||||
const module = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
server: {
|
||||
host: 'testing',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
const config = module.get(Config);
|
||||
|
||||
t.is(config.server.host, 'testing');
|
||||
|
||||
await module.close();
|
||||
});
|
||||
@@ -7,14 +7,7 @@ import { AuthService } from '../core/auth';
|
||||
import { QuotaModule } from '../core/quota';
|
||||
import { CopilotModule } from '../plugins/copilot';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderService,
|
||||
FalProvider,
|
||||
OpenAIProvider,
|
||||
PerplexityProvider,
|
||||
registerCopilotProvider,
|
||||
unregisterCopilotProvider,
|
||||
} from '../plugins/copilot/providers';
|
||||
import { CopilotProviderFactory } from '../plugins/copilot/providers';
|
||||
import {
|
||||
CopilotChatTextExecutor,
|
||||
CopilotWorkflowService,
|
||||
@@ -32,7 +25,7 @@ type Tester = {
|
||||
auth: AuthService;
|
||||
module: TestingModule;
|
||||
prompt: PromptService;
|
||||
provider: CopilotProviderService;
|
||||
factory: CopilotProviderFactory;
|
||||
workflow: CopilotWorkflowService;
|
||||
executors: {
|
||||
image: CopilotChatImageExecutor;
|
||||
@@ -67,9 +60,9 @@ const runIfCopilotConfigured = test.macro(
|
||||
test.serial.before(async t => {
|
||||
const module = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
plugins: {
|
||||
copilot: {
|
||||
ConfigModule.override({
|
||||
copilot: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: process.env.COPILOT_OPENAI_API_KEY,
|
||||
},
|
||||
@@ -79,6 +72,9 @@ test.serial.before(async t => {
|
||||
perplexity: {
|
||||
apiKey: process.env.COPILOT_PERPLEXITY_API_KEY,
|
||||
},
|
||||
gemini: {
|
||||
apiKey: process.env.COPILOT_GOOGLE_API_KEY,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -89,13 +85,13 @@ test.serial.before(async t => {
|
||||
|
||||
const auth = module.get(AuthService);
|
||||
const prompt = module.get(PromptService);
|
||||
const provider = module.get(CopilotProviderService);
|
||||
const factory = module.get(CopilotProviderFactory);
|
||||
const workflow = module.get(CopilotWorkflowService);
|
||||
|
||||
t.context.module = module;
|
||||
t.context.auth = auth;
|
||||
t.context.prompt = prompt;
|
||||
t.context.provider = provider;
|
||||
t.context.factory = factory;
|
||||
t.context.workflow = workflow;
|
||||
t.context.executors = {
|
||||
image: module.get(CopilotChatImageExecutor),
|
||||
@@ -113,10 +109,6 @@ test.serial.before(async t => {
|
||||
executors.html.register();
|
||||
executors.json.register();
|
||||
|
||||
registerCopilotProvider(OpenAIProvider);
|
||||
registerCopilotProvider(FalProvider);
|
||||
registerCopilotProvider(PerplexityProvider);
|
||||
|
||||
for (const name of await prompt.listNames()) {
|
||||
await prompt.delete(name);
|
||||
}
|
||||
@@ -126,12 +118,6 @@ test.serial.before(async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test.after(async _ => {
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
unregisterCopilotProvider(FalProvider.type);
|
||||
unregisterCopilotProvider(PerplexityProvider.type);
|
||||
});
|
||||
|
||||
test.after(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
@@ -523,12 +509,10 @@ for (const { name, promptName, messages, verifier, type } of actions) {
|
||||
`should be able to run action: ${promptName}${name ? ` - ${name}` : ''}`,
|
||||
runIfCopilotConfigured,
|
||||
async t => {
|
||||
const { provider: providerService, prompt: promptService } = t.context;
|
||||
const { factory, prompt: promptService } = t.context;
|
||||
const prompt = (await promptService.get(promptName))!;
|
||||
t.truthy(prompt, 'should have prompt');
|
||||
const provider = (await providerService.getProviderByModel(
|
||||
prompt.model
|
||||
))!;
|
||||
const provider = (await factory.getProviderByModel(prompt.model))!;
|
||||
t.truthy(provider, 'should have provider');
|
||||
await retry(`action: ${promptName}`, t, async t => {
|
||||
if (type === 'text' && 'generateText' in provider) {
|
||||
|
||||
@@ -6,12 +6,11 @@ import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { AppModule } from '../app.module';
|
||||
import { JobQueue } from '../base';
|
||||
import { ConfigModule } from '../base/config';
|
||||
import { AuthService } from '../core/auth';
|
||||
import { DocReader } from '../core/doc';
|
||||
import { WorkspaceModule } from '../core/workspaces';
|
||||
import { CopilotModule } from '../plugins/copilot';
|
||||
import {
|
||||
CopilotContextDocJob,
|
||||
CopilotContextService,
|
||||
@@ -19,14 +18,11 @@ import {
|
||||
import { MockEmbeddingClient } from '../plugins/copilot/context/embedding';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderService,
|
||||
FalProvider,
|
||||
CopilotProviderFactory,
|
||||
OpenAIProvider,
|
||||
PerplexityProvider,
|
||||
registerCopilotProvider,
|
||||
unregisterCopilotProvider,
|
||||
} from '../plugins/copilot/providers';
|
||||
import { CopilotStorage } from '../plugins/copilot/storage';
|
||||
import { MockCopilotProvider } from './mocks';
|
||||
import {
|
||||
acceptInviteById,
|
||||
createTestingApp,
|
||||
@@ -53,7 +49,6 @@ import {
|
||||
listContextDocAndFiles,
|
||||
matchFiles,
|
||||
matchWorkspaceDocs,
|
||||
MockCopilotTestProvider,
|
||||
sse2array,
|
||||
textToEventStream,
|
||||
unsplashSearch,
|
||||
@@ -67,7 +62,7 @@ const test = ava as TestFn<{
|
||||
context: CopilotContextService;
|
||||
jobs: CopilotContextDocJob;
|
||||
prompt: PromptService;
|
||||
provider: CopilotProviderService;
|
||||
factory: CopilotProviderFactory;
|
||||
storage: CopilotStorage;
|
||||
u1: TestUser;
|
||||
}>;
|
||||
@@ -75,24 +70,19 @@ const test = ava as TestFn<{
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
plugins: {
|
||||
copilot: {
|
||||
openai: {
|
||||
apiKey: '1',
|
||||
},
|
||||
fal: {
|
||||
apiKey: '1',
|
||||
},
|
||||
perplexity: {
|
||||
apiKey: '1',
|
||||
},
|
||||
unsplashKey: process.env.UNSPLASH_ACCESS_KEY || '1',
|
||||
ConfigModule.override({
|
||||
copilot: {
|
||||
providers: {
|
||||
openai: { apiKey: '1' },
|
||||
fal: {},
|
||||
perplexity: {},
|
||||
},
|
||||
unsplash: {
|
||||
key: process.env.UNSPLASH_ACCESS_KEY || '1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
WorkspaceModule,
|
||||
CopilotModule,
|
||||
AppModule,
|
||||
],
|
||||
tapModule: m => {
|
||||
// use real JobQueue for testing
|
||||
@@ -105,6 +95,7 @@ test.before(async t => {
|
||||
};
|
||||
},
|
||||
});
|
||||
m.overrideProvider(OpenAIProvider).useClass(MockCopilotProvider);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -129,14 +120,9 @@ test.beforeEach(async t => {
|
||||
Sinon.restore();
|
||||
const { app, prompt } = t.context;
|
||||
await app.initTestingDB();
|
||||
await prompt.onModuleInit();
|
||||
await prompt.onApplicationBootstrap();
|
||||
t.context.u1 = await app.signupV1('u1@affine.pro');
|
||||
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
unregisterCopilotProvider(FalProvider.type);
|
||||
unregisterCopilotProvider(PerplexityProvider.type);
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
await prompt.set(promptName, 'test', [
|
||||
{ role: 'system', content: 'hello {{word}}' },
|
||||
]);
|
||||
@@ -761,13 +747,12 @@ test('should be able to manage context', async t => {
|
||||
'should throw error if create context with invalid session id'
|
||||
);
|
||||
|
||||
const context = createCopilotContext(app, workspaceId, sessionId);
|
||||
await t.notThrowsAsync(context, 'should create context with chat session');
|
||||
const context = await createCopilotContext(app, workspaceId, sessionId);
|
||||
|
||||
const list = await listContext(app, workspaceId, sessionId);
|
||||
t.deepEqual(
|
||||
list.map(f => ({ id: f.id })),
|
||||
[{ id: await context }],
|
||||
[{ id: context }],
|
||||
'should list context'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,18 +19,14 @@ import {
|
||||
import { MockEmbeddingClient } from '../plugins/copilot/context/embedding';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderService,
|
||||
CopilotCapability,
|
||||
CopilotProviderFactory,
|
||||
CopilotProviderType,
|
||||
OpenAIProvider,
|
||||
registerCopilotProvider,
|
||||
unregisterCopilotProvider,
|
||||
} from '../plugins/copilot/providers';
|
||||
import { CitationParser } from '../plugins/copilot/providers/perplexity';
|
||||
import { ChatSessionService } from '../plugins/copilot/session';
|
||||
import { CopilotStorage } from '../plugins/copilot/storage';
|
||||
import {
|
||||
CopilotCapability,
|
||||
CopilotProviderType,
|
||||
} from '../plugins/copilot/types';
|
||||
import {
|
||||
CopilotChatTextExecutor,
|
||||
CopilotWorkflowService,
|
||||
@@ -50,8 +46,9 @@ import {
|
||||
} from '../plugins/copilot/workflow/executor';
|
||||
import { AutoRegisteredWorkflowExecutor } from '../plugins/copilot/workflow/executor/utils';
|
||||
import { WorkflowGraphList } from '../plugins/copilot/workflow/graph';
|
||||
import { MockCopilotProvider } from './mocks';
|
||||
import { createTestingModule, TestingModule } from './utils';
|
||||
import { MockCopilotTestProvider, WorkflowTestCases } from './utils/copilot';
|
||||
import { WorkflowTestCases } from './utils/copilot';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
@@ -60,7 +57,7 @@ const test = ava as TestFn<{
|
||||
event: EventBus;
|
||||
context: CopilotContextService;
|
||||
prompt: PromptService;
|
||||
provider: CopilotProviderService;
|
||||
factory: CopilotProviderFactory;
|
||||
session: ChatSessionService;
|
||||
jobs: CopilotContextDocJob;
|
||||
storage: CopilotStorage;
|
||||
@@ -77,9 +74,9 @@ let userId: string;
|
||||
test.before(async t => {
|
||||
const module = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
plugins: {
|
||||
copilot: {
|
||||
ConfigModule.override({
|
||||
copilot: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: process.env.COPILOT_OPENAI_API_KEY ?? '1',
|
||||
},
|
||||
@@ -95,6 +92,9 @@ test.before(async t => {
|
||||
QuotaModule,
|
||||
CopilotModule,
|
||||
],
|
||||
tapModule: builder => {
|
||||
builder.overrideProvider(OpenAIProvider).useClass(MockCopilotProvider);
|
||||
},
|
||||
});
|
||||
|
||||
const auth = module.get(AuthService);
|
||||
@@ -102,7 +102,7 @@ test.before(async t => {
|
||||
const event = module.get(EventBus);
|
||||
const context = module.get(CopilotContextService);
|
||||
const prompt = module.get(PromptService);
|
||||
const provider = module.get(CopilotProviderService);
|
||||
const factory = module.get(CopilotProviderFactory);
|
||||
const session = module.get(ChatSessionService);
|
||||
const workflow = module.get(CopilotWorkflowService);
|
||||
const jobs = module.get(CopilotContextDocJob);
|
||||
@@ -114,7 +114,7 @@ test.before(async t => {
|
||||
t.context.event = event;
|
||||
t.context.context = context;
|
||||
t.context.prompt = prompt;
|
||||
t.context.provider = provider;
|
||||
t.context.factory = factory;
|
||||
t.context.session = session;
|
||||
t.context.workflow = workflow;
|
||||
t.context.jobs = jobs;
|
||||
@@ -131,7 +131,7 @@ test.beforeEach(async t => {
|
||||
Sinon.restore();
|
||||
const { module, auth, prompt } = t.context;
|
||||
await module.initTestingDB();
|
||||
await prompt.onModuleInit();
|
||||
await prompt.onApplicationBootstrap();
|
||||
const user = await auth.signUp('test@affine.pro', '123456');
|
||||
userId = user.id;
|
||||
});
|
||||
@@ -730,10 +730,10 @@ test('should handle params correctly in chat session', async t => {
|
||||
// ==================== provider ====================
|
||||
|
||||
test('should be able to get provider', async t => {
|
||||
const { provider } = t.context;
|
||||
const { factory } = t.context;
|
||||
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
const p = await factory.getProviderByCapability(
|
||||
CopilotCapability.TextToText
|
||||
);
|
||||
t.is(
|
||||
@@ -744,108 +744,40 @@ test('should be able to get provider', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToEmbedding
|
||||
const p = await factory.getProviderByCapability(
|
||||
CopilotCapability.ImageToImage,
|
||||
{ model: 'lora/image-to-image' }
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'fal',
|
||||
'should get provider support text-to-embedding'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToImage
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
'should get provider support text-to-image'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.ImageToImage
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
'should get provider support image-to-image'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.ImageToText
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'fal',
|
||||
'should get provider support image-to-text'
|
||||
);
|
||||
}
|
||||
|
||||
// text-to-image use fal by default, but this case can use
|
||||
// model dall-e-3 to select openai provider
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
CopilotCapability.TextToImage,
|
||||
'dall-e-3'
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support text-to-image and model'
|
||||
);
|
||||
}
|
||||
|
||||
// gpt4o is not defined now, but it already published by openai
|
||||
// we should check from online api if it is available
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
const p = await factory.getProviderByCapability(
|
||||
CopilotCapability.ImageToText,
|
||||
'gpt-4o-2024-08-06'
|
||||
{ prefer: CopilotProviderType.FAL }
|
||||
);
|
||||
t.is(
|
||||
p?.type.toString(),
|
||||
'openai',
|
||||
'should get provider support text-to-image and model'
|
||||
'fal',
|
||||
'should get provider support text-to-embedding'
|
||||
);
|
||||
}
|
||||
|
||||
// if a model is not defined and not available in online api
|
||||
// it should return null
|
||||
{
|
||||
const p = await provider.getProviderByCapability(
|
||||
const p = await factory.getProviderByCapability(
|
||||
CopilotCapability.ImageToText,
|
||||
'gpt-4-not-exist'
|
||||
{ model: 'gpt-4-not-exist' }
|
||||
);
|
||||
t.falsy(p, 'should not get provider');
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to register test provider', async t => {
|
||||
const { provider } = t.context;
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
const assertProvider = async (cap: CopilotCapability) => {
|
||||
const p = await provider.getProviderByCapability(cap, 'test');
|
||||
t.is(
|
||||
p?.type,
|
||||
CopilotProviderType.Test,
|
||||
`should get test provider with ${cap}`
|
||||
);
|
||||
};
|
||||
|
||||
await assertProvider(CopilotCapability.TextToText);
|
||||
await assertProvider(CopilotCapability.TextToEmbedding);
|
||||
await assertProvider(CopilotCapability.TextToImage);
|
||||
await assertProvider(CopilotCapability.ImageToImage);
|
||||
await assertProvider(CopilotCapability.ImageToText);
|
||||
});
|
||||
|
||||
// ==================== workflow ====================
|
||||
|
||||
// this test used to preview the final result of the workflow
|
||||
@@ -854,7 +786,6 @@ test.skip('should be able to preview workflow', async t => {
|
||||
const { prompt, workflow, executors } = t.context;
|
||||
|
||||
executors.text.register();
|
||||
registerCopilotProvider(OpenAIProvider);
|
||||
|
||||
for (const p of prompts) {
|
||||
await prompt.set(p.name, p.model, p.messages, p.config);
|
||||
@@ -878,8 +809,6 @@ test.skip('should be able to preview workflow', async t => {
|
||||
}
|
||||
console.log('final stream result:', result);
|
||||
t.truthy(result, 'should return result');
|
||||
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
});
|
||||
|
||||
const runWorkflow = async function* runWorkflow(
|
||||
@@ -900,8 +829,6 @@ test('should be able to run pre defined workflow', async t => {
|
||||
executors.text.register();
|
||||
executors.html.register();
|
||||
executors.json.register();
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
const executor = Sinon.spy(executors.text, 'next');
|
||||
|
||||
@@ -941,17 +868,12 @@ test('should be able to run pre defined workflow', async t => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unregisterCopilotProvider(MockCopilotTestProvider.type);
|
||||
registerCopilotProvider(OpenAIProvider);
|
||||
});
|
||||
|
||||
test('should be able to run workflow', async t => {
|
||||
const { workflow, executors } = t.context;
|
||||
|
||||
executors.text.register();
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
|
||||
const executor = Sinon.spy(executors.text, 'next');
|
||||
|
||||
@@ -998,9 +920,6 @@ test('should be able to run workflow', async t => {
|
||||
'graph params should correct'
|
||||
);
|
||||
}
|
||||
|
||||
unregisterCopilotProvider(MockCopilotTestProvider.type);
|
||||
registerCopilotProvider(OpenAIProvider);
|
||||
});
|
||||
|
||||
// ==================== workflow executor ====================
|
||||
@@ -1037,18 +956,16 @@ test('should be able to run executor', async t => {
|
||||
});
|
||||
|
||||
test('should be able to run text executor', async t => {
|
||||
const { executors, provider, prompt } = t.context;
|
||||
const { executors, factory, prompt } = t.context;
|
||||
|
||||
executors.text.register();
|
||||
const executor = getWorkflowExecutor(executors.text.type);
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
await prompt.set('test', 'test', [
|
||||
{ role: 'system', content: 'hello {{word}}' },
|
||||
]);
|
||||
// mock provider
|
||||
const testProvider =
|
||||
(await provider.getProviderByModel<CopilotCapability.TextToText>('test'))!;
|
||||
(await factory.getProviderByModel<CopilotCapability.TextToText>('test'))!;
|
||||
const text = Sinon.spy(testProvider, 'generateText');
|
||||
const textStream = Sinon.spy(testProvider, 'generateTextStream');
|
||||
|
||||
@@ -1103,23 +1020,19 @@ test('should be able to run text executor', async t => {
|
||||
}
|
||||
|
||||
Sinon.restore();
|
||||
unregisterCopilotProvider(MockCopilotTestProvider.type);
|
||||
registerCopilotProvider(OpenAIProvider);
|
||||
});
|
||||
|
||||
test('should be able to run image executor', async t => {
|
||||
const { executors, provider, prompt } = t.context;
|
||||
const { executors, factory, prompt } = t.context;
|
||||
|
||||
executors.image.register();
|
||||
const executor = getWorkflowExecutor(executors.image.type);
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
registerCopilotProvider(MockCopilotTestProvider);
|
||||
await prompt.set('test', 'test', [
|
||||
{ role: 'user', content: 'tag1, tag2, tag3, {{#tags}}{{.}}, {{/tags}}' },
|
||||
]);
|
||||
// mock provider
|
||||
const testProvider =
|
||||
(await provider.getProviderByModel<CopilotCapability.TextToImage>('test'))!;
|
||||
(await factory.getProviderByModel<CopilotCapability.TextToImage>('test'))!;
|
||||
const image = Sinon.spy(testProvider, 'generateImages');
|
||||
const imageStream = Sinon.spy(testProvider, 'generateImagesStream');
|
||||
|
||||
@@ -1184,8 +1097,6 @@ test('should be able to run image executor', async t => {
|
||||
}
|
||||
|
||||
Sinon.restore();
|
||||
unregisterCopilotProvider(MockCopilotTestProvider.type);
|
||||
registerCopilotProvider(OpenAIProvider);
|
||||
});
|
||||
|
||||
test('CitationParser should replace citation placeholders with URLs', t => {
|
||||
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
TestingModule as NestjsTestingModule,
|
||||
TestingModuleBuilder,
|
||||
} from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { FunctionalityModules } from '../app.module';
|
||||
import { AFFiNELogger } from '../base';
|
||||
import { AFFiNELogger, EventBus, JobQueue } from '../base';
|
||||
import { createFactory, MockEventBus, MockJobQueue } from './mocks';
|
||||
import { TEST_LOG_LEVEL } from './utils';
|
||||
|
||||
interface TestingModuleMetadata extends ModuleMetadata {
|
||||
@@ -15,10 +17,13 @@ interface TestingModuleMetadata extends ModuleMetadata {
|
||||
|
||||
export interface TestingModule extends NestjsTestingModule {
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
create: ReturnType<typeof createFactory>;
|
||||
queue: MockJobQueue;
|
||||
event: MockEventBus;
|
||||
}
|
||||
|
||||
export async function createModule(
|
||||
metadata: TestingModuleMetadata
|
||||
metadata: TestingModuleMetadata = {}
|
||||
): Promise<TestingModule> {
|
||||
const { tapModule, ...meta } = metadata;
|
||||
|
||||
@@ -27,6 +32,12 @@ export async function createModule(
|
||||
imports: [...FunctionalityModules, ...(meta.imports ?? [])],
|
||||
});
|
||||
|
||||
builder
|
||||
.overrideProvider(JobQueue)
|
||||
.useValue(new MockJobQueue())
|
||||
.overrideProvider(EventBus)
|
||||
.useValue(new MockEventBus());
|
||||
|
||||
// when custom override happens
|
||||
if (tapModule) {
|
||||
tapModule(builder);
|
||||
@@ -44,6 +55,9 @@ export async function createModule(
|
||||
module[Symbol.asyncDispose] = async () => {
|
||||
await module.close();
|
||||
};
|
||||
module.create = createFactory(module.get(PrismaClient));
|
||||
module.queue = module.get(JobQueue);
|
||||
module.event = module.get(EventBus);
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { DocRendererModule } from '../../core/doc-renderer';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
@@ -45,13 +44,11 @@ function initTestStaticFiles(staticPath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
test.before('init selfhost server', async t => {
|
||||
test.before(async t => {
|
||||
const staticPath = new Package('@affine/server').join('static').value;
|
||||
initTestStaticFiles(staticPath);
|
||||
|
||||
const app = await createTestingApp({
|
||||
imports: [DocRendererModule],
|
||||
});
|
||||
const app = await createTestingApp();
|
||||
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
+3
-8
@@ -1,7 +1,7 @@
|
||||
import { getCurrentUserQuery } from '@affine/graphql';
|
||||
|
||||
import { Mockers } from '../mocks';
|
||||
import { app, e2e } from './test';
|
||||
import { Mockers } from '../../mocks';
|
||||
import { app, e2e } from '../test';
|
||||
|
||||
e2e('should create test app correctly', async t => {
|
||||
t.truthy(app);
|
||||
@@ -18,12 +18,7 @@ e2e('should mock queue work', async t => {
|
||||
e2e('should handle http request', async t => {
|
||||
const res = await app.GET('/info');
|
||||
t.is(res.status, 200);
|
||||
t.is(res.body.compatibility, AFFiNE.version);
|
||||
});
|
||||
|
||||
e2e('should handle gql request', async t => {
|
||||
const user = await app.gql({ query: getCurrentUserQuery });
|
||||
t.is(user.currentUser, null);
|
||||
t.is(res.body.compatibility, env.version);
|
||||
});
|
||||
|
||||
e2e('should create workspace with owner', async t => {
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getCurrentUserQuery } from '@affine/graphql';
|
||||
|
||||
import { createApp } from '../create-app';
|
||||
import { e2e } from '../test';
|
||||
|
||||
e2e('should init doc service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'doc';
|
||||
await using app = await createApp();
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'doc');
|
||||
|
||||
await t.throwsAsync(app.gql({ query: getCurrentUserQuery }));
|
||||
});
|
||||
|
||||
e2e('should init graphql service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'graphql';
|
||||
await using app = await createApp();
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
|
||||
t.is(res.body.flavor, 'graphql');
|
||||
|
||||
const user = await app.gql({ query: getCurrentUserQuery });
|
||||
t.is(user.currentUser, null);
|
||||
});
|
||||
|
||||
e2e('should init sync service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'sync';
|
||||
await using app = await createApp();
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'sync');
|
||||
});
|
||||
|
||||
e2e('should init renderer service', async t => {
|
||||
// @ts-expect-error override
|
||||
globalThis.env.FLAVOR = 'renderer';
|
||||
await using app = await createApp();
|
||||
|
||||
const res = await app.GET('/info').expect(200);
|
||||
t.is(res.body.flavor, 'renderer');
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
AFFiNELogger,
|
||||
CacheInterceptor,
|
||||
CloudThrottlerGuard,
|
||||
EventBus,
|
||||
GlobalExceptionFilter,
|
||||
JobQueue,
|
||||
OneMB,
|
||||
@@ -23,6 +24,7 @@ import { Mailer } from '../../core/mail';
|
||||
import {
|
||||
createFactory,
|
||||
MockedUser,
|
||||
MockEventBus,
|
||||
MockJobQueue,
|
||||
MockMailer,
|
||||
MockUser,
|
||||
@@ -181,23 +183,19 @@ export class TestingApp extends NestApplication {
|
||||
}
|
||||
}
|
||||
|
||||
let GLOBAL_APP_INSTANCE: TestingApp | null = null;
|
||||
export async function createApp(
|
||||
metadata: TestingAppMetadata = {}
|
||||
): Promise<TestingApp> {
|
||||
if (GLOBAL_APP_INSTANCE) {
|
||||
return GLOBAL_APP_INSTANCE;
|
||||
}
|
||||
|
||||
const { buildAppModule } = await import('../../app.module');
|
||||
const { tapModule, tapApp } = metadata;
|
||||
|
||||
const builder = Test.createTestingModule({
|
||||
imports: [buildAppModule()],
|
||||
imports: [buildAppModule(globalThis.env)],
|
||||
});
|
||||
|
||||
builder.overrideProvider(Mailer).useValue(new MockMailer());
|
||||
builder.overrideProvider(JobQueue).useValue(new MockJobQueue());
|
||||
builder.overrideProvider(EventBus).useValue(new MockEventBus());
|
||||
|
||||
// when custom override happens
|
||||
if (tapModule) {
|
||||
@@ -240,6 +238,5 @@ export async function createApp(
|
||||
|
||||
await app.init();
|
||||
|
||||
GLOBAL_APP_INSTANCE = app;
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import test from 'ava';
|
||||
|
||||
import { Env } from '../env';
|
||||
|
||||
const envs = { ...process.env };
|
||||
test.beforeEach(() => {
|
||||
process.env = { ...envs };
|
||||
});
|
||||
|
||||
test('should init env', t => {
|
||||
t.true(globalThis.env.testing);
|
||||
});
|
||||
|
||||
test('should read NODE_ENV', t => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
t.deepEqual(
|
||||
['test', 'development', 'production'].map(envVal => {
|
||||
process.env.NODE_ENV = envVal;
|
||||
const env = new Env();
|
||||
return env.NODE_ENV;
|
||||
}),
|
||||
['test', 'development', 'production']
|
||||
);
|
||||
|
||||
t.throws(
|
||||
() => {
|
||||
process.env.NODE_ENV = 'unknown';
|
||||
new Env();
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Invalid value "unknown" for environment variable NODE_ENV, expected one of ["development","test","production"]',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should read NAMESPACE', t => {
|
||||
t.deepEqual(
|
||||
['dev', 'beta', 'production'].map(envVal => {
|
||||
process.env.AFFINE_ENV = envVal;
|
||||
const env = new Env();
|
||||
return env.NAMESPACE;
|
||||
}),
|
||||
['dev', 'beta', 'production']
|
||||
);
|
||||
|
||||
t.throws(() => {
|
||||
process.env.AFFINE_ENV = 'unknown';
|
||||
new Env();
|
||||
});
|
||||
});
|
||||
|
||||
test('should read DEPLOYMENT_TYPE', t => {
|
||||
t.deepEqual(
|
||||
['affine', 'selfhosted'].map(envVal => {
|
||||
process.env.DEPLOYMENT_TYPE = envVal;
|
||||
const env = new Env();
|
||||
return env.DEPLOYMENT_TYPE;
|
||||
}),
|
||||
['affine', 'selfhosted']
|
||||
);
|
||||
|
||||
t.throws(() => {
|
||||
process.env.DEPLOYMENT_TYPE = 'unknown';
|
||||
new Env();
|
||||
});
|
||||
});
|
||||
|
||||
test('should read FLAVOR', t => {
|
||||
t.deepEqual(
|
||||
['allinone', 'graphql', 'sync', 'renderer', 'doc', 'script'].map(envVal => {
|
||||
process.env.SERVER_FLAVOR = envVal;
|
||||
const env = new Env();
|
||||
return env.FLAVOR;
|
||||
}),
|
||||
['allinone', 'graphql', 'sync', 'renderer', 'doc', 'script']
|
||||
);
|
||||
|
||||
t.throws(
|
||||
() => {
|
||||
process.env.SERVER_FLAVOR = 'unknown';
|
||||
new Env();
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Invalid value "unknown" for environment variable SERVER_FLAVOR, expected one of ["allinone","graphql","sync","renderer","doc","script"]',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should read platform', t => {
|
||||
t.deepEqual(
|
||||
['gcp', 'unknown'].map(envVal => {
|
||||
process.env.DEPLOYMENT_PLATFORM = envVal;
|
||||
const env = new Env();
|
||||
return env.platform;
|
||||
}),
|
||||
['gcp', 'unknown']
|
||||
);
|
||||
|
||||
t.notThrows(() => {
|
||||
process.env.PLATFORM = 'unknown';
|
||||
new Env();
|
||||
});
|
||||
});
|
||||
|
||||
test('should tell flavors correctly', t => {
|
||||
process.env.SERVER_FLAVOR = 'allinone';
|
||||
t.deepEqual(new Env().flavors, {
|
||||
graphql: true,
|
||||
sync: true,
|
||||
renderer: true,
|
||||
doc: true,
|
||||
script: true,
|
||||
});
|
||||
|
||||
process.env.SERVER_FLAVOR = 'graphql';
|
||||
t.deepEqual(new Env().flavors, {
|
||||
graphql: true,
|
||||
sync: false,
|
||||
renderer: false,
|
||||
doc: false,
|
||||
script: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('should tell selfhosted correctly', t => {
|
||||
process.env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
t.true(new Env().selfhosted);
|
||||
|
||||
process.env.DEPLOYMENT_TYPE = 'affine';
|
||||
t.false(new Env().selfhosted);
|
||||
});
|
||||
|
||||
test('should tell namespaces correctly', t => {
|
||||
process.env.AFFINE_ENV = 'dev';
|
||||
t.deepEqual(new Env().namespaces, {
|
||||
canary: true,
|
||||
beta: false,
|
||||
production: false,
|
||||
});
|
||||
|
||||
process.env.AFFINE_ENV = 'beta';
|
||||
t.deepEqual(new Env().namespaces, {
|
||||
canary: false,
|
||||
beta: true,
|
||||
production: false,
|
||||
});
|
||||
|
||||
process.env.AFFINE_ENV = 'production';
|
||||
t.deepEqual(new Env().namespaces, {
|
||||
canary: false,
|
||||
beta: false,
|
||||
production: true,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import {
|
||||
CopilotCapability,
|
||||
CopilotChatOptions,
|
||||
CopilotEmbeddingOptions,
|
||||
PromptMessage,
|
||||
} from '../../plugins/copilot/providers';
|
||||
import {
|
||||
DEFAULT_DIMENSIONS,
|
||||
OpenAIProvider,
|
||||
} from '../../plugins/copilot/providers/openai';
|
||||
import { sleep } from '../utils/utils';
|
||||
|
||||
export class MockCopilotProvider extends OpenAIProvider {
|
||||
override readonly models = [
|
||||
'test',
|
||||
'gpt-4o',
|
||||
'gpt-4o-2024-08-06',
|
||||
'fast-sdxl/image-to-image',
|
||||
'lcm-sd15-i2i',
|
||||
'clarity-upscaler',
|
||||
'imageutils/rembg',
|
||||
];
|
||||
|
||||
override readonly capabilities = [
|
||||
CopilotCapability.TextToText,
|
||||
CopilotCapability.TextToEmbedding,
|
||||
CopilotCapability.TextToImage,
|
||||
CopilotCapability.ImageToImage,
|
||||
CopilotCapability.ImageToText,
|
||||
];
|
||||
|
||||
// ====== text to text ======
|
||||
|
||||
override async generateText(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'test',
|
||||
options: CopilotChatOptions = {}
|
||||
): Promise<string> {
|
||||
this.checkParams({ messages, model, options });
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
return 'generate text to text';
|
||||
}
|
||||
|
||||
override async *generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'gpt-4o-mini',
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
this.checkParams({ messages, model, options });
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
const result = 'generate text to text stream';
|
||||
for (const message of result) {
|
||||
yield message;
|
||||
if (options.signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== text to embedding ======
|
||||
|
||||
override async generateEmbedding(
|
||||
messages: string | string[],
|
||||
model: string,
|
||||
options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS }
|
||||
): Promise<number[][]> {
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
this.checkParams({ embeddings: messages, model, options });
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)];
|
||||
}
|
||||
|
||||
// ====== text to image ======
|
||||
override async generateImages(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'test',
|
||||
_options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): Promise<Array<string>> {
|
||||
const { content: prompt } = messages[0] || {};
|
||||
if (!prompt) {
|
||||
throw new Error('Prompt is required');
|
||||
}
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
// just let test case can easily verify the final prompt
|
||||
return [`https://example.com/${model}.jpg`, prompt];
|
||||
}
|
||||
|
||||
override async *generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'dall-e-3',
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): AsyncIterable<string> {
|
||||
const ret = await this.generateImages(messages, model, options);
|
||||
for (const url of ret) {
|
||||
yield url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { EventBus } from '../../base';
|
||||
import { EventName } from '../../base/event/def';
|
||||
|
||||
export class MockEventBus {
|
||||
private readonly stub = Sinon.createStubInstance(EventBus);
|
||||
|
||||
emit = this.stub.emitAsync;
|
||||
emitAsync = this.stub.emitAsync;
|
||||
broadcast = this.stub.broadcast;
|
||||
|
||||
last<Event extends EventName>(
|
||||
name: Event
|
||||
): { name: Event; payload: Events[Event] } {
|
||||
const call = this.emitAsync
|
||||
.getCalls()
|
||||
.reverse()
|
||||
.find(call => call.args[0] === name);
|
||||
if (!call) {
|
||||
throw new Error(`Event ${name} never called`);
|
||||
}
|
||||
|
||||
// @ts-expect-error allow
|
||||
return {
|
||||
name,
|
||||
payload: call.args[1],
|
||||
};
|
||||
}
|
||||
|
||||
count(name: EventName) {
|
||||
return this.emitAsync.getCalls().filter(call => call.args[0] === name)
|
||||
.length;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ export * from './user.mock';
|
||||
export * from './workspace.mock';
|
||||
export * from './workspace-user.mock';
|
||||
|
||||
import { MockCopilotProvider } from './copilot.mock';
|
||||
import { MockDocMeta } from './doc-meta.mock';
|
||||
import { MockEventBus } from './eventbus.mock';
|
||||
import { MockMailer } from './mailer.mock';
|
||||
import { MockJobQueue } from './queue.mock';
|
||||
import { MockTeamWorkspace } from './team-workspace.mock';
|
||||
@@ -22,4 +24,4 @@ export const Mockers = {
|
||||
DocMeta: MockDocMeta,
|
||||
};
|
||||
|
||||
export { MockJobQueue, MockMailer };
|
||||
export { MockCopilotProvider, MockEventBus, MockJobQueue, MockMailer };
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { User } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
|
||||
import { ConfigModule } from '../../base/config';
|
||||
import { FeatureType, Models, UserFeatureModel, UserModel } from '../../models';
|
||||
import { createTestingModule, TestingModule } from '../utils';
|
||||
|
||||
@@ -126,13 +125,9 @@ test('should not switch user quota if the new quota is the same as the current o
|
||||
});
|
||||
|
||||
test('should use pro plan as free for selfhost instance', async t => {
|
||||
await using module = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isSelfhosted: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
// @ts-expect-error
|
||||
env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
await using module = await createTestingModule();
|
||||
|
||||
const models = module.get(Models);
|
||||
const u1 = await models.user.create({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import '../../plugins/config';
|
||||
|
||||
import { Controller, Get, HttpStatus, UseGuards } from '@nestjs/common';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
@@ -89,11 +87,13 @@ class NonThrottledController {
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
throttler: {
|
||||
default: {
|
||||
ttl: 60,
|
||||
limit: 120,
|
||||
ConfigModule.override({
|
||||
throttle: {
|
||||
throttlers: {
|
||||
default: {
|
||||
ttl: 60,
|
||||
limit: 120,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import '../../plugins/config';
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
@@ -30,14 +28,12 @@ const test = ava as TestFn<{
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
plugins: {
|
||||
oauth: {
|
||||
providers: {
|
||||
google: {
|
||||
clientId: 'google-client-id',
|
||||
clientSecret: 'google-client-secret',
|
||||
},
|
||||
ConfigModule.override({
|
||||
oauth: {
|
||||
providers: {
|
||||
google: {
|
||||
clientId: 'google-client-id',
|
||||
clientSecret: 'google-client-secret',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,12 +6,13 @@ import Sinon from 'sinon';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { AppModule } from '../../app.module';
|
||||
import { EventBus, Runtime } from '../../base';
|
||||
import { ConfigModule } from '../../base/config';
|
||||
import { EventBus } from '../../base';
|
||||
import { ConfigFactory, ConfigModule } from '../../base/config';
|
||||
import { CurrentUser } from '../../core/auth';
|
||||
import { AuthService } from '../../core/auth/service';
|
||||
import { EarlyAccessType, FeatureService } from '../../core/features';
|
||||
import { SubscriptionService } from '../../plugins/payment/service';
|
||||
import { StripeFactory } from '../../plugins/payment/stripe';
|
||||
import {
|
||||
CouponType,
|
||||
encodeLookupKey,
|
||||
@@ -159,7 +160,6 @@ const test = ava as TestFn<{
|
||||
service: SubscriptionService;
|
||||
event: Sinon.SinonStubbedInstance<EventBus>;
|
||||
feature: Sinon.SinonStubbedInstance<FeatureService>;
|
||||
runtime: Sinon.SinonStubbedInstance<Runtime>;
|
||||
stripe: {
|
||||
customers: Sinon.SinonStubbedInstance<Stripe.CustomersResource>;
|
||||
prices: Sinon.SinonStubbedInstance<Stripe.PricesResource>;
|
||||
@@ -184,16 +184,12 @@ function getLastCheckoutPrice(checkoutStub: Sinon.SinonStub) {
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
plugins: {
|
||||
payment: {
|
||||
stripe: {
|
||||
keys: {
|
||||
APIKey: '1',
|
||||
webhookKey: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
ConfigModule.override({
|
||||
payment: {
|
||||
enabled: true,
|
||||
showLifetimePrice: true,
|
||||
apiKey: '1',
|
||||
webhookKey: '1',
|
||||
},
|
||||
}),
|
||||
AppModule,
|
||||
@@ -203,18 +199,19 @@ test.before(async t => {
|
||||
Sinon.createStubInstance(FeatureService)
|
||||
);
|
||||
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
|
||||
m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime));
|
||||
},
|
||||
});
|
||||
|
||||
t.context.event = app.get(EventBus);
|
||||
t.context.service = app.get(SubscriptionService);
|
||||
t.context.feature = app.get(FeatureService);
|
||||
t.context.runtime = app.get(Runtime);
|
||||
t.context.db = app.get(PrismaClient);
|
||||
t.context.app = app;
|
||||
|
||||
const stripe = app.get(Stripe);
|
||||
const stripeFactory = app.get(StripeFactory);
|
||||
await stripeFactory.onConfigInit();
|
||||
|
||||
const stripe = stripeFactory.stripe;
|
||||
const stripeStubs = {
|
||||
customers: Sinon.stub(stripe.customers),
|
||||
prices: Sinon.stub(stripe.prices),
|
||||
@@ -234,6 +231,12 @@ test.beforeEach(async t => {
|
||||
await t.context.app.initTestingDB();
|
||||
t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1');
|
||||
|
||||
app.get(ConfigFactory).override({
|
||||
payment: {
|
||||
showLifetimePrice: true,
|
||||
},
|
||||
});
|
||||
|
||||
await db.workspace.create({
|
||||
data: {
|
||||
id: 'ws_1',
|
||||
@@ -249,11 +252,6 @@ test.beforeEach(async t => {
|
||||
|
||||
Sinon.reset();
|
||||
|
||||
// default stubs
|
||||
t.context.runtime.fetch
|
||||
.withArgs('plugins.payment/showLifetimePrice')
|
||||
.resolves(true);
|
||||
|
||||
// @ts-expect-error stub
|
||||
stripe.prices.list.callsFake((params: Stripe.PriceListParams) => {
|
||||
if (params.lookup_keys) {
|
||||
@@ -294,8 +292,13 @@ test('should list normal prices for authenticated user', async t => {
|
||||
});
|
||||
|
||||
test('should not show lifetime price if not enabled', async t => {
|
||||
const { service, runtime } = t.context;
|
||||
runtime.fetch.withArgs('plugins.payment/showLifetimePrice').resolves(false);
|
||||
const { service, app } = t.context;
|
||||
|
||||
app.get(ConfigFactory).override({
|
||||
payment: {
|
||||
showLifetimePrice: false,
|
||||
},
|
||||
});
|
||||
|
||||
const prices = await service.listPrices(t.context.u1);
|
||||
|
||||
@@ -539,8 +542,11 @@ test('should get correct pro plan price for checking out', async t => {
|
||||
// any user, lifetime recurring
|
||||
{
|
||||
feature.isEarlyAccessUser.resolves(false);
|
||||
const runtime = app.get(Runtime);
|
||||
await runtime.set('plugins.payment/showLifetimePrice', true);
|
||||
app.get(ConfigFactory).override({
|
||||
payment: {
|
||||
showLifetimePrice: true,
|
||||
},
|
||||
});
|
||||
|
||||
await service.checkout(
|
||||
{
|
||||
@@ -1181,8 +1187,12 @@ const onetimeYearlyInvoice: Stripe.Invoice = {
|
||||
};
|
||||
|
||||
test('should not be able to checkout for lifetime recurring if not enabled', async t => {
|
||||
const { service, u1, runtime } = t.context;
|
||||
runtime.fetch.withArgs('plugins.payment/showLifetimePrice').resolves(false);
|
||||
const { service, u1, app } = t.context;
|
||||
app.get(ConfigFactory).override({
|
||||
payment: {
|
||||
showLifetimePrice: false,
|
||||
},
|
||||
});
|
||||
|
||||
await t.throwsAsync(
|
||||
() =>
|
||||
@@ -1202,7 +1212,13 @@ test('should not be able to checkout for lifetime recurring if not enabled', asy
|
||||
});
|
||||
|
||||
test('should be able to checkout for lifetime recurring', async t => {
|
||||
const { service, u1, stripe } = t.context;
|
||||
const { service, u1, stripe, app } = t.context;
|
||||
|
||||
app.get(ConfigFactory).override({
|
||||
payment: {
|
||||
showLifetimePrice: true,
|
||||
},
|
||||
});
|
||||
|
||||
await service.checkout(
|
||||
{
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const gql = '/graphql';
|
||||
@@ -1,160 +1,11 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import {
|
||||
DEFAULT_DIMENSIONS,
|
||||
OpenAIProvider,
|
||||
} from '../../plugins/copilot/providers/openai';
|
||||
import {
|
||||
CopilotCapability,
|
||||
CopilotChatOptions,
|
||||
CopilotEmbeddingOptions,
|
||||
CopilotImageToImageProvider,
|
||||
CopilotImageToTextProvider,
|
||||
CopilotProviderType,
|
||||
CopilotTextToEmbeddingProvider,
|
||||
CopilotTextToImageProvider,
|
||||
CopilotTextToTextProvider,
|
||||
PromptConfig,
|
||||
PromptMessage,
|
||||
} from '../../plugins/copilot/types';
|
||||
import { PromptConfig, PromptMessage } from '../../plugins/copilot/providers';
|
||||
import { NodeExecutorType } from '../../plugins/copilot/workflow/executor';
|
||||
import {
|
||||
WorkflowGraph,
|
||||
WorkflowNodeType,
|
||||
WorkflowParams,
|
||||
} from '../../plugins/copilot/workflow/types';
|
||||
import { gql } from './common';
|
||||
import { TestingApp } from './testing-app';
|
||||
import { sleep } from './utils';
|
||||
|
||||
// @ts-expect-error no error
|
||||
export class MockCopilotTestProvider
|
||||
extends OpenAIProvider
|
||||
implements
|
||||
CopilotTextToTextProvider,
|
||||
CopilotTextToEmbeddingProvider,
|
||||
CopilotTextToImageProvider,
|
||||
CopilotImageToImageProvider,
|
||||
CopilotImageToTextProvider
|
||||
{
|
||||
static override readonly type = CopilotProviderType.Test;
|
||||
override readonly availableModels = [
|
||||
'test',
|
||||
'gpt-4o',
|
||||
'gpt-4o-2024-08-06',
|
||||
'fast-sdxl/image-to-image',
|
||||
'lcm-sd15-i2i',
|
||||
'clarity-upscaler',
|
||||
'imageutils/rembg',
|
||||
];
|
||||
static override readonly capabilities = [
|
||||
CopilotCapability.TextToText,
|
||||
CopilotCapability.TextToEmbedding,
|
||||
CopilotCapability.TextToImage,
|
||||
CopilotCapability.ImageToImage,
|
||||
CopilotCapability.ImageToText,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super({ apiKey: '1' });
|
||||
}
|
||||
|
||||
override getCapabilities(): CopilotCapability[] {
|
||||
return MockCopilotTestProvider.capabilities;
|
||||
}
|
||||
|
||||
static override assetsConfig(_config: any) {
|
||||
return true;
|
||||
}
|
||||
|
||||
override get type(): CopilotProviderType {
|
||||
return CopilotProviderType.Test;
|
||||
}
|
||||
|
||||
override async isModelAvailable(model: string): Promise<boolean> {
|
||||
return this.availableModels.includes(model);
|
||||
}
|
||||
|
||||
// ====== text to text ======
|
||||
|
||||
override async generateText(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'test',
|
||||
options: CopilotChatOptions = {}
|
||||
): Promise<string> {
|
||||
this.checkParams({ messages, model, options });
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
return 'generate text to text';
|
||||
}
|
||||
|
||||
override async *generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'gpt-4o-mini',
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
this.checkParams({ messages, model, options });
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
const result = 'generate text to text stream';
|
||||
for (const message of result) {
|
||||
yield message;
|
||||
if (options.signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====== text to embedding ======
|
||||
|
||||
override async generateEmbedding(
|
||||
messages: string | string[],
|
||||
model: string,
|
||||
options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS }
|
||||
): Promise<number[][]> {
|
||||
messages = Array.isArray(messages) ? messages : [messages];
|
||||
this.checkParams({ embeddings: messages, model, options });
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
return [Array.from(randomBytes(options.dimensions)).map(v => v % 128)];
|
||||
}
|
||||
|
||||
// ====== text to image ======
|
||||
override async generateImages(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'test',
|
||||
_options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): Promise<Array<string>> {
|
||||
const { content: prompt } = messages[0] || {};
|
||||
if (!prompt) {
|
||||
throw new Error('Prompt is required');
|
||||
}
|
||||
|
||||
// make some time gap for history test case
|
||||
await sleep(100);
|
||||
// just let test case can easily verify the final prompt
|
||||
return [`https://example.com/${model}.jpg`, prompt];
|
||||
}
|
||||
|
||||
override async *generateImagesStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'dall-e-3',
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
} = {}
|
||||
): AsyncIterable<string> {
|
||||
const ret = await this.generateImages(messages, model, options);
|
||||
for (const url of ret) {
|
||||
yield url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const cleanObject = (
|
||||
obj: any[] | undefined,
|
||||
@@ -342,7 +193,7 @@ export async function addContextFile(
|
||||
content: Buffer
|
||||
): Promise<{ id: string }> {
|
||||
const res = await app
|
||||
.POST(gql)
|
||||
.POST('/graphql')
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.field(
|
||||
'operations',
|
||||
|
||||
@@ -41,20 +41,17 @@ export async function createTestingApp(
|
||||
moduleDef: TestingAppMetadata = {}
|
||||
): Promise<TestingApp> {
|
||||
const module = await createTestingModule(moduleDef, false);
|
||||
const logger = new AFFiNELogger();
|
||||
logger.setLogLevels([TEST_LOG_LEVEL]);
|
||||
|
||||
const app = module.createNestApplication<NestExpressApplication>({
|
||||
cors: true,
|
||||
bodyParser: true,
|
||||
rawBody: true,
|
||||
logger,
|
||||
});
|
||||
|
||||
app.useBodyParser('raw', { limit: 1 * OneMB });
|
||||
|
||||
const logger = new AFFiNELogger();
|
||||
|
||||
logger.setLogLevels([TEST_LOG_LEVEL]);
|
||||
app.useLogger(logger);
|
||||
|
||||
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
} from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { AppModule, FunctionalityModules } from '../../app.module';
|
||||
import { AFFiNELogger, JobQueue, Runtime } from '../../base';
|
||||
import { buildAppModule, FunctionalityModules } from '../../app.module';
|
||||
import { AFFiNELogger, JobQueue } from '../../base';
|
||||
import { GqlModule } from '../../base/graphql';
|
||||
import { ServerConfigModule } from '../../core';
|
||||
import { AuthGuard, AuthModule } from '../../core/auth';
|
||||
import { Mailer, MailModule } from '../../core/mail';
|
||||
import { ModelsModule } from '../../models';
|
||||
@@ -63,16 +64,18 @@ export async function createTestingModule(
|
||||
autoInitialize = true
|
||||
): Promise<TestingModule> {
|
||||
// setting up
|
||||
let imports = moduleDef.imports ?? [AppModule];
|
||||
let imports = moduleDef.imports ?? [buildAppModule(globalThis.env)];
|
||||
imports =
|
||||
imports[0] === AppModule
|
||||
? [AppModule]
|
||||
// @ts-expect-error
|
||||
imports[0].module?.name === 'AppModule'
|
||||
? imports
|
||||
: dedupeModules([
|
||||
...FunctionalityModules,
|
||||
ModelsModule,
|
||||
AuthModule,
|
||||
GqlModule,
|
||||
MailModule,
|
||||
ServerConfigModule,
|
||||
...imports,
|
||||
]);
|
||||
|
||||
@@ -101,10 +104,6 @@ export async function createTestingModule(
|
||||
|
||||
testingModule.initTestingDB = async () => {
|
||||
await initTestingDB(module);
|
||||
|
||||
const runtime = module.get(Runtime);
|
||||
// by pass password min length validation
|
||||
await runtime.set('auth/password.min', 1);
|
||||
};
|
||||
|
||||
testingModule.create = createFactory(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { INestApplicationContext, LogLevel } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import whywhywhy from 'why-is-node-running';
|
||||
|
||||
import { RefreshFeatures0001 } from '../../data/migrations/0001-refresh-features';
|
||||
|
||||
@@ -32,3 +33,21 @@ export async function initTestingDB(context: INestApplicationContext) {
|
||||
export async function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function debugProcessHolding(ignorePrismaStack = true) {
|
||||
setImmediate(() => {
|
||||
whywhywhy({
|
||||
error: message => {
|
||||
// ignore prisma error
|
||||
if (
|
||||
ignorePrismaStack &&
|
||||
(message.includes('Prisma') || message.includes('prisma'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import test from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { AppModule } from '../app.module';
|
||||
import { Runtime, UseNamedGuard } from '../base';
|
||||
import { ConfigFactory, UseNamedGuard } from '../base';
|
||||
import { Public } from '../core/auth/guard';
|
||||
import { VersionService } from '../core/version/service';
|
||||
import { createTestingApp, TestingApp } from './utils';
|
||||
@@ -19,28 +19,28 @@ class GuardedController {
|
||||
}
|
||||
|
||||
let app: TestingApp;
|
||||
let runtime: Sinon.SinonStubbedInstance<Runtime>;
|
||||
let config: ConfigFactory;
|
||||
let version: VersionService;
|
||||
|
||||
function checkVersion(enabled = true) {
|
||||
runtime.fetch.withArgs('client/versionControl.enabled').resolves(enabled);
|
||||
|
||||
runtime.fetch
|
||||
.withArgs('client/versionControl.requiredVersion')
|
||||
.resolves('>=0.20.0');
|
||||
config.override({
|
||||
client: {
|
||||
versionControl: {
|
||||
enabled,
|
||||
requiredVersion: '>=0.20.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
app = await createTestingApp({
|
||||
imports: [AppModule],
|
||||
controllers: [GuardedController],
|
||||
tapModule: m => {
|
||||
m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime));
|
||||
},
|
||||
});
|
||||
|
||||
runtime = app.get(Runtime);
|
||||
version = app.get(VersionService, { strict: false });
|
||||
config = app.get(ConfigFactory, { strict: false });
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
@@ -74,9 +74,13 @@ test('should passthrough if version check is not enabled', async t => {
|
||||
});
|
||||
|
||||
test('should passthrough is version range is invalid', async t => {
|
||||
runtime.fetch
|
||||
.withArgs('client/versionControl.requiredVersion')
|
||||
.resolves('invalid');
|
||||
config.override({
|
||||
client: {
|
||||
versionControl: {
|
||||
requiredVersion: 'invalid',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let res = await app.GET('/guarded/test').set('x-affine-version', 'invalid');
|
||||
|
||||
@@ -92,9 +96,13 @@ test('should pass if client version is allowed', async t => {
|
||||
|
||||
t.is(res.status, 200);
|
||||
|
||||
runtime.fetch
|
||||
.withArgs('client/versionControl.requiredVersion')
|
||||
.resolves('>=0.19.0');
|
||||
config.override({
|
||||
client: {
|
||||
versionControl: {
|
||||
requiredVersion: '>=0.19.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res = await app.GET('/guarded/test').set('x-affine-version', '0.19.0');
|
||||
|
||||
@@ -120,9 +128,13 @@ test('should fail if client version is not set or invalid', async t => {
|
||||
});
|
||||
|
||||
test('should tell upgrade if client version is lower than allowed', async t => {
|
||||
runtime.fetch
|
||||
.withArgs('client/versionControl.requiredVersion')
|
||||
.resolves('>=0.21.0 <=0.22.0');
|
||||
config.override({
|
||||
client: {
|
||||
versionControl: {
|
||||
requiredVersion: '>=0.21.0 <=0.22.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0');
|
||||
|
||||
@@ -134,9 +146,13 @@ test('should tell upgrade if client version is lower than allowed', async t => {
|
||||
});
|
||||
|
||||
test('should tell downgrade if client version is higher than allowed', async t => {
|
||||
runtime.fetch
|
||||
.withArgs('client/versionControl.requiredVersion')
|
||||
.resolves('>=0.20.0 <=0.22.0');
|
||||
config.override({
|
||||
client: {
|
||||
versionControl: {
|
||||
requiredVersion: '>=0.20.0 <=0.22.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let res = await app.GET('/guarded/test').set('x-affine-version', '0.23.0');
|
||||
|
||||
@@ -148,9 +164,13 @@ test('should tell downgrade if client version is higher than allowed', async t =
|
||||
});
|
||||
|
||||
test('should test prerelease version', async t => {
|
||||
runtime.fetch
|
||||
.withArgs('client/versionControl.requiredVersion')
|
||||
.resolves('>=0.19.0');
|
||||
config.override({
|
||||
client: {
|
||||
versionControl: {
|
||||
requiredVersion: '>=0.19.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let res = await app
|
||||
.GET('/guarded/test')
|
||||
|
||||
@@ -3,7 +3,6 @@ import ava from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
import type { Response } from 'supertest';
|
||||
|
||||
import { WorkerModule } from '../plugins/worker';
|
||||
import { createTestingApp, TestingApp } from './utils';
|
||||
|
||||
type TestContext = {
|
||||
@@ -13,9 +12,9 @@ type TestContext = {
|
||||
const test = ava as TestFn<TestContext>;
|
||||
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [WorkerModule],
|
||||
});
|
||||
// @ts-expect-error test
|
||||
env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
const app = await createTestingApp();
|
||||
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user