Skip to content

Commit 157e5b9

Browse files
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 <[email protected]>
1 parent 9027406 commit 157e5b9

25 files changed

+654
-58
lines changed

.github/workflows/ci-server.yaml

+22
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@ on:
55
- main
66
pull_request_target:
77
jobs:
8+
postgres-job:
9+
runs-on: ubuntu-latest
10+
container: node:10.18-jessie
11+
steps:
12+
- run: echo "Postgres job finished"
13+
services:
14+
postgres:
15+
image: postgres
16+
env:
17+
POSTGRES_HOST: postgres
18+
POSTGRES_PASSWORD: postgrespassword
19+
POSTGRES_DB: test
20+
POSTGRES_PORT: 5432
21+
options: >-
22+
--health-cmd pg_isready
23+
--health-interval 10s
24+
--health-timeout 5s
25+
--health-retries 5
826
server-test:
27+
needs: postgres-job
928
runs-on: ubuntu-latest
1029
steps:
1130
- uses: actions/checkout@v3
@@ -27,3 +46,6 @@ jobs:
2746
- name: Server / Run jest tests
2847
run: |
2948
cd server && yarn test
49+
- name: Server / Run e2e tests
50+
run: |
51+
cd server && yarn test:e2e

front/src/generated/graphql.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,7 @@ export type QueryFindManyWorkspaceMemberArgs = {
16371637

16381638

16391639
export type QueryFindUniqueCompanyArgs = {
1640-
id: Scalars['String'];
1640+
where: CompanyWhereUniqueInput;
16411641
};
16421642

16431643

@@ -2205,7 +2205,7 @@ export type GetCompaniesQueryVariables = Exact<{
22052205
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 }> };
22062206

22072207
export type GetCompanyQueryVariables = Exact<{
2208-
id: Scalars['String'];
2208+
where: CompanyWhereUniqueInput;
22092209
}>;
22102210

22112211

@@ -3284,8 +3284,8 @@ export type GetCompaniesQueryHookResult = ReturnType<typeof useGetCompaniesQuery
32843284
export type GetCompaniesLazyQueryHookResult = ReturnType<typeof useGetCompaniesLazyQuery>;
32853285
export type GetCompaniesQueryResult = Apollo.QueryResult<GetCompaniesQuery, GetCompaniesQueryVariables>;
32863286
export const GetCompanyDocument = gql`
3287-
query GetCompany($id: String!) {
3288-
findUniqueCompany(id: $id) {
3287+
query GetCompany($where: CompanyWhereUniqueInput!) {
3288+
findUniqueCompany(where: $where) {
32893289
id
32903290
domainName
32913291
name
@@ -3316,7 +3316,7 @@ export const GetCompanyDocument = gql`
33163316
* @example
33173317
* const { data, loading, error } = useGetCompanyQuery({
33183318
* variables: {
3319-
* id: // value for 'id'
3319+
* where: // value for 'where'
33203320
* },
33213321
* });
33223322
*/

front/src/modules/companies/queries/show.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { gql } from '@apollo/client';
33
import { useGetCompanyQuery } from '~/generated/graphql';
44

