feat: implement e2e test for CompanyResolver (#944)

* feat: wip e2e server test

* feat: use github action postgres & use infra for local

* feat: company e2e test

* feat: add company e2e test for permissions

* Simplify server e2e test run

* Fix lint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2023-07-27 18:48:40 +02:00
committed by GitHub
parent 9027406fdf
commit 157e5b9a2e
25 changed files with 655 additions and 59 deletions

View File

@ -5,7 +5,26 @@ on:
- main
pull_request_target:
jobs:
postgres-job:
runs-on: ubuntu-latest
container: node:10.18-jessie
steps:
- run: echo "Postgres job finished"
services:
postgres:
image: postgres
env:
POSTGRES_HOST: postgres
POSTGRES_PASSWORD: postgrespassword
POSTGRES_DB: test
POSTGRES_PORT: 5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
server-test:
needs: postgres-job
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -27,3 +46,6 @@ jobs:
- name: Server / Run jest tests
run: |
cd server && yarn test
- name: Server / Run e2e tests
run: |
cd server && yarn test:e2e

View File

@ -1637,7 +1637,7 @@ export type QueryFindManyWorkspaceMemberArgs = {
export type QueryFindUniqueCompanyArgs = {
id: Scalars['String'];
where: CompanyWhereUniqueInput;
};
@ -2205,7 +2205,7 @@ export type GetCompaniesQueryVariables = Exact<{
export type GetCompaniesQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, linkedinUrl?: string | null, employees?: number | null, _commentThreadCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } | null }> };
export type GetCompanyQueryVariables = Exact<{
id: Scalars['String'];
where: CompanyWhereUniqueInput;
}>;
@ -3284,8 +3284,8 @@ export type GetCompaniesQueryHookResult = ReturnType<typeof useGetCompaniesQuery
export type GetCompaniesLazyQueryHookResult = ReturnType<typeof useGetCompaniesLazyQuery>;
export type GetCompaniesQueryResult = Apollo.QueryResult<GetCompaniesQuery, GetCompaniesQueryVariables>;
export const GetCompanyDocument = gql`
query GetCompany($id: String!) {
findUniqueCompany(id: $id) {
query GetCompany($where: CompanyWhereUniqueInput!) {
findUniqueCompany(where: $where) {
id
domainName
name
@ -3316,7 +3316,7 @@ export const GetCompanyDocument = gql`
* @example
* const { data, loading, error } = useGetCompanyQuery({
* variables: {
* id: // value for 'id'
* where: // value for 'where'
* },
* });
*/

View File

@ -3,8 +3,8 @@ import { gql } from '@apollo/client';
import { useGetCompanyQuery } from '~/generated/graphql';
export const GET_COMPANY = gql`
query GetCompany($id: String!) {
findUniqueCompany(id: $id) {
query GetCompany($where: CompanyWhereUniqueInput!) {
findUniqueCompany(where: $where) {
id
domainName
name
@ -24,5 +24,5 @@ export const GET_COMPANY = gql`
`;
export function useCompanyQuery(id: string) {
return useGetCompanyQuery({ variables: { id } });
return useGetCompanyQuery({ variables: { where: { id } } });
}

View File

@ -1 +1,5 @@
-- Create the default database for development
CREATE DATABASE "default";
-- Create the tests database for e2e testing
CREATE DATABASE "tests";

12
server/.env.test Normal file
View File

@ -0,0 +1,12 @@
DEBUG_MODE=true
AUTH_GOOGLE_ENABLED=false
ACCESS_TOKEN_SECRET=secret_jwt
ACCESS_TOKEN_EXPIRES_IN=1d
REFRESH_TOKEN_SECRET=secret_refresh_token
REFRESH_TOKEN_EXPIRES_IN=30d
LOGIN_TOKEN_SECRET=secret_login_token
LOGIN_TOKEN_EXPIRES_IN=15m
FRONT_AUTH_CALLBACK_URL=http://localhost:3001/auth/callback
PG_DATABASE_URL=postgres://postgres:postgrespassword@localhost:5432/tests?connection_limit=1
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=.local-storage

View File

@ -2,7 +2,9 @@ module.exports = {
clearMocks: true,
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/src/database/client-mock/jest-prisma-singleton.ts'],
setupFilesAfterEnv: [
'<rootDir>/src/database/client-mock/jest-prisma-singleton.ts',
],
moduleFileExtensions: ['js', 'json', 'ts'],
moduleNameMapper: {

View File

@ -18,7 +18,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e": "./scripts/run-integration.sh",
"prisma:generate-client": "npx prisma generate --generator client && yarn prisma:generate-gql-select",
"prisma:generate-gql-select": "node scripts/generate-model-select-map.js",
"prisma:generate-nest-graphql": "npx prisma generate --generator nestgraphql",
@ -57,7 +57,7 @@
"class-validator": "^0.14.0",
"date-fns": "^2.30.0",
"file-type": "13.0.0",
"graphql": "^16.6.0",
"graphql": "^16.7.1",
"graphql-type-json": "^0.3.2",
"graphql-upload": "^13.0.0",
"jest-mock-extended": "^3.0.4",

View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# src/run-integration.sh
DIR="$(cd "$(dirname "$0")" && pwd)"
source $DIR/setenv.sh
npx ts-node ./test/utils/check-db.ts
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo '🟡 - Database is not initialized. Running migrations...'
npx prisma migrate reset --force && yarn prisma:generate
else
echo "🟢 - Database is already initialized."
fi
yarn jest --config ./test/jest-e2e.json

18
server/scripts/setenv.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# scripts/setenv.sh
# Get script's directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Construct the absolute path of .env file in the project root directory
ENV_PATH="${SCRIPT_DIR}/../.env.test"
# Check if the file exists
if [ -f "${ENV_PATH}" ]; then
echo "🔵 - Loading environment variables from "${ENV_PATH}"..."
# Export env vars
export $(grep -v '^#' ${ENV_PATH} | xargs)
else
echo "Error: ${ENV_PATH} does not exist."
exit 1
fi

View File

@ -26,7 +26,7 @@ import {
} from './handlers/workspace-member.ability-handler';
import {
ManageCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
CreateCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
@ -124,7 +124,7 @@ import {
DeleteWorkspaceMemberAbilityHandler,
// Company
ManageCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
CreateCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
@ -208,7 +208,7 @@ import {
DeleteWorkspaceMemberAbilityHandler,
// Company
ManageCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
CreateCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,

View File

@ -205,3 +205,42 @@ export async function relationAbilityChecker(
return true;
}
const isWhereInput = (input: any): boolean => {
return Object.values(input).some((value) => typeof value === 'object');
};
type ExcludeUnique<T> = T extends infer U
? 'AND' extends keyof U
? U
: never
: never;
/**
* Convert a where unique input to a where input prisma
* @param args Can be a where unique input or a where input
* @returns whare input
*/
export const convertToWhereInput = <T>(
where: T | undefined,
): ExcludeUnique<T> | undefined => {
const input = where as any;
if (!input) {
return input;
}
// If it's already a WhereInput, return it directly
if (isWhereInput(input)) {
return input;
}
// If not convert it to a WhereInput
const whereInput = {};
for (const key in input) {
whereInput[key] = { equals: input[key] };
}
return whereInput as ExcludeUnique<T>;
};

View File

@ -13,11 +13,15 @@ import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory';
import { CompanyWhereInput } from 'src/core/@generated/company/company-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { CompanyWhereUniqueInput } from 'src/core/@generated/company/company-where-unique.input';
import {
convertToWhereInput,
relationAbilityChecker,
} from 'src/ability/ability.util';
import { assert } from 'src/utils/assert';
class CompanyArgs {
where?: CompanyWhereInput;
where?: CompanyWhereUniqueInput | CompanyWhereInput;
[key: string]: any;
}
@ -29,9 +33,18 @@ export class ManageCompanyAbilityHandler implements IAbilityHandler {
}
@Injectable()
export class ReadCompanyAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'Company');
export class ReadOneCompanyAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CompanyArgs>();
const company = await this.prismaService.client.company.findFirst({
where: args.where,
});
assert(company, '', NotFoundException);
return ability.can(AbilityAction.Read, subject('Company', company));
}
}
@ -65,10 +78,11 @@ export class UpdateCompanyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CompanyArgs>();
const company = await this.prismaService.client.company.findFirst({
where: args.where,
const where = convertToWhereInput(args.where);
const companies = await this.prismaService.client.company.findMany({
where,
});
assert(company, '', NotFoundException);
assert(companies.length, '', NotFoundException);
const allowed = await relationAbilityChecker(
'Company',
@ -81,7 +95,18 @@ export class UpdateCompanyAbilityHandler implements IAbilityHandler {
return false;
}
return ability.can(AbilityAction.Update, subject('Company', company));
for (const company of companies) {
const allowed = ability.can(
AbilityAction.Delete,
subject('Company', company),
);
if (!allowed) {
return false;
}
}
return true;
}
}
@ -92,11 +117,23 @@ export class DeleteCompanyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CompanyArgs>();
const company = await this.prismaService.client.company.findFirst({
where: args.where,
const where = convertToWhereInput(args.where);
const companies = await this.prismaService.client.company.findMany({
where,
});
assert(company, '', NotFoundException);
assert(companies.length, '', NotFoundException);
return ability.can(AbilityAction.Delete, subject('Company', company));
for (const company of companies) {
const allowed = ability.can(
AbilityAction.Delete,
subject('Company', company),
);
if (!allowed) {
return false;
}
}
return true;
}
}

View File

@ -21,11 +21,12 @@ import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
} from 'src/ability/handlers/company.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { FindUniqueCompanyArgs } from 'src/core/@generated/company/find-unique-company.args';
import { CompanyService } from './company.service';
@ -36,7 +37,6 @@ export class CompanyResolver {
@Query(() => [Company])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadCompanyAbilityHandler)
async findManyCompany(
@Args() args: FindManyCompanyArgs,
@UserAbility() ability: AppAbility,
@ -60,19 +60,18 @@ export class CompanyResolver {
@Query(() => Company)
@UseGuards(AbilityGuard)
@CheckAbilities(ReadCompanyAbilityHandler)
@CheckAbilities(ReadOneCompanyAbilityHandler)
async findUniqueCompany(
@Args('id') id: string,
@UserAbility() ability: AppAbility,
@Args() args: FindUniqueCompanyArgs,
@PrismaSelector({ modelName: 'Company' })
prismaSelect: PrismaSelect<'Company'>,
): Promise<Partial<Company>> {
return this.companyService.findUniqueOrThrow({
where: {
id: id,
},
const company = this.companyService.findUniqueOrThrow({
where: args.where,
select: prismaSelect.value,
});
return company;
}
@Mutation(() => Company, {

View File

@ -1,9 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ViewFieldService } from 'src/core/view/services/view-field.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { ViewFieldResolver } from './view-field.resolver';
import { AbilityFactory } from 'src/ability/ability.factory';
describe('ViewFieldResolver', () => {
let resolver: ViewFieldResolver;

View File

@ -2,6 +2,7 @@ import {
INestApplication,
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
@ -20,7 +21,7 @@ const createPrismaClient = (options: Prisma.PrismaClientOptions) => {
type ExtendedPrismaClient = ReturnType<typeof createPrismaClient>;
@Injectable()
export class PrismaService implements OnModuleInit {
export class PrismaService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
private prismaClient!: ExtendedPrismaClient;
@ -61,6 +62,10 @@ export class PrismaService implements OnModuleInit {
await this.prismaClient.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.prismaClient.$disconnect();
}
async enableShutdownHooks(app: INestApplication) {
this.prismaClient.$on('beforeExit', async () => {
await app.close();

View File

@ -1,24 +1,31 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import request from 'supertest';
import { createApp } from './utils/create-app';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
[app] = await createApp();
});
it('/ (GET)', () => {
afterEach(async () => {
await app.close();
});
it('/healthz (GET)', () => {
return request(app.getHttpServer())
.get('/')
.get('/healthz')
.expect(200)
.expect('Hello World!');
.expect((response) => {
expect(response.body).toEqual({
status: 'ok',
info: { database: { status: 'up' } },
error: {},
details: { database: { status: 'up' } },
});
});
});
});

View File

@ -0,0 +1,298 @@
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { createApp } from './utils/create-app';
describe('CompanyResolver (e2e)', () => {
let app: INestApplication;
let companyId: string | undefined;
const authGuardMock = { canActivate: (): any => true };
beforeEach(async () => {
[app] = await createApp({
moduleBuilderHook: (moduleBuilder) =>
moduleBuilder.overrideGuard(JwtAuthGuard).useValue(authGuardMock),
});
});
afterEach(async () => {
await app.close();
});
it('should create a company', () => {
const queryData = {
query: `
mutation CreateOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) {
id
name
domainName
address
}
}
`,
variables: {
data: {
name: 'New Company',
domainName: 'new-company.com',
address: 'New Address',
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.createOneCompany;
companyId = data.id;
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name', 'New Company');
expect(data).toHaveProperty('domainName', 'new-company.com');
expect(data).toHaveProperty('address', 'New Address');
});
});
it('should find many companies', () => {
const queryData = {
query: `
query FindManyCompany {
findManyCompany {
id
name
domainName
address
}
}
`,
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.findManyCompany;
expect(data).toBeDefined();
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBeGreaterThan(0);
const company = data.find((c) => c.id === companyId);
expect(company).toBeDefined();
expect(company).toHaveProperty('id');
expect(company).toHaveProperty('name', 'New Company');
expect(company).toHaveProperty('domainName', 'new-company.com');
expect(company).toHaveProperty('address', 'New Address');
// Check if we have access to ressources outside of our workspace
const instagramCompany = data.find((c) => c.name === 'Instagram');
expect(instagramCompany).toBeUndefined();
});
});
it('should find unique company', () => {
const queryData = {
query: `
query FindUniqueCompany($where: CompanyWhereUniqueInput!) {
findUniqueCompany(where: $where) {
id
name
domainName
address
}
}
`,
variables: {
where: {
id: companyId,
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.findUniqueCompany;
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name', 'New Company');
expect(data).toHaveProperty('domainName', 'new-company.com');
expect(data).toHaveProperty('address', 'New Address');
});
});
it('should not find unique company (forbidden because outside workspace)', () => {
const queryData = {
query: `
query FindUniqueCompany($where: CompanyWhereUniqueInput!) {
findUniqueCompany(where: $where) {
id
name
domainName
address
}
}
`,
variables: {
where: {
id: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e',
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const errors = res.body.errors;
const error = errors?.[0];
expect(error).toBeDefined();
expect(error.extensions.code).toBe('FORBIDDEN');
expect(error.extensions.originalError.statusCode).toBe(403);
});
});
it('should update a company', () => {
const queryData = {
query: `
mutation UpdateOneCompany($where: CompanyWhereUniqueInput!, $data: CompanyUpdateInput!) {
updateOneCompany(data: $data, where: $where) {
id
name
domainName
address
}
}
`,
variables: {
where: {
id: companyId,
},
data: {
name: 'Updated Company',
domainName: 'updated-company.com',
address: 'Updated Address',
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.updateOneCompany;
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name', 'Updated Company');
expect(data).toHaveProperty('domainName', 'updated-company.com');
expect(data).toHaveProperty('address', 'Updated Address');
});
});
it('should not update a company (forbidden because outside workspace)', () => {
const queryData = {
query: `
mutation UpdateOneCompany($where: CompanyWhereUniqueInput!, $data: CompanyUpdateInput!) {
updateOneCompany(data: $data, where: $where) {
id
name
domainName
address
}
}
`,
variables: {
where: {
id: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e',
},
data: {
name: 'Updated Instagram',
},
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const errors = res.body.errors;
const error = errors?.[0];
expect(error).toBeDefined();
expect(error.extensions.code).toBe('FORBIDDEN');
expect(error.extensions.originalError.statusCode).toBe(403);
});
});
it('should delete a company', () => {
const queryData = {
query: `
mutation DeleteManyCompany($ids: [String!]) {
deleteManyCompany(where: {id: {in: $ids}}) {
count
}
}
`,
variables: {
ids: [companyId],
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const data = res.body.data.deleteManyCompany;
companyId = undefined;
expect(data).toBeDefined();
expect(data).toHaveProperty('count', 1);
});
});
it('should not delete a company (forbidden because outside workspace)', () => {
const queryData = {
query: `
mutation DeleteManyCompany($ids: [String!]) {
deleteManyCompany(where: {id: {in: $ids}}) {
count
}
}
`,
variables: {
ids: ['twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e'],
},
};
return request(app.getHttpServer())
.post('/graphql')
.send(queryData)
.expect(200)
.expect((res) => {
const errors = res.body.errors;
const error = errors?.[0];
expect(error).toBeDefined();
expect(error.extensions.code).toBe('FORBIDDEN');
expect(error.extensions.originalError.statusCode).toBe(403);
});
});
});

View File

@ -3,6 +3,11 @@
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"setupFilesAfterEnv": ["<rootDir>/utils/setup-tests.ts"],
"moduleNameMapper": {
"^src/(.*)": "<rootDir>/../src/$1",
"^test/(.*)": "<rootDir>/$1"
},
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}

View File

@ -0,0 +1,9 @@
{
"id": "twenty-ge256b39-3ec3-4fe3-8997-b76aa0bfc102",
"firstName": "Tim",
"lastName": "Apple",
"email": "tim@apple.dev",
"locale": "en",
"passwordHash": "$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6",
"avatarUrl": null
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
// check-db.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const schemaDatabaseExists = async (databaseName: string) => {
try {
const result = await prisma.$queryRawUnsafe<[any]>(
`SELECT 1 FROM pg_database WHERE datname = '${databaseName}';`,
);
return result.length > 0;
} catch {
return false;
}
};
async function main() {
const databaseName = 'tests';
// Check if schema exists
const databaseExistsResult = await schemaDatabaseExists(databaseName);
if (!databaseExistsResult) {
throw new Error(`Schema ${databaseName} does not exist`);
}
// Check if database is initialized
await prisma.$queryRaw`SELECT 1 FROM pg_tables WHERE tablename='_prisma_migrations';`;
}
main()
.then(() => {
process.exit(0);
})
.catch(() => {
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -0,0 +1,56 @@
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing';
import mockUser from 'test/mock-data/user.json';
import mockWorkspace from 'test/mock-data/workspace.json';
import { RequestHandler } from 'express';
import { AppModule } from 'src/app.module';
interface TestingModuleCreatePreHook {
(moduleBuilder: TestingModuleBuilder): TestingModuleBuilder;
}
/**
* Hook for adding items to nest application
*/
export type TestingAppCreatePreHook = (
app: NestExpressApplication,
) => Promise<void>;
/**
* Sets basic e2e testing module of app
*/
export async function createApp(
config: {
moduleBuilderHook?: TestingModuleCreatePreHook;
appInitHook?: TestingAppCreatePreHook;
} = {},
): Promise<[NestExpressApplication, TestingModule]> {
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
imports: [AppModule],
});
if (!!config.moduleBuilderHook) {
moduleBuilder = config.moduleBuilderHook(moduleBuilder);
}
const moduleFixture: TestingModule = await moduleBuilder.compile();
const app = moduleFixture.createNestApplication<NestExpressApplication>();
if (config.appInitHook) {
await config.appInitHook(app);
}
const mockAuthHandler: RequestHandler = (req, _res, next) => {
req.user = {
user: mockUser,
workspace: mockWorkspace,
};
next();
};
app.use(mockAuthHandler);
return [await app.init(), moduleFixture];
}

View File

@ -0,0 +1,18 @@
import { PrismaClient, Prisma } from '@prisma/client';
import { camelCase } from 'src/utils/camel-case';
const prisma = new PrismaClient();
export default async () => {
const models = Prisma.dmmf.datamodel.models;
const modelNames = models.map((model) => model.name);
const entities = modelNames.map((modelName) => camelCase(modelName));
await prisma.$transaction(
entities.map((entity) => {
console.log('entity: ', entity);
return prisma[entity].deleteMany();
}),
);
};

View File

@ -0,0 +1,5 @@
import resetDb from './reset-db';
global.beforeEach(() => {
// resetDb();
});

View File

@ -1711,9 +1711,9 @@
integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==
"@mapbox/node-pre-gyp@^1.0.10":
version "1.0.10"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==
version "1.0.11"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==
dependencies:
detect-libc "^2.0.0"
https-proxy-agent "^5.0.0"
@ -5532,16 +5532,11 @@ graphql-ws@5.13.1:
resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.13.1.tgz"
integrity sha512-eiX7ES/ZQr0q7hSM5UBOEIFfaAUmAY9/CSDyAnsETuybByU7l/v46drRg9DQoTvVABEHp3QnrvwgTRMhqy7zxQ==
"graphql@0.13.1 - 16":
"graphql@0.13.1 - 16", graphql@^16.7.1:
version "16.7.1"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
graphql@^16.6.0:
version "16.6.0"
resolved "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz"
integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"