Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-5727 - Use etag to use preview cache #4537

Merged
merged 13 commits into from
Nov 9, 2023
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { AntivirusService } from '@infra/antivirus';
import { PreviewProducer } from '@infra/preview-generator';
import { S3ClientAdapter } from '@infra/s3-client';
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { ICurrentUser } from '@modules/authentication';
import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard';
import { ExecutionContext, INestApplication, NotFoundException, StreamableFile } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ApiValidationError } from '@shared/common';
import { EntityId, Permission } from '@shared/domain';
import { AntivirusService } from '@infra/antivirus';
import { PreviewProducer } from '@infra/preview-generator';
import { S3ClientAdapter } from '@infra/s3-client';
import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing';
import NodeClam from 'clamscan';
import { Request } from 'express';
Expand Down Expand Up @@ -63,6 +63,19 @@ class API {
};
}

async getPreviewWithEtag(routeName: string, etag: string, query?: string | Record<string, unknown>) {
const response = await request(this.app.getHttpServer())
.get(routeName)
.query(query || {})
.set('If-None-Match', etag);

return {
result: response.body as StreamableFile,
error: response.body as ApiValidationError,
status: response.status,
};
}

async getPreviewBytesRange(routeName: string, bytesRange: string, query?: string | Record<string, unknown>) {
const response = await request(this.app.getHttpServer())
.get(routeName)
Expand Down Expand Up @@ -299,34 +312,75 @@ describe('File Controller (API) - preview', () => {
return { uploadedFile };
};

it('should return status 200 for successful download', async () => {
const { uploadedFile } = await setup();
const query = {
...defaultQueryParameters,
forceUpdate: false,
};

const response = await api.getPreview(`/file/preview/${uploadedFile.id}/${uploadedFile.name}`, query);

expect(response.status).toEqual(200);
describe('WHEN header contains no etag', () => {
it('should return status 200 for successful download', async () => {
const { uploadedFile } = await setup();
const query = {
...defaultQueryParameters,
forceUpdate: false,
};

const response = await api.getPreview(`/file/preview/${uploadedFile.id}/${uploadedFile.name}`, query);

expect(response.status).toEqual(200);
});

it('should return status 206 and required headers for the successful partial file stream download', async () => {
const { uploadedFile } = await setup();
const query = {
...defaultQueryParameters,
forceUpdate: false,
};

const response = await api.getPreviewBytesRange(
`/file/preview/${uploadedFile.id}/${uploadedFile.name}`,
'bytes=0-',
query
);

expect(response.status).toEqual(206);
expect(response.headers['accept-ranges']).toMatch('bytes');
expect(response.headers['content-range']).toMatch('bytes 0-3/4');
expect(response.headers.etag).toMatch('testTag');
});
});

it('should return status 206 and required headers for the successful partial file stream download', async () => {
const { uploadedFile } = await setup();
const query = {
...defaultQueryParameters,
forceUpdate: false,
};

const response = await api.getPreviewBytesRange(
`/file/preview/${uploadedFile.id}/${uploadedFile.name}`,
'bytes=0-',
query
);
describe('WHEN header contains not matching etag', () => {
it('should return status 200 for successful download', async () => {
const { uploadedFile } = await setup();
const query = {
...defaultQueryParameters,
forceUpdate: false,
};
const etag = 'otherTag';

const response = await api.getPreviewWithEtag(
`/file/preview/${uploadedFile.id}/${uploadedFile.name}`,
etag,
query
);

expect(response.status).toEqual(200);
});
});

expect(response.status).toEqual(206);
expect(response.headers['accept-ranges']).toMatch('bytes');
expect(response.headers['content-range']).toMatch('bytes 0-3/4');
describe('WHEN header contains matching etag', () => {
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
it('should return status 304', async () => {
const { uploadedFile } = await setup();
const query = {
...defaultQueryParameters,
forceUpdate: false,
};
const etag = 'testTag';

const response = await api.getPreviewWithEtag(
`/file/preview/${uploadedFile.id}/${uploadedFile.name}`,
etag,
query
);

expect(response.status).toEqual(304);
});
});
});

Expand Down Expand Up @@ -369,6 +423,7 @@ describe('File Controller (API) - preview', () => {
expect(response.status).toEqual(206);
expect(response.headers['accept-ranges']).toMatch('bytes');
expect(response.headers['content-range']).toMatch('bytes 0-3/4');
expect(response.headers.etag).toMatch('testTag');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export class FilesStorageController {
@ApiOperation({ summary: 'Streamable download of a preview file.' })
@ApiResponse({ status: 200, type: StreamableFile })
@ApiResponse({ status: 206, type: StreamableFile })
@ApiResponse({ status: 304, description: 'Not Modified' })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
Expand All @@ -134,15 +135,24 @@ export class FilesStorageController {
@Query() previewParams: PreviewParams,
@Req() req: Request,
@Res({ passthrough: true }) response: Response,
@Headers('Range') bytesRange?: string
): Promise<StreamableFile> {
@Headers('Range') bytesRange?: string,
@Headers('If-None-Match') etag?: string
): Promise<StreamableFile | void> {
const fileResponse = await this.filesStorageUC.downloadPreview(
currentUser.userId,
params,
previewParams,
bytesRange
);

response.set({ ETag: fileResponse.etag });

if (etag === fileResponse.etag) {
response.status(HttpStatus.NOT_MODIFIED);

return undefined;
dyedwiper marked this conversation as resolved.
Show resolved Hide resolved
}

const streamableFile = this.streamFileToClient(req, fileResponse, response, bytesRange);

return streamableFile;
Expand Down
Loading