Skip to content

Commit

Permalink
S3 Upload Files CampaignApplication (#658)
Browse files Browse the repository at this point in the history
* Merge branch 'prod-s3-task' into prod

* updated campaingApplicationFile model

* Merge branch 'prod-s3task-copy' into task-s3

* remove console.log

* fix .env and mocks

* increase files limit to 30 MB
  • Loading branch information
Martbul authored Jul 24, 2024
1 parent 6653561 commit ff951b3
Show file tree
Hide file tree
Showing 16 changed files with 234 additions and 46 deletions.
9 changes: 7 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ SENDGRID_API_KEY=sendgrid-key
SENDGRID_SENDER_EMAIL=[email protected]
SENDGRID_INTERNAL_EMAIL=[email protected]
SENDGRID_CONTACTS_URL=/v3/marketing/contacts
MARKETING_LIST_ID=6add1a52-f74e-4c14-af56-ec7e1d2318f0
SENDGRID_SENDER_ID=
## if marketing notifications should be active --> true/false -> defaults to false
SEND_MARKETING_NOTIFICATIONS=

## Stripe ##
############
Expand All @@ -95,10 +99,11 @@ IRIS_USER_HASH=
BANK_BIC=UNCRBGSF
PLATFORM_IBAN=
IMPORT_TRX_TASK_INTERVAL_MINUTES=60
#which hour of the day to run the check for consent
CHECK_IRIS_CONSENT_TASK_HOUR=10
BILLING_ADMIN_MAIL=[email protected]
CAMPAIGN_ADMIN_MAIL=
CAMPAIGN_ADMIN_MAIL=responsible for campaign management

## Cache ##
##############
CACHE_TTL=60000
CACHE_TTL=30000
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ S3_ENDPOINT=https://cdn-dev.podkrepi.bg
S3_ACCESS_KEY=s3-access-key
S3_SECRET_ACCESS_KEY=s3-secret-access-key


## Keycloak ##
##############
KEYCLOAK_URL=http://localhost:8180/auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ export const mockNewCampaignApplication = {
category: CampaignTypeCategory.medical,
}

const dto: CreateCampaignApplicationDto = {
...mockNewCampaignApplication,
acceptTermsAndConditions: true,
transparencyTermsAccepted: true,
personalInformationProcessingAccepted: true,
toEntity: new CreateCampaignApplicationDto().toEntity,
}

export const mockCampaigns = [
{
id: '1',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Readable } from 'stream'
import { CampaignApplicationFileRole } from '@prisma/client'
import { CreateCampaignApplicationFileDto } from '../dto/create-campaignApplication-file.dto'

export const mockCampaignApplicationFileFn = () => ({
id: 'mockCampaignApplicationFileId',
filename: 'test.pdf',
mimetype: 'application/pdf',
campaignApplicationId: 'mockCampaignApplicationId',
personId: 'mockPersonId',
role: CampaignApplicationFileRole.document,
})

export const mockCampaignApplicationUploadFileFn = () => ({
bucketName: 'campaignapplication-files',
...mockCampaignApplicationFileFn(),
campaignApplicationId: 'mockCampaignApplicationId',
personId: 'mockPersonId',
})

export const mockCampaignApplicationFilesFn = (): Express.Multer.File[] => [
{
fieldname: 'resume',
originalname: 'john_doe_resume.pdf',
encoding: '7bit',
mimetype: 'application/pdf',
size: 102400,
stream: new Readable(),
destination: '/uploads/resumes',
filename: 'john_doe_resume_1234.pdf',
path: '/uploads/resumes/john_doe_resume_1234.pdf',
buffer: Buffer.from(''),
},
{
fieldname: 'cover_letter',
originalname: 'john_doe_cover_letter.pdf',
encoding: '7bit',
mimetype: 'application/pdf',
size: 51200,
stream: new Readable(),
destination: '/uploads/cover_letters',
filename: 'john_doe_cover_letter_1234.pdf',
path: '/uploads/cover_letters/john_doe_cover_letter_1234.pdf',
buffer: Buffer.from(''),
},
]

export const mockFileDtoFn = (): CreateCampaignApplicationFileDto => ({
filename: 'Test Filename',
mimetype: 'Test mimetype',
campaignApplicationId: 'Test CampaignApplicationId',
personId: 'Test PersonId',
role: CampaignApplicationFileRole.document,
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ForbiddenException, NotFoundException } from '@nestjs/common'
import { PersonService } from '../person/person.service'
import { mockUser, mockUserAdmin } from './../auth/__mocks__'
import { mockNewCampaignApplication } from './__mocks__/campaign-application-mocks'
import { mockCampaignApplicationFilesFn } from './__mocks__/campaing-application-file-mocks'

describe('CampaignApplicationController', () => {
let controller: CampaignApplicationController
Expand Down Expand Up @@ -45,20 +46,32 @@ describe('CampaignApplicationController', () => {
// Arrange
jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUser)

const mockCampaignApplicationFiles = mockCampaignApplicationFilesFn()

// Act
await controller.create(mockCreateNewCampaignApplication, mockUser)
await controller.create(
mockCampaignApplicationFiles,
mockCreateNewCampaignApplication,
mockUser,
)

// Assert
expect(service.create).toHaveBeenCalledWith(mockCreateNewCampaignApplication, mockUser)
expect(service.create).toHaveBeenCalledWith(
mockCreateNewCampaignApplication,
mockUser,
mockCampaignApplicationFiles,
)
})

it('when create called with wrong user it should throw NotFoundException', async () => {
jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(null)

const mockCampaignApplicationFiles = mockCampaignApplicationFilesFn()

// Act & Assert
await expect(controller.create(mockCreateNewCampaignApplication, mockUser)).rejects.toThrow(
NotFoundException,
)
await expect(
controller.create(mockCampaignApplicationFiles, mockCreateNewCampaignApplication, mockUser),
).rejects.toThrow(NotFoundException)
})

it('when findAll called by a non-admin user it should throw a ForbiddenException', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
ForbiddenException,
NotFoundException,
Logger,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common'
import { CampaignApplicationService } from './campaign-application.service'
import { CreateCampaignApplicationDto } from './dto/create-campaign-application.dto'
Expand All @@ -17,6 +19,8 @@ import { AuthenticatedUser, RoleMatchingMode, Roles } from 'nest-keycloak-connec
import { RealmViewSupporters, ViewSupporters } from '@podkrepi-bg/podkrepi-types'
import { KeycloakTokenParsed, isAdmin } from '../auth/keycloak'
import { PersonService } from '../person/person.service'
import { FilesInterceptor } from '@nestjs/platform-express'
import { validateFileType } from '../common/files'

@ApiTags('campaign-application')
@Controller('campaign-application')
Expand All @@ -27,18 +31,26 @@ export class CampaignApplicationController {
) {}

@Post('create')
@UseInterceptors(
FilesInterceptor('file', 10, {
limits: { fileSize: 1024 * 1024 * 30 },
fileFilter: (_req: Request, file, cb) => {
validateFileType(file, cb)
},
}),
)
async create(
@UploadedFiles() files: Express.Multer.File[],
@Body() createCampaignApplicationDto: CreateCampaignApplicationDto,
@AuthenticatedUser() user: KeycloakTokenParsed,
) {
const person = await this.personService.findOneByKeycloakId(user.sub)

if (!person) {
Logger.error('No person found in database')
throw new NotFoundException('No person found in database')
}

return this.campaignApplicationService.create(createCampaignApplicationDto, person)
return this.campaignApplicationService.create(createCampaignApplicationDto, person, files)
}

@Get('list')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { CampaignApplicationController } from './campaign-application.controller
import { PrismaModule } from '../prisma/prisma.module'
import { PersonModule } from '../person/person.module'
import { OrganizerModule } from '../organizer/organizer.module'
import { S3Service } from '../s3/s3.service'
@Module({
imports: [PrismaModule, PersonModule, OrganizerModule],
controllers: [CampaignApplicationController],
providers: [CampaignApplicationService],
providers: [CampaignApplicationService, S3Service],
})
export class CampaignApplicationModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import { Test, TestingModule } from '@nestjs/testing'
import { CampaignApplicationService } from './campaign-application.service'
import { CreateCampaignApplicationDto } from './dto/create-campaign-application.dto'
import { BadRequestException } from '@nestjs/common'
import { CampaignApplicationState, CampaignTypeCategory, Person } from '@prisma/client'
import { CampaignApplicationFileRole, CampaignTypeCategory, Person } from '@prisma/client'
import { prismaMock, MockPrismaService } from '../prisma/prisma-client.mock'
import { EmailService } from '../email/email.service'
import { OrganizerService } from '../organizer/organizer.service'
import { personMock } from '../person/__mock__/personMock'
import {
mockCampaigns,
mockCreatedCampaignApplication,
mockNewCampaignApplication,
} from './__mocks__/campaign-application-mocks'
import { S3Service } from '../s3/s3.service'
import {
mockCampaignApplicationFileFn,
mockCampaignApplicationFilesFn,
mockCampaignApplicationUploadFileFn,
} from './__mocks__/campaing-application-file-mocks'

describe('CampaignApplicationService', () => {
let service: CampaignApplicationService
Expand All @@ -30,22 +35,17 @@ describe('CampaignApplicationService', () => {
}),
}

const mockS3Service = {
uploadObject: jest.fn(),
}

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CampaignApplicationService,
MockPrismaService,
{
provide: EmailService,
useValue: {
sendFromTemplate: jest.fn(() => true),
},
},
MockPrismaService,
{
provide: OrganizerService,
useValue: mockOrganizerService,
},
{ provide: OrganizerService, useValue: mockOrganizerService },
{ provide: S3Service, useValue: mockS3Service },
],
}).compile()

Expand All @@ -65,7 +65,9 @@ describe('CampaignApplicationService', () => {
toEntity: new CreateCampaignApplicationDto().toEntity,
}

await expect(service.create(dto, mockPerson)).rejects.toThrow(
const mockCampaignApplicationFiles = mockCampaignApplicationFilesFn()

await expect(service.create(dto, mockPerson, mockCampaignApplicationFiles)).rejects.toThrow(
new BadRequestException('All agreements must be checked'),
)
})
Expand All @@ -79,7 +81,9 @@ describe('CampaignApplicationService', () => {
toEntity: new CreateCampaignApplicationDto().toEntity,
}

await expect(service.create(dto, mockPerson)).rejects.toThrow(
const mockCampaignApplicationFiles = mockCampaignApplicationFilesFn()

await expect(service.create(dto, mockPerson, mockCampaignApplicationFiles)).rejects.toThrow(
new BadRequestException('All agreements must be checked'),
)
})
Expand All @@ -93,12 +97,14 @@ describe('CampaignApplicationService', () => {
toEntity: new CreateCampaignApplicationDto().toEntity,
}

await expect(service.create(dto, mockPerson)).rejects.toThrow(
const mockCampaignApplicationFiles = mockCampaignApplicationFilesFn()

await expect(service.create(dto, mockPerson, mockCampaignApplicationFiles)).rejects.toThrow(
new BadRequestException('All agreements must be checked'),
)
})

it('should add a new campaign-application if all agreements are true', async () => {
it('should add a new campaign-application to db if all agreements are true', async () => {
const dto: CreateCampaignApplicationDto = {
...mockNewCampaignApplication,
acceptTermsAndConditions: true,
Expand All @@ -107,6 +113,10 @@ describe('CampaignApplicationService', () => {
toEntity: new CreateCampaignApplicationDto().toEntity,
}

const mockCampaignApplicationFiles = mockCampaignApplicationFilesFn()
const mockCampaignApplicationFile = mockCampaignApplicationFileFn()
const mockCampaignApplicationUploadFile = mockCampaignApplicationUploadFileFn()

const mockOrganizerId = 'mockOrganizerId'
jest.spyOn(mockOrganizerService, 'create').mockResolvedValue({
id: mockOrganizerId,
Expand All @@ -117,7 +127,13 @@ describe('CampaignApplicationService', () => {
.spyOn(prismaMock.campaignApplication, 'create')
.mockResolvedValue(mockCreatedCampaignApplication)

const result = await service.create(dto, mockPerson)
jest
.spyOn(prismaMock.campaignApplicationFile, 'create')
.mockResolvedValue(mockCampaignApplicationFile)

jest.spyOn(mockS3Service, 'uploadObject').mockResolvedValue(mockCampaignApplicationUploadFile)

const result = await service.create(dto, mockPerson, mockCampaignApplicationFiles)

expect(result).toEqual(mockCreatedCampaignApplication)

Expand Down Expand Up @@ -145,11 +161,36 @@ describe('CampaignApplicationService', () => {
},
})

mockCampaignApplicationFiles.forEach((file) => {
const fileDto = {
data: {
filename: file.originalname,
mimetype: file.mimetype,
campaignApplicationId: mockCreatedCampaignApplication.id,
personId: mockPerson.id,
role: CampaignApplicationFileRole.document,
},
}
expect(prismaMock.campaignApplicationFile.create).toHaveBeenCalledWith(fileDto)
})

mockCampaignApplicationFiles.forEach((file) => {
expect(mockS3Service.uploadObject).toHaveBeenCalledWith(
'campaignapplication-files',
mockCampaignApplicationFile.id,
file.filename,
file.mimetype,
file.buffer,
'CampaignApplicationFile',
mockCreatedCampaignApplication.id,
mockPerson.id,
)
})

expect(mockOrganizerService.create).toHaveBeenCalledTimes(1)
expect(prismaMock.campaignApplication.create).toHaveBeenCalledTimes(1)
})
})

describe('findAll', () => {
it('should return an array of campaign-applications', async () => {
prismaMock.campaignApplication.findMany.mockResolvedValue(mockCampaigns)
Expand Down
Loading

0 comments on commit ff951b3

Please sign in to comment.