diff --git a/apps/user-service/src/auth/auth.service.spec.ts b/apps/user-service/src/auth/auth.service.spec.ts index b139f4b..37db931 100644 --- a/apps/user-service/src/auth/auth.service.spec.ts +++ b/apps/user-service/src/auth/auth.service.spec.ts @@ -2,42 +2,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { JwtService } from '@nestjs/jwt'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts'; import bcrypt from 'bcryptjs'; import { User } from '@terramatch-microservices/database/entities'; import { UserFactory } from '@terramatch-microservices/database/factories'; -import { Sequelize } from 'sequelize-typescript'; -import * as Entities from '@terramatch-microservices/database/entities'; describe('AuthService', () => { let service: AuthService; let jwtService: DeepMocked; - const sequelize = new Sequelize({ - dialect: 'mariadb', - host: 'localhost', - port: 3360, - username: 'wri', - password: 'wri', - database: 'terramatch_microservices_test', - models: Object.values(Entities), - logging: false, - }); - - beforeAll(async () => { - await sequelize.sync({ force: true }); - FactoryGirl.setAdapter(new SequelizeAdapter()); - }) - - afterAll(async () => { - await sequelize.close(); - }) - beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AuthService, - { provide: JwtService, useValue: jwtService = createMock() } + { + provide: JwtService, + useValue: (jwtService = createMock()), + }, ], }).compile(); @@ -46,21 +26,27 @@ describe('AuthService', () => { afterEach(() => { jest.restoreAllMocks(); - }) + }); it('should return null with invalid email', async () => { jest.spyOn(User, 'findOne').mockImplementation(() => Promise.resolve(null)); - expect(await service.login('fake@foo.bar', 'asdfasdfsadf')).toBeNull() - }) + expect(await service.login('fake@foo.bar', 'asdfasdfsadf')).toBeNull(); + }); it('should return null with an invalid password', async () => { - const { emailAddress } = await UserFactory.create({ password: 'fakepasswordhash' }); + const { emailAddress } = await UserFactory.create({ + password: 'fakepasswordhash', + }); expect(await service.login(emailAddress, 'fakepassword')).toBeNull(); - }) + }); it('should return a token and id with a valid password', async () => { - const { id, emailAddress } = await UserFactory.create({ password: 'fakepasswordhash' }); - jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(true)); + const { id, emailAddress } = await UserFactory.create({ + password: 'fakepasswordhash', + }); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation(() => Promise.resolve(true)); const token = 'fake jwt token'; jwtService.signAsync.mockReturnValue(Promise.resolve(token)); @@ -74,7 +60,9 @@ describe('AuthService', () => { it('should update the last logged in date on the user', async () => { const user = await UserFactory.create({ password: 'fakepasswordhash' }); - jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(true)); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation(() => Promise.resolve(true)); jwtService.signAsync.mockResolvedValue('fake jwt token'); await service.login(user.emailAddress, 'fakepassword'); @@ -82,5 +70,5 @@ describe('AuthService', () => { const { lastLoggedInAt } = user; await user.reload(); expect(lastLoggedInAt).not.toBe(user.lastLoggedInAt); - }) + }); }); diff --git a/apps/user-service/src/auth/login.controller.ts b/apps/user-service/src/auth/login.controller.ts index c7190fe..0fdaf73 100644 --- a/apps/user-service/src/auth/login.controller.ts +++ b/apps/user-service/src/auth/login.controller.ts @@ -19,7 +19,7 @@ export class LoginController { constructor(private readonly authService: AuthService) {} @Post() - @NoBearerAuth() + @NoBearerAuth @ApiOperation({ operationId: 'authLogin', description: 'Receive a JWT Token in exchange for login credentials', diff --git a/jest.preset.js b/jest.preset.js index 2cc06ad..ee18844 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -10,5 +10,7 @@ module.exports = { lines: 95, statements: 95, } - } + }, + + setupFilesAfterEnv: ['./setup-jest.ts'], } diff --git a/libs/common/src/lib/guards/auth.guard.spec.ts b/libs/common/src/lib/guards/auth.guard.spec.ts index 69d733b..54d3c50 100644 --- a/libs/common/src/lib/guards/auth.guard.spec.ts +++ b/libs/common/src/lib/guards/auth.guard.spec.ts @@ -1,4 +1,4 @@ -import { AuthGuard } from './auth.guard'; +import { AuthGuard, NoBearerAuth } from './auth.guard'; import { Test } from '@nestjs/testing'; import { APP_GUARD } from '@nestjs/core'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; @@ -12,6 +12,12 @@ class TestController { test() { return 'test'; } + + @NoBearerAuth + @Get('/no-auth') + noAuth() { + return 'no-auth'; + } } describe('AuthGuard', () => { @@ -31,6 +37,7 @@ describe('AuthGuard', () => { afterEach(async () => { await app.close(); + jest.restoreAllMocks(); }); it('should return an error when no auth header is present', async () => { @@ -38,4 +45,25 @@ describe('AuthGuard', () => { .get('/test') .expect(HttpStatus.UNAUTHORIZED); }); + + it('should not return an error when a valid auth header is present', async () => { + const token = 'fake jwt token'; + jwtService.verifyAsync.mockResolvedValue({ sub: 'fakeuserid' }); + + await request(app.getHttpServer()) + .get('/test') + .set('Authorization', `Bearer ${token}`) + .expect(HttpStatus.OK); + }); + + it('should ignore bearer token on an endpoint with @NoBearerAuth', async () => { + await request(app.getHttpServer()) + .get('/test/no-auth') + .expect(HttpStatus.OK); + + await request(app.getHttpServer()) + .get('/test/no-auth') + .set('Authorization', 'Bearer fake jwt token') + .expect(HttpStatus.OK); + }); }); diff --git a/libs/common/src/lib/guards/auth.guard.ts b/libs/common/src/lib/guards/auth.guard.ts index 34e41d7..f60ef7f 100644 --- a/libs/common/src/lib/guards/auth.guard.ts +++ b/libs/common/src/lib/guards/auth.guard.ts @@ -8,7 +8,7 @@ import { JwtService } from '@nestjs/jwt'; import { Reflector } from '@nestjs/core'; const NO_BEARER_AUTH = 'noBearerAuth'; -export const NoBearerAuth = () => SetMetadata(NO_BEARER_AUTH, true); +export const NoBearerAuth = SetMetadata(NO_BEARER_AUTH, true); @Injectable() export class AuthGuard implements CanActivate { diff --git a/libs/common/src/lib/log.ts b/libs/common/src/lib/log.ts new file mode 100644 index 0000000..a6bd4ca --- /dev/null +++ b/libs/common/src/lib/log.ts @@ -0,0 +1,21 @@ +const IS_PROD = process.env['NODE_ENV'] === 'production'; +const IS_TEST = process.env['NODE_ENV'] === 'test'; + +// TODO: Add Sentry support +export default class Log { + static debug(message: any, ...optionalParams: any[]) { + if (!IS_PROD && !IS_TEST) console.debug(message, ...optionalParams); + } + + static info(message: any, ...optionalParams: any[]) { + if (!IS_PROD && !IS_TEST) console.info(message, ...optionalParams); + } + + static warn(message: any, ...optionalParams: any[]) { + if (!IS_TEST) console.warn(message, ...optionalParams); + } + + static error(message: any, ...optionalParams: any[]) { + if (!IS_TEST) console.error(message, ...optionalParams); + } +} diff --git a/libs/common/src/lib/policies/policy.service.spec.ts b/libs/common/src/lib/policies/policy.service.spec.ts new file mode 100644 index 0000000..c1c7e1e --- /dev/null +++ b/libs/common/src/lib/policies/policy.service.spec.ts @@ -0,0 +1,44 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PolicyService } from './policy.service'; +import { UnauthorizedException } from '@nestjs/common'; +import { + ModelHasRole, + User, +} from '@terramatch-microservices/database/entities'; +import { RequestContext } from 'nestjs-request-context'; + +export function mockUserId(userId?: number) { + jest + .spyOn(RequestContext, 'currentContext', 'get') + .mockReturnValue({ req: { authenticatedUserId: userId }, res: {} }); +} + +describe('PolicyService', () => { + let service: PolicyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PolicyService], + }).compile(); + + service = module.get(PolicyService); + }); + + afterEach(async () => { + jest.restoreAllMocks(); + }); + + it('should throw an error if no authed user is found', async () => { + mockUserId(); + await expect(() => service.authorize('foo', new User())).rejects.toThrow( + UnauthorizedException + ); + }); + + it('should throw an error if there is no policy defined', async () => { + mockUserId(123); + await expect(() => service.authorize('foo', new ModelHasRole())).rejects.toThrow( + UnauthorizedException + ); + }); +}); diff --git a/libs/common/src/lib/policies/policy.service.ts b/libs/common/src/lib/policies/policy.service.ts index eaa5157..1272afe 100644 --- a/libs/common/src/lib/policies/policy.service.ts +++ b/libs/common/src/lib/policies/policy.service.ts @@ -5,10 +5,12 @@ import { BuilderType, EntityPolicy } from './entity.policy'; import { Permission, User } from '@terramatch-microservices/database/entities'; import { AbilityBuilder, createMongoAbility } from '@casl/ability'; import { Model } from 'sequelize-typescript'; +import Log from '../log'; type EntityClass = { // eslint-disable-next-line @typescript-eslint/no-explicit-any new (...args: any[]): Model; + name?: string; }; type PolicyClass = { @@ -38,7 +40,7 @@ export class PolicyService { const [, PolicyClass] = POLICIES.find(([entityClass]) => subject instanceof entityClass) ?? []; if (PolicyClass == null) { - console.error('No policy found for subject type', subject); + Log.error('No policy found for subject type', subject.constructor.name); throw new UnauthorizedException(); } diff --git a/libs/database/src/lib/entities/framework.entity.ts b/libs/database/src/lib/entities/framework.entity.ts index 90cb323..0c9f3fb 100644 --- a/libs/database/src/lib/entities/framework.entity.ts +++ b/libs/database/src/lib/entities/framework.entity.ts @@ -6,12 +6,12 @@ import { BIGINT, STRING } from 'sequelize'; export class Framework extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; - @Column({ type: STRING(20) }) + @Column(STRING(20)) slug: string; - @Column + @Column(STRING) name: string; } diff --git a/libs/database/src/lib/entities/model-has-role.entity.ts b/libs/database/src/lib/entities/model-has-role.entity.ts index 620174d..a537649 100644 --- a/libs/database/src/lib/entities/model-has-role.entity.ts +++ b/libs/database/src/lib/entities/model-has-role.entity.ts @@ -1,6 +1,6 @@ import { Column, ForeignKey, Model, Table } from 'sequelize-typescript'; import { Role } from './role.entity'; -import { BIGINT } from 'sequelize'; +import { BIGINT, STRING } from 'sequelize'; @Table({ tableName: 'model_has_roles', underscored: true, timestamps: false }) export class ModelHasRole extends Model { @@ -8,9 +8,9 @@ export class ModelHasRole extends Model { @Column({ type: BIGINT.UNSIGNED, primaryKey: true }) roleId: number; - @Column({ primaryKey: true }) + @Column({ type: STRING, primaryKey: true }) modelType: string; - @Column({ primaryKey: true }) - modelId: bigint; + @Column({ type: BIGINT.UNSIGNED, primaryKey: true }) + modelId: number; } diff --git a/libs/database/src/lib/entities/organisation-user.entity.ts b/libs/database/src/lib/entities/organisation-user.entity.ts index 95502a7..2a4961e 100644 --- a/libs/database/src/lib/entities/organisation-user.entity.ts +++ b/libs/database/src/lib/entities/organisation-user.entity.ts @@ -7,17 +7,17 @@ import { User } from './user.entity'; export class OrganisationUser extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; @ForeignKey(() => User) - @Column({ type: BIGINT.UNSIGNED}) + @Column(BIGINT.UNSIGNED) userId: number; @ForeignKey(() => Organisation) - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) organisationId: number; - @Column({ type: STRING(20) }) + @Column(STRING(20)) status: string; } diff --git a/libs/database/src/lib/entities/organisation.entity.ts b/libs/database/src/lib/entities/organisation.entity.ts index 7fa610d..2aafce9 100644 --- a/libs/database/src/lib/entities/organisation.entity.ts +++ b/libs/database/src/lib/entities/organisation.entity.ts @@ -2,92 +2,106 @@ import { AllowNull, AutoIncrement, Column, + Default, Index, Model, PrimaryKey, - Table + Table, } from 'sequelize-typescript'; -import { BIGINT, DECIMAL, ENUM, INTEGER, TEXT, TINYINT, UUID } from 'sequelize'; +import { + BIGINT, + BOOLEAN, + DATE, + DECIMAL, + ENUM, + INTEGER, + STRING, + TEXT, + TINYINT, + UUID +} from 'sequelize'; @Table({ tableName: 'organisations', underscored: true }) export class Organisation extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; @Index - @Column({ type: UUID }) - uuid: string | null; + @Column(UUID) + uuid: string; - @Column({ defaultValue: 'draft' }) + @Default('draft') + @Column(STRING) status: string; @AllowNull - @Column + @Column(STRING) type: string | null; - @Column({ defaultValue: false }) + @Default(false) + @Column(BOOLEAN) private: boolean; @AllowNull - @Column + @Column(STRING) name: string | null; @AllowNull - @Column + @Column(STRING) phone: string | null; @AllowNull - @Column({ field: 'hq_street_1' }) + @Column({ type: STRING, field: 'hq_street_1' }) hqStreet1: string | null; @AllowNull - @Column({ field: 'hq_street_2' }) + @Column({ type: STRING, field: 'hq_street_2' }) hqStreet2: string | null; @AllowNull - @Column + @Column(STRING) hqCity: string | null; @AllowNull - @Column + @Column(STRING) hqState: string | null; @AllowNull - @Column + @Column(STRING) hqZipcode: string | null; @AllowNull - @Column + @Column(STRING) hqCountry: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) leadershipTeamTxt: string | null; @AllowNull - @Column + @Column(DATE) foundingDate: Date | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) description: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) countries: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) languages: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) treeCareApproach: string | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) relevantExperienceYears: number | null; @AllowNull @@ -95,7 +109,7 @@ export class Organisation extends Model { treesGrown3Year: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) treesGrownTotal: number | null; @AllowNull @@ -103,15 +117,15 @@ export class Organisation extends Model { haRestored3Year: number | null; @AllowNull - @Column({ type: DECIMAL(10, 2) }) + @Column(DECIMAL(10, 2)) haRestoredTotal: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) finStartMonth: number | null; @AllowNull - @Column({ type: DECIMAL(15, 2) }) + @Column(DECIMAL(15, 2)) finBudgetCurrentYear: number | null; @AllowNull @@ -127,47 +141,47 @@ export class Organisation extends Model { finBudget3Year: number | null; @AllowNull - @Column + @Column(STRING) webUrl: string | null; @AllowNull - @Column + @Column(STRING) facebookUrl: string | null; @AllowNull - @Column + @Column(STRING) instagramUrl: string | null; @AllowNull - @Column + @Column(STRING) linkedinUrl: string | null; @AllowNull - @Column + @Column(STRING) twitterUrl: string | null; @AllowNull - @Column({ type: INTEGER({ length: 10, unsigned: true }) }) + @Column(INTEGER({ length: 10, unsigned: true })) ftPermanentEmployees: number | null; @AllowNull - @Column({ type: INTEGER({ length: 10, unsigned: true }) }) + @Column(INTEGER({ length: 10, unsigned: true })) ptPermanentEmployees: number | null; @AllowNull - @Column({ type: INTEGER({ length: 10 }) }) + @Column(INTEGER({ length: 10 })) tempEmployees: number | null; @AllowNull - @Column({ type: INTEGER({ length: 10, unsigned: true }) }) + @Column(INTEGER({ length: 10, unsigned: true })) femaleEmployees: number | null; @AllowNull - @Column({ type: INTEGER({ length: 10, unsigned: true }) }) + @Column(INTEGER({ length: 10, unsigned: true })) maleEmployees: number | null; @AllowNull - @Column({ type: INTEGER({ length: 10, unsigned: true }) }) + @Column(INTEGER({ length: 10, unsigned: true })) youngEmployees: number | null; @AllowNull @@ -175,11 +189,11 @@ export class Organisation extends Model { over35Employees: number | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) additionalFundingDetails: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) communityExperience: string | null; @AllowNull @@ -191,7 +205,7 @@ export class Organisation extends Model { percentEngagedWomen3Yr: number | null; @AllowNull - @Column({ type: TINYINT({ length: 4}), field: 'percent_engaged_men_3yr' }) + @Column({ type: TINYINT({ length: 4 }), field: 'percent_engaged_men_3yr' }) percentEngagedMen3Yr: number | null; @AllowNull @@ -207,58 +221,59 @@ export class Organisation extends Model { percentEngagedSmallholder3Yr: number | null; @AllowNull - @Column({ type: INTEGER({ length: 10, unsigned: true }) }) + @Column(INTEGER({ length: 10, unsigned: true })) totalTreesGrown: number | null; @AllowNull - @Column({ type: TINYINT({ length: 4 }) }) + @Column(TINYINT({ length: 4 })) avgTreeSurvivalRate: number | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) treeMaintenanceAftercareApproach: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) restoredAreasDescription: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) restorationTypesImplemented: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) historicMonitoringGeojson: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) monitoringEvaluationExperience: string | null; @AllowNull - @Column({ type: TEXT('long') }) + @Column(TEXT('long')) fundingHistory: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) engagementFarmers: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) engagementWomen: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) engagementYouth: string | null; - @Column({ defaultValue: 'USD' }) + @Default('usd') + @Column(STRING) currency: string; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) states: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) district: string | null; @AllowNull @@ -270,31 +285,31 @@ export class Organisation extends Model { accountNumber2: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) loanStatusAmount: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) loanStatusTypes: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) approachOfMarginalizedCommunities: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) communityEngagementNumbersMarginalized: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) landSystems: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) fundUtilisation: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) detailedInterventionTypes: string | null; @AllowNull @@ -326,47 +341,47 @@ export class Organisation extends Model { communityMembersEngaged3YrBackwardClass: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) totalBoardMembers: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) pctBoardWomen: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) pctBoardMen: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) pctBoardYouth: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) pctBoardNonYouth: number | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) engagementNonYouth: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) treeRestorationPractices: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) businessModel: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) subtype: string | null; @AllowNull - @Column({ type: BIGINT({ length: 20 }) }) + @Column(BIGINT({ length: 20 })) organisationRevenueThisYear: number | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) fieldStaffSkills: string | null; @AllowNull @@ -374,47 +389,47 @@ export class Organisation extends Model { fpcCompany: string | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) numOfFarmersOnBoard: number | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) + @Column(INTEGER({ length: 11 })) numOfMarginalisedEmployees: number | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) benefactorsFpcCompany: string | null; @AllowNull - @Column + @Column(STRING) boardRemunerationFpcCompany: string | null; @AllowNull - @Column + @Column(STRING) boardEngagementFpcCompany: string | null; @AllowNull - @Column + @Column(STRING) biodiversityFocus: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) globalPlanningFrameworks: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) pastGovCollaboration: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) engagementLandless: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) socioeconomicImpact: string | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) environmentalImpact: string | null; // field misspelled intentionally to match the current DB schema @@ -423,10 +438,10 @@ export class Organisation extends Model { growthStage: string | null; @AllowNull - @Column({ type: INTEGER({ length: 11 }) }) - totalEmployees: number | null + @Column(INTEGER({ length: 11 })) + totalEmployees: number | null; @AllowNull - @Column({ type: TEXT }) + @Column(TEXT) additionalComments: string | null; } diff --git a/libs/database/src/lib/entities/permission.entity.ts b/libs/database/src/lib/entities/permission.entity.ts index 46ae5a8..f7f4383 100644 --- a/libs/database/src/lib/entities/permission.entity.ts +++ b/libs/database/src/lib/entities/permission.entity.ts @@ -1,16 +1,17 @@ -import { Column, Model, PrimaryKey, Table } from 'sequelize-typescript'; -import { BIGINT, QueryTypes } from 'sequelize'; +import { AutoIncrement, Column, Model, PrimaryKey, Table } from 'sequelize-typescript'; +import { BIGINT, QueryTypes, STRING } from 'sequelize'; @Table({ tableName: 'permissions', underscored: true }) export class Permission extends Model { @PrimaryKey - @Column({ type: BIGINT.UNSIGNED }) + @AutoIncrement + @Column(BIGINT.UNSIGNED) override id: number; - @Column + @Column(STRING) name: string; - @Column + @Column(STRING) guardName: string; /** diff --git a/libs/database/src/lib/entities/project-user.entity.ts b/libs/database/src/lib/entities/project-user.entity.ts index be040c1..c2626a7 100644 --- a/libs/database/src/lib/entities/project-user.entity.ts +++ b/libs/database/src/lib/entities/project-user.entity.ts @@ -1,7 +1,7 @@ import { AllowNull, AutoIncrement, - Column, + Column, Default, ForeignKey, Model, PrimaryKey, @@ -9,30 +9,32 @@ import { } from 'sequelize-typescript'; import { Project } from './project.entity'; import { User } from './user.entity'; -import { BIGINT } from 'sequelize'; +import { BIGINT, BOOLEAN, STRING } from 'sequelize'; @Table({ tableName: 'v2_project_users', underscored: true }) export class ProjectUser extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; @ForeignKey(() => Project) - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) projectId: number; @ForeignKey(() => User) - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) userId: number; @AllowNull - @Column + @Column(STRING) status: string; - @Column({ defaultValue: false }) + @Default(false) + @Column(BOOLEAN) isMonitoring: boolean; - @Column({ defaultValue: false }) + @Default(false) + @Column(BOOLEAN) isManaging: boolean; } diff --git a/libs/database/src/lib/entities/project.entity.ts b/libs/database/src/lib/entities/project.entity.ts index f541fb6..abe8dd6 100644 --- a/libs/database/src/lib/entities/project.entity.ts +++ b/libs/database/src/lib/entities/project.entity.ts @@ -1,14 +1,14 @@ import { AutoIncrement, Column, Model, PrimaryKey, Table } from 'sequelize-typescript'; -import { BIGINT } from 'sequelize'; +import { BIGINT, STRING } from 'sequelize'; // A quick stub to get The information needed for users/me @Table({ tableName: 'v2_projects', underscored: true }) export class Project extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; - @Column + @Column(STRING) frameworkKey: string; } diff --git a/libs/database/src/lib/entities/role.entity.ts b/libs/database/src/lib/entities/role.entity.ts index eb25759..0a93c93 100644 --- a/libs/database/src/lib/entities/role.entity.ts +++ b/libs/database/src/lib/entities/role.entity.ts @@ -5,18 +5,18 @@ import { PrimaryKey, Table, } from 'sequelize-typescript'; -import { BIGINT } from 'sequelize'; +import { BIGINT, STRING } from 'sequelize'; @Table({ tableName: 'roles', underscored: true }) export class Role extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; - @Column + @Column(STRING) name: string; - @Column + @Column(STRING) guardName: string; } diff --git a/libs/database/src/lib/entities/user.entity.ts b/libs/database/src/lib/entities/user.entity.ts index e0896fb..4e9bbd5 100644 --- a/libs/database/src/lib/entities/user.entity.ts +++ b/libs/database/src/lib/entities/user.entity.ts @@ -1,14 +1,17 @@ import { - AllowNull, AutoIncrement, + AllowNull, + AutoIncrement, BelongsTo, BelongsToMany, - Column, + Column, Default, ForeignKey, Index, - Model, PrimaryKey, - Table + Model, + PrimaryKey, + Table, + Unique } from 'sequelize-typescript'; -import { BIGINT, col, fn, Op, UUID } from 'sequelize'; +import { BIGINT, BOOLEAN, col, DATE, fn, Op, STRING, UUID } from 'sequelize'; import { Role } from './role.entity'; import { ModelHasRole } from './model-has-role.entity'; import { Permission } from './permission.entity'; @@ -22,99 +25,103 @@ import { OrganisationUser } from './organisation-user.entity'; export class User extends Model { @PrimaryKey @AutoIncrement - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) override id: number; // There are many rows in the prod DB without a UUID assigned, so this cannot be a unique // index until that is fixed. - @Column({ type: UUID, allowNull: true }) + @AllowNull @Index({ unique: false }) + @Column(UUID) uuid: string | null; @ForeignKey(() => Organisation) @AllowNull - @Column({ type: BIGINT.UNSIGNED }) + @Column(BIGINT.UNSIGNED) organisationId: number | null; @AllowNull - @Column + @Column(STRING) firstName: string | null; @AllowNull - @Column + @Column(STRING) lastName: string | null; @AllowNull - @Column({ unique: true }) + @Unique + @Column(STRING) emailAddress: string; @AllowNull - @Column + @Column(STRING) password: string | null; @AllowNull - @Column + @Column(DATE) emailAddressVerifiedAt: Date | null; @AllowNull - @Column + @Column(DATE) lastLoggedInAt: Date | null; @AllowNull - @Column + @Column(STRING) jobRole: string | null; @AllowNull - @Column + @Column(STRING) facebook: string | null; @AllowNull - @Column + @Column(STRING) twitter: string | null; @AllowNull - @Column + @Column(STRING) linkedin: string | null; @AllowNull - @Column + @Column(STRING) instagram: string | null; @AllowNull - @Column + @Column(STRING) avatar: string | null; @AllowNull - @Column + @Column(STRING) phoneNumber: string | null; @AllowNull - @Column + @Column(STRING) whatsappPhone: string | null; - @Column({ defaultValue: true }) + @Default(true) + @Column(BOOLEAN) isSubscribed: boolean; - @Column({ defaultValue: true }) + @Default(true) + @Column(BOOLEAN) hasConsented: boolean; @AllowNull - @Column + @Column(STRING) banners: string | null; @AllowNull - @Column + @Column(STRING) apiKey: string | null; @AllowNull - @Column + @Column(STRING) country: string | null; @AllowNull - @Column + @Column(STRING) program: string | null; - @Column + @Column(STRING) locale: string; @BelongsToMany(() => Role, { @@ -204,38 +211,50 @@ export class User extends Model { async loadOrganisationsRequested() { if (this.organisationsRequested == null) { this.organisationsRequested = await (this as User).$get( - 'organisationsRequested', + 'organisationsRequested' ); } return this.organisationsRequested; } - private _primaryOrganisation: (Organisation & { OrganisationUser?: OrganisationUser }) | false; - async primaryOrganisation(): Promise<(Organisation & { OrganisationUser?: OrganisationUser }) | null> { + private _primaryOrganisation: + | (Organisation & { OrganisationUser?: OrganisationUser }) + | false; + async primaryOrganisation(): Promise< + (Organisation & { OrganisationUser?: OrganisationUser }) | null + > { if (this._primaryOrganisation == null) { await this.loadOrganisation(); if (this.organisation != null) { - const userOrg = (await (this as User).$get( - 'organisations', - { limit: 1, where: { id: this.organisation.id } } - ))[0]; - return this._primaryOrganisation = userOrg ?? this.organisation; + const userOrg = ( + await (this as User).$get('organisations', { + limit: 1, + where: { id: this.organisation.id }, + }) + )[0]; + return (this._primaryOrganisation = userOrg ?? this.organisation); } - const confirmed = (await (this as User).$get('organisationsConfirmed', { limit: 1 }))[0]; + const confirmed = ( + await (this as User).$get('organisationsConfirmed', { limit: 1 }) + )[0]; if (confirmed != null) { - return this._primaryOrganisation = confirmed; + return (this._primaryOrganisation = confirmed); } - const requested = (await (this as User).$get('organisationsRequested', { limit: 1 }))[0]; + const requested = ( + await (this as User).$get('organisationsRequested', { limit: 1 }) + )[0]; if (requested != null) { - return this._primaryOrganisation = requested; + return (this._primaryOrganisation = requested); } this._primaryOrganisation = false; } - return this._primaryOrganisation === false ? null : this._primaryOrganisation; + return this._primaryOrganisation === false + ? null + : this._primaryOrganisation; } private _frameworks?: Framework[]; @@ -265,10 +284,10 @@ export class User extends Model { ).map(({ frameworkKey }) => frameworkKey); } - if (frameworkSlugs.length == 0) return this._frameworks = []; - return this._frameworks = await Framework.findAll({ + if (frameworkSlugs.length == 0) return (this._frameworks = []); + return (this._frameworks = await Framework.findAll({ where: { slug: { [Op.in]: frameworkSlugs } }, - }); + })); } return this._frameworks; diff --git a/setup-jest.ts b/setup-jest.ts new file mode 100644 index 0000000..b771f0c --- /dev/null +++ b/setup-jest.ts @@ -0,0 +1,25 @@ +import { Sequelize } from 'sequelize-typescript'; +import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts'; +import * as Entities from '@terramatch-microservices/database/entities'; + +let sequelize: Sequelize; + +beforeAll(async () => { + sequelize = new Sequelize({ + dialect: 'mariadb', + host: 'localhost', + port: 3360, + username: 'wri', + password: 'wri', + database: 'terramatch_microservices_test', + models: Object.values(Entities), + logging: false, + }) + + await sequelize.sync(); + FactoryGirl.setAdapter(new SequelizeAdapter()); +}); + +afterAll(async () => { + await sequelize.close(); +});