Skip to content

Commit

Permalink
fix: include files in campaign application (#671)
Browse files Browse the repository at this point in the history
* fix: include files in campaign application

- add: download a file

* chore: add tests for campaign application file fetch
  • Loading branch information
gparlakov authored Sep 29, 2024
1 parent 1bcb946 commit 4c1bfba
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Param,
Patch,
Post,
Response,
StreamableFile,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common'
Expand Down Expand Up @@ -101,6 +103,33 @@ export class CampaignApplicationController {
return this.campaignApplicationService.deleteFile(id, isAdminFlag, person)
}

@Get('fileById/:id')
async fetchFile(
@Param('id') id: string,
@AuthenticatedUser() user: KeycloakTokenParsed,
@Response({ passthrough: true }) res,
): Promise<StreamableFile> {
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')
}

const isAdminFlag = isAdmin(user)

const file = await this.campaignApplicationService.getFile(id, isAdminFlag, person)

res.set({
'Content-Type': file?.mimetype,
'Content-Disposition': 'attachment; filename="' + file.filename + '"',
'Cache-Control': (file.mimetype ?? '').startsWith('image/')
? 'public, s-maxage=15552000, stale-while-revalidate=15552000, immutable'
: 'no-store',
})

return new StreamableFile(file.stream)
}

@Patch(':id')
async update(
@Param('id') id: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('CampaignApplicationService', () => {
const mockS3Service = {
uploadObject: jest.fn(),
deleteObject: jest.fn(),
streamFile: jest.fn().mockResolvedValue(1234),
}

const mockEmailService = {
Expand Down Expand Up @@ -290,6 +291,18 @@ describe('CampaignApplicationService', () => {

expect(result).toEqual(mockSingleCampaignApplication)
expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1)
expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledWith({
where: { id: 'id' },
include: {
documents: {
select: {
id: true,
filename: true,
mimetype: true,
},
},
},
})
})

it('should throw a NotFoundException if no campaign-application is found', async () => {
Expand Down Expand Up @@ -422,4 +435,72 @@ describe('CampaignApplicationService', () => {
})
})
})

describe('getFile', () => {
it('should return a single campaign-application file', async () => {
prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication)
prismaMock.campaignApplicationFile.findFirst.mockResolvedValue({
id: '123',
filename: 'my-file',
} as File)

const result = await service.getFile('id', false, mockPerson)

expect(result).toEqual({
filename: 'my-file',
stream: 1234,
})
expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledTimes(1)
expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledWith({
where: {
documents: {
some: {
id: 'id',
},
},
},
})

expect(prismaMock.campaignApplicationFile.findFirst).toHaveBeenNthCalledWith(1, {
where: { id: 'id' },
})
})

it('should throw a NotFoundException if no campaign-application is found', async () => {
prismaMock.campaignApplication.findUnique.mockResolvedValue(null)

await expect(service.getFile('id', false, mockPerson)).rejects.toThrow(
new NotFoundException('File does not exist'),
)
})

it('should handle errors and throw an exception', async () => {
const errorMessage = 'error'
prismaMock.campaignApplication.findFirst.mockRejectedValue(new Error(errorMessage))

await expect(service.getFile('id', false, mockPerson)).rejects.toThrow(errorMessage)
})

it('should not allow non-admin users to see files belonging to other users', async () => {
prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication)
await expect(
service.getFile('id', false, { ...mockPerson, organizer: { id: 'different-id' } }),
).rejects.toThrow(
new ForbiddenException('User is not admin or organizer of the campaignApplication'),
)
})

it('should allow admin users to see files belonging to other users', async () => {
prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication)
prismaMock.campaignApplicationFile.findFirst.mockResolvedValue({
id: '123',
filename: 'my-file',
} as File)
await expect(
service.getFile('id', true, { ...mockPerson, organizer: { id: 'different-id' } }),
).resolves.not.toThrow(
new ForbiddenException('User is not admin or organizer of the campaignApplication'),
)
})
})
})
50 changes: 50 additions & 0 deletions apps/api/src/campaign-application/campaign-application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ export class CampaignApplicationService {
try {
const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({
where: { id },
include: {
documents: {
select: {
id: true,
filename: true,
mimetype: true,
},
},
},
})
if (!singleCampaignApplication) {
throw new NotFoundException('Campaign application doesnt exist')
Expand Down Expand Up @@ -338,4 +347,45 @@ export class CampaignApplicationService {
throw error
}
}

async getFile(
id: string,
isAdminFlag: boolean,
person: Prisma.PersonGetPayload<{ include: { organizer: { select: { id: true } } } }>,
) {
try {
const campaignApplication = await this.prisma.campaignApplication.findFirst({
where: {
documents: {
some: {
id: id,
},
},
},
})

if (!campaignApplication) {
throw new NotFoundException('File does not exist')
}

if (isAdminFlag === false && campaignApplication.organizerId !== person.organizer?.id) {
throw new ForbiddenException('User is not admin or organizer of the campaignApplication')
}

const file = await this.prisma.campaignApplicationFile.findFirst({ where: { id: id } })
if (!file) {
Logger.warn('No campaign application file record with ID: ' + id)
throw new NotFoundException('No campaign application file record with ID: ' + id)
}

return {
filename: encodeURIComponent(file.filename),
mimetype: file.mimetype,
stream: await this.s3.streamFile(this.bucketName, id),
}
} catch (error) {
Logger.error('Error in getFile():', error)
throw error
}
}
}
3 changes: 1 addition & 2 deletions apps/api/src/email/template.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ export class CreateCampaignApplicationAdminEmailDto extends EmailTemplate<{
name = TemplateType.createCampaignApplicationAdmin
}


export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{
campaignApplicationName: string
editLink?: string
Expand All @@ -122,4 +121,4 @@ export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{
firstName: string
}> {
name = TemplateType.createCampaignApplicationOrganizer
}
}

0 comments on commit 4c1bfba

Please sign in to comment.