Skip to content

Commit

Permalink
feat: implement e2e test for CompanyResolver (#944)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
magrinj and charlesBochet authored Jul 27, 2023
1 parent 9027406 commit 157e5b9
Show file tree
Hide file tree
Showing 25 changed files with 654 additions and 58 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
10 changes: 5 additions & 5 deletions front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1637,7 +1637,7 @@ export type QueryFindManyWorkspaceMemberArgs = {


export type QueryFindUniqueCompanyArgs = {
id: Scalars['String'];
where: CompanyWhereUniqueInput;
};


Expand Down Expand Up @@ -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;
}>;


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3316,7 +3316,7 @@ export const GetCompanyDocument = gql`
* @example
* const { data, loading, error } = useGetCompanyQuery({
* variables: {
* id: // value for 'id'
* where: // value for 'where'
* },
* });
*/
Expand Down
6 changes: 3 additions & 3 deletions front/src/modules/companies/queries/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,5 +24,5 @@ export const GET_COMPANY = gql`
`;

export function useCompanyQuery(id: string) {
return useGetCompanyQuery({ variables: { id } });
return useGetCompanyQuery({ variables: { where: { id } } });
}
4 changes: 4 additions & 0 deletions infra/dev/postgres/init.sql
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
-- Create the default database for development
CREATE DATABASE "default";

-- Create the tests database for e2e testing
CREATE DATABASE "tests";
12 changes: 12 additions & 0 deletions server/.env.test
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion server/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions server/scripts/run-integration.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions server/scripts/setenv.sh
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions server/src/ability/ability.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from './handlers/workspace-member.ability-handler';
import {
ManageCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
CreateCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
Expand Down Expand Up @@ -124,7 +124,7 @@ import {
DeleteWorkspaceMemberAbilityHandler,
// Company
ManageCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
CreateCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
Expand Down Expand Up @@ -208,7 +208,7 @@ import {
DeleteWorkspaceMemberAbilityHandler,
// Company
ManageCompanyAbilityHandler,
ReadCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
CreateCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
Expand Down
39 changes: 39 additions & 0 deletions server/src/ability/ability.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
};
63 changes: 50 additions & 13 deletions server/src/ability/handlers/company.ability-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

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

Expand Down Expand Up @@ -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',
Expand All @@ -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;
}
}

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

for (const company of companies) {
const allowed = ability.can(
AbilityAction.Delete,
subject('Company', company),
);

if (!allowed) {
return false;
}
}

return ability.can(AbilityAction.Delete, subject('Company', company));
return true;
}
}
17 changes: 8 additions & 9 deletions server/src/core/company/company.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

@Query(() => [Company])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadCompanyAbilityHandler)
async findManyCompany(
@Args() args: FindManyCompanyArgs,
@UserAbility() ability: AppAbility,
Expand All @@ -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, {
Expand Down
Loading

0 comments on commit 157e5b9

Please sign in to comment.