55
export const GET_COMPANY = gql`
6-
query GetCompany($id: String!) {
7-
findUniqueCompany(id: $id) {
6+
query GetCompany($where: CompanyWhereUniqueInput!) {
7+
findUniqueCompany(where: $where) {
88
id
99
domainName
1010
name
@@ -24,5 +24,5 @@ export const GET_COMPANY = gql`
2424
`;
2525

2626
export function useCompanyQuery(id: string) {
27-
return useGetCompanyQuery({ variables: { id } });
27+
return useGetCompanyQuery({ variables: { where: { id } } });
2828
}

infra/dev/postgres/init.sql

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
-- Create the default database for development
12
CREATE DATABASE "default";
3+
4+
-- Create the tests database for e2e testing
5+
CREATE DATABASE "tests";

server/.env.test

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
DEBUG_MODE=true
2+
AUTH_GOOGLE_ENABLED=false
3+
ACCESS_TOKEN_SECRET=secret_jwt
4+
ACCESS_TOKEN_EXPIRES_IN=1d
5+
REFRESH_TOKEN_SECRET=secret_refresh_token
6+
REFRESH_TOKEN_EXPIRES_IN=30d
7+
LOGIN_TOKEN_SECRET=secret_login_token
8+
LOGIN_TOKEN_EXPIRES_IN=15m
9+
FRONT_AUTH_CALLBACK_URL=http://localhost:3001/auth/callback
10+
PG_DATABASE_URL=postgres://postgres:postgrespassword@localhost:5432/tests?connection_limit=1
11+
STORAGE_TYPE=local
12+
STORAGE_LOCAL_PATH=.local-storage

server/jest.config.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ module.exports = {
22
clearMocks: true,
33
preset: 'ts-jest',
44
testEnvironment: 'node',
5-
setupFilesAfterEnv: ['<rootDir>/src/database/client-mock/jest-prisma-singleton.ts'],
5+
setupFilesAfterEnv: [
6+
'<rootDir>/src/database/client-mock/jest-prisma-singleton.ts',
7+
],
68

79
moduleFileExtensions: ['js', 'json', 'ts'],
810
moduleNameMapper: {

server/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"test:watch": "jest --watch",
1919
"test:cov": "jest --coverage",
2020
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21-
"test:e2e": "jest --config ./test/jest-e2e.json",
21+
"test:e2e": "./scripts/run-integration.sh",
2222
"prisma:generate-client": "npx prisma generate --generator client && yarn prisma:generate-gql-select",
2323
"prisma:generate-gql-select": "node scripts/generate-model-select-map.js",
2424
"prisma:generate-nest-graphql": "npx prisma generate --generator nestgraphql",
@@ -57,7 +57,7 @@
5757
"class-validator": "^0.14.0",
5858
"date-fns": "^2.30.0",
5959
"file-type": "13.0.0",
60-
"graphql": "^16.6.0",
60+
"graphql": "^16.7.1",
6161
"graphql-type-json": "^0.3.2",
6262
"graphql-upload": "^13.0.0",
6363
"jest-mock-extended": "^3.0.4",

server/scripts/run-integration.sh

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
# src/run-integration.sh
3+
4+
DIR="$(cd "$(dirname "$0")" && pwd)"
5+
source $DIR/setenv.sh
6+
7+
npx ts-node ./test/utils/check-db.ts
8+
EXIT_CODE=$?
9+
10+
if [ $EXIT_CODE -ne 0 ]; then
11+
echo '🟡 - Database is not initialized. Running migrations...'
12+
npx prisma migrate reset --force && yarn prisma:generate
13+
else
14+
echo "🟢 - Database is already initialized."
15+
fi
16+
17+
yarn jest --config ./test/jest-e2e.json

server/scripts/setenv.sh

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
# scripts/setenv.sh
3+
4+
# Get script's directory
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
7+
# Construct the absolute path of .env file in the project root directory
8+
ENV_PATH="${SCRIPT_DIR}/../.env.test"
9+
10+
# Check if the file exists
11+
if [ -f "${ENV_PATH}" ]; then
12+
echo "🔵 - Loading environment variables from "${ENV_PATH}"..."
13+
# Export env vars
14+
export $(grep -v '^#' ${ENV_PATH} | xargs)
15+
else
16+
echo "Error: ${ENV_PATH} does not exist."
17+
exit 1
18+
fi

server/src/ability/ability.module.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from './handlers/workspace-member.ability-handler';
2727
import {
2828
ManageCompanyAbilityHandler,
29-
ReadCompanyAbilityHandler,
29+
ReadOneCompanyAbilityHandler,
3030
CreateCompanyAbilityHandler,
3131
UpdateCompanyAbilityHandler,
3232
DeleteCompanyAbilityHandler,
@@ -124,7 +124,7 @@ import {
124124
DeleteWorkspaceMemberAbilityHandler,
125125
// Company
126126
ManageCompanyAbilityHandler,
127-
ReadCompanyAbilityHandler,
127+
ReadOneCompanyAbilityHandler,
128128
CreateCompanyAbilityHandler,
129129
UpdateCompanyAbilityHandler,
130130
DeleteCompanyAbilityHandler,
@@ -208,7 +208,7 @@ import {
208208
DeleteWorkspaceMemberAbilityHandler,
209209
// Company
210210
ManageCompanyAbilityHandler,
211-
ReadCompanyAbilityHandler,
211+
ReadOneCompanyAbilityHandler,
212212
CreateCompanyAbilityHandler,
213213
UpdateCompanyAbilityHandler,
214214
DeleteCompanyAbilityHandler,

server/src/ability/ability.util.ts

+39
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,42 @@ export async function relationAbilityChecker(
205205

206206
return true;
207207
}
208+
209+
const isWhereInput = (input: any): boolean => {
210+
return Object.values(input).some((value) => typeof value === 'object');
211+
};
212+
213+
type ExcludeUnique<T> = T extends infer U
214+
? 'AND' extends keyof U
215+
? U
216+
: never
217+
: never;
218+
219+
/**
220+
* Convert a where unique input to a where input prisma
221+
* @param args Can be a where unique input or a where input
222+
* @returns whare input
223+
*/
224+
export const convertToWhereInput = <T>(
225+
where: T | undefined,
226+
): ExcludeUnique<T> | undefined => {
227+
const input = where as any;
228+
229+
if (!input) {
230+
return input;
231+
}
232+
233+
// If it's already a WhereInput, return it directly
234+
if (isWhereInput(input)) {
235+
return input;
236+
}
237+
238+
// If not convert it to a WhereInput
239+
const whereInput = {};
240+
241+
for (const key in input) {
242+
whereInput[key] = { equals: input[key] };
243+
}
244+
245+
return whereInput as ExcludeUnique<T>;
246+
};

server/src/ability/handlers/company.ability-handler.ts

+50-13
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ import { PrismaService } from 'src/database/prisma.service';
1313
import { AbilityAction } from 'src/ability/ability.action';
1414
import { AppAbility } from 'src/ability/ability.factory';
1515
import { CompanyWhereInput } from 'src/core/@generated/company/company-where.input';
16-
import { relationAbilityChecker } from 'src/ability/ability.util';
16+
import { CompanyWhereUniqueInput } from 'src/core/@generated/company/company-where-unique.input';
17+
import {
18+
convertToWhereInput,
19+
relationAbilityChecker,
20+
} from 'src/ability/ability.util';
1721
import { assert } from 'src/utils/assert';
1822

1923
class CompanyArgs {
20-
where?: CompanyWhereInput;
24+
where?: CompanyWhereUniqueInput | CompanyWhereInput;
2125
[key: string]: any;
2226
}
2327

@@ -29,9 +33,18 @@ export class ManageCompanyAbilityHandler implements IAbilityHandler {
2933
}
3034

3135
@Injectable()
32-
export class ReadCompanyAbilityHandler implements IAbilityHandler {
33-
handle(ability: AppAbility) {
34-
return ability.can(AbilityAction.Read, 'Company');
36+
export class ReadOneCompanyAbilityHandler implements IAbilityHandler {
37+
constructor(private readonly prismaService: PrismaService) {}
38+
39+
async handle(ability: AppAbility, context: ExecutionContext) {
40+
const gqlContext = GqlExecutionContext.create(context);
41+
const args = gqlContext.getArgs<CompanyArgs>();
42+
const company = await this.prismaService.client.company.findFirst({
43+
where: args.where,
44+
});
45+
assert(company, '', NotFoundException);
46+
47+
return ability.can(AbilityAction.Read, subject('Company', company));
3548
}
3649
}
3750

@@ -65,10 +78,11 @@ export class UpdateCompanyAbilityHandler implements IAbilityHandler {
6578
async handle(ability: AppAbility, context: ExecutionContext) {
6679
const gqlContext = GqlExecutionContext.create(context);
6780
const args = gqlContext.getArgs<CompanyArgs>();
68-
const company = await this.prismaService.client.company.findFirst({
69-
where: args.where,
81+
const where = convertToWhereInput(args.where);
82+
const companies = await this.prismaService.client.company.findMany({
83+
where,
7084
});
71-
assert(company, '', NotFoundException);
85+
assert(companies.length, '', NotFoundException);
7286

7387
const allowed = await relationAbilityChecker(
7488
'Company',
@@ -81,7 +95,18 @@ export class UpdateCompanyAbilityHandler implements IAbilityHandler {
8195
return false;
8296
}
8397

84-
return ability.can(AbilityAction.Update, subject('Company', company));
98+
for (const company of companies) {
99+
const allowed = ability.can(
100+
AbilityAction.Delete,
101+
subject('Company', company),
102+
);
103+
104+
if (!allowed) {
105+
return false;
106+
}
107+
}
108+
109+
return true;
85110
}
86111
}
87112

@@ -92,11 +117,23 @@ export class DeleteCompanyAbilityHandler implements IAbilityHandler {
92117
async handle(ability: AppAbility, context: ExecutionContext) {
93118
const gqlContext = GqlExecutionContext.create(context);
94119
const args = gqlContext.getArgs<CompanyArgs>();
95-
const company = await this.prismaService.client.company.findFirst({
96-
where: args.where,
120+
const where = convertToWhereInput(args.where);
121+
const companies = await this.prismaService.client.company.findMany({
122+
where,
97123
});
98-
assert(company, '', NotFoundException);
124+
assert(companies.length, '', NotFoundException);
125+
126+
for (const company of companies) {
127+
const allowed = ability.can(
128+
AbilityAction.Delete,
129+
subject('Company', company),
130+
);
131+
132+
if (!allowed) {
133+
return false;
134+
}
135+
}
99136

100-
return ability.can(AbilityAction.Delete, subject('Company', company));
137+
return true;
101138
}
102139
}

server/src/core/company/company.resolver.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
2121
import {
2222
CreateCompanyAbilityHandler,
2323
DeleteCompanyAbilityHandler,
24-
ReadCompanyAbilityHandler,
24+
ReadOneCompanyAbilityHandler,
2525
UpdateCompanyAbilityHandler,
2626
} from 'src/ability/handlers/company.ability-handler';
2727
import { UserAbility } from 'src/decorators/user-ability.decorator';
2828
import { AppAbility } from 'src/ability/ability.factory';
29+
import { FindUniqueCompanyArgs } from 'src/core/@generated/company/find-unique-company.args';
2930

3031
import { CompanyService } from './company.service';
3132

@@ -36,7 +37,6 @@ export class CompanyResolver {
3637

3738
@Query(() => [Company])
3839
@UseGuards(AbilityGuard)
39-
@CheckAbilities(ReadCompanyAbilityHandler)
4040
async findManyCompany(
4141
@Args() args: FindManyCompanyArgs,
4242
@UserAbility() ability: AppAbility,
@@ -60,19 +60,18 @@ export class CompanyResolver {
6060

6161
@Query(() => Company)
6262
@UseGuards(AbilityGuard)
63-
@CheckAbilities(ReadCompanyAbilityHandler)
63+
@CheckAbilities(ReadOneCompanyAbilityHandler)
6464
async findUniqueCompany(
65-
@Args('id') id: string,
66-
@UserAbility() ability: AppAbility,
65+
@Args() args: FindUniqueCompanyArgs,
6766
@PrismaSelector({ modelName: 'Company' })
6867
prismaSelect: PrismaSelect<'Company'>,
6968
): Promise<Partial<Company>> {
70-
return this.companyService.findUniqueOrThrow({
71-
where: {
72-
id: id,
73-
},
69+
const company = this.companyService.findUniqueOrThrow({
70+
where: args.where,
7471
select: prismaSelect.value,
7572
});
73+
74+
return company;
7675
}
7776

7877
@Mutation(() => Company, {

0 commit comments

Comments
 (0)