Skip to content

Commit

Permalink
feat(cb2-11294): allow signed url zip files to download (#1438)
Browse files Browse the repository at this point in the history
* feat(cb2-11294): allow signed url zip files to download

* feat(cb2-11294): tidy up)
  • Loading branch information
naathanbrown authored Mar 19, 2024
1 parent 3de7d44 commit 41be7d1
Show file tree
Hide file tree
Showing 18 changed files with 165 additions and 51 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@angular/router": "^17.2.1",
"@azure/msal-angular": "^3.0.13",
"@azure/msal-browser": "^3.10.0",
"@dvsa/cvs-type-definitions": "^5.1.2",
"@dvsa/cvs-type-definitions": "^6.1.0",
"@ngrx/effects": "^17.1.0",
"@ngrx/entity": "^17.1.0",
"@ngrx/router-store": "^17.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { EUVehicleCategory } from '@dvsa/cvs-type-definitions/types/iva/defects/enums/euVehicleCategory.enum.js';
import { RequiredStandard, SectionIVA } from '@dvsa/cvs-type-definitions/types/iva/defects/get';
import { EUVehicleCategory } from '@dvsa/cvs-type-definitions/types/required-standards/defects/enums/euVehicleCategory.enum.js';
import { RequiredStandard, RequiredStandardTaxonomySection } from '@dvsa/cvs-type-definitions/types/required-standards/defects/get';
import { INSPECTION_TYPE } from '@models/test-results/test-result-required-standard.model';
import { provideMockStore } from '@ngrx/store/testing';
import { initialAppState } from '@store/index';
Expand Down Expand Up @@ -36,8 +36,8 @@ describe('RequiredStandardSelectComponent', () => {
describe('handleSelectBasicOrNormal', () => {
it('should work for basic inspection', () => {
component.basicAndNormalRequiredStandards = {
basic: ['basic' as unknown as SectionIVA, 'basic1' as unknown as SectionIVA],
normal: ['normal' as unknown as SectionIVA, 'normal1' as unknown as SectionIVA],
basic: ['basic' as unknown as RequiredStandardTaxonomySection, 'basic1' as unknown as RequiredStandardTaxonomySection],
normal: ['normal' as unknown as RequiredStandardTaxonomySection, 'normal1' as unknown as RequiredStandardTaxonomySection],
euVehicleCategories: [EUVehicleCategory.M1],
};

Expand All @@ -47,8 +47,8 @@ describe('RequiredStandardSelectComponent', () => {
});
it('should work for normal inspection', () => {
component.basicAndNormalRequiredStandards = {
basic: ['basic' as unknown as SectionIVA, 'basic1' as unknown as SectionIVA],
normal: ['normal' as unknown as SectionIVA, 'normal1' as unknown as SectionIVA],
basic: ['basic' as unknown as RequiredStandardTaxonomySection, 'basic1' as unknown as RequiredStandardTaxonomySection],
normal: ['normal' as unknown as RequiredStandardTaxonomySection, 'normal1' as unknown as RequiredStandardTaxonomySection],
euVehicleCategories: [EUVehicleCategory.M1],
};

Expand All @@ -70,7 +70,7 @@ describe('RequiredStandardSelectComponent', () => {
expect(component.selectedRequiredStandard).toBeUndefined();
});
it('should handle when I pick a section', () => {
component.handleSelect('section' as unknown as SectionIVA, Types.Section);
component.handleSelect('section' as unknown as RequiredStandardTaxonomySection, Types.Section);

expect(component.selectedSection).toBe('section');
expect(component.selectedRequiredStandard).toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { DefectGETIVA, RequiredStandard, SectionIVA } from '@dvsa/cvs-type-definitions/types/iva/defects/get';
import {
DefectGETRequiredStandards,
RequiredStandard,
RequiredStandardTaxonomySection,
} from '@dvsa/cvs-type-definitions/types/required-standards/defects/get';
import { INSPECTION_TYPE } from '@models/test-results/test-result-required-standard.model';
import { Store } from '@ngrx/store';
import { RequiredStandardState } from '@store/required-standards/reducers/required-standards.reducer';
Expand All @@ -14,13 +18,13 @@ import { Subject, takeUntil } from 'rxjs';
})
export class RequiredStandardSelectComponent implements OnInit, OnDestroy {

requiredStandards?: SectionIVA[];
requiredStandards?: RequiredStandardTaxonomySection[];
normalAndBasic?: boolean;
isEditing = false;
selectedInspectionType?: INSPECTION_TYPE;
selectedSection?: SectionIVA;
selectedSection?: RequiredStandardTaxonomySection;
selectedRequiredStandard?: RequiredStandard;
basicAndNormalRequiredStandards?: DefectGETIVA;
basicAndNormalRequiredStandards?: DefectGETRequiredStandards;

onDestroy$ = new Subject();

Expand Down Expand Up @@ -54,7 +58,7 @@ export class RequiredStandardSelectComponent implements OnInit, OnDestroy {
? this.basicAndNormalRequiredStandards?.basic : this.basicAndNormalRequiredStandards?.normal;
}

handleSelect(selected?: INSPECTION_TYPE | SectionIVA | RequiredStandard, type?: Types): void {
handleSelect(selected?: INSPECTION_TYPE | RequiredStandardTaxonomySection | RequiredStandard, type?: Types): void {
switch (type) {
case Types.InspectionType:
this.handleSelectBasicOrNormal(selected as INSPECTION_TYPE);
Expand All @@ -63,7 +67,7 @@ export class RequiredStandardSelectComponent implements OnInit, OnDestroy {
this.selectedRequiredStandard = undefined;
break;
case Types.Section:
this.selectedSection = selected as SectionIVA;
this.selectedSection = selected as RequiredStandardTaxonomySection;
this.selectedRequiredStandard = undefined;
break;
case Types.RequiredStandard:
Expand Down
11 changes: 11 additions & 0 deletions src/app/forms/custom-sections/adr/adr.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
<button
*ngIf="hasAdrDocumentation()"
[id]="'adr-doc-link'"
class="link"
appRetrieveDocument
[params]="documentParams"
[fileName]="fileName"
[fileType]="'zip'"
>
Download ADR Documentation
</button>
<app-dynamic-form-group [template]="template" [data]="techRecord" [edit]="isEditing" (formChange)="handleFormChange($event)"></app-dynamic-form-group>
22 changes: 22 additions & 0 deletions src/app/forms/custom-sections/adr/adr.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import 'node_modules/govuk-frontend/govuk/all';

.link {
border: none;
padding: 0;
text-decoration: underline;
text-decoration-thickness: max(1px, 0.0625rem);
text-underline-offset: 0.1em;
font-size: 19px;
color: $govuk-brand-colour;
font-weight: 400;
background-color: transparent;
box-shadow: none;
cursor: pointer;

&:hover {
text-decoration: underline;
text-decoration-thickness: max(3px, 0.1875rem, 0.12em);
background-color: transparent;
color: govuk-colour('dark-blue', $legacy: 'light-blue');
}
}
33 changes: 32 additions & 1 deletion src/app/forms/custom-sections/adr/adr.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type';
import { DynamicFormsModule } from '@forms/dynamic-forms.module';
import { createMockHgv } from '@mocks/hgv-record.mock';
import { provideMockStore } from '@ngrx/store/testing';
import { AdrService } from '@services/adr/adr.service';
import { TechnicalRecordService } from '@services/technical-record/technical-record.service';
import { initialAppState } from '@store/index';
import { TechRecordType } from '@dvsa/cvs-type-definitions/types/v3/tech-record/tech-record-vehicle-type';
import { AdrComponent } from './adr.component';

describe('AdrComponent', () => {
Expand Down Expand Up @@ -66,4 +66,35 @@ describe('AdrComponent', () => {
expect(spy).not.toHaveBeenCalled();
});
});

describe('adr documentation methods', () => {
it('should return false if I do not have a document id', () => {
component.techRecord = { } as unknown as TechRecordType<'hgv' | 'lgv' | 'trl'>;
const res = component.hasAdrDocumentation();
expect(res).toBeFalsy();
});

it('should return true if I do have a document id', () => {
component.techRecord = { techRecord_adrDetails_documentId: '1234' } as unknown as TechRecordType<'hgv' | 'lgv' | 'trl'>;
const res = component.hasAdrDocumentation();
expect(res).toBeTruthy();
});

it('should return a map with filename in', () => {
const map = new Map([['adrDocumentId', 'filename']]);
component.techRecord.techRecord_adrDetails_documentId = 'filename';
expect(component.documentParams).toStrictEqual(map);
});

it('should return the filename', () => {
component.techRecord.techRecord_adrDetails_documentId = 'filename';
expect(component.fileName).toBe('filename');
});

it('should error if no filename', () => {
component.techRecord.techRecord_adrDetails_documentId = undefined;
// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions
expect(() => { component.fileName; }).toThrow('Could not find ADR Documentation');
});
});
});
15 changes: 15 additions & 0 deletions src/app/forms/custom-sections/adr/adr.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ export class AdrComponent implements OnInit, OnDestroy {
this.technicalRecordService.updateEditingTechRecord({ ...this.techRecord, ...event } as TechRecordTypeVerb<'put'>);
}

get documentParams(): Map<string, string> {
return new Map([['adrDocumentId', this.fileName]]);
}

get fileName(): string {
if (this.hasAdrDocumentation()) {
return this.techRecord.techRecord_adrDetails_documentId ?? '';
}
throw new Error('Could not find ADR Documentation.');
}

hasAdrDocumentation(): boolean {
return !!this.techRecord.techRecord_adrDetails_documentId;
}

handleSubmit() {
this.globalErrorService.errors$
.pipe(takeUntil(this.destroy$), skipWhile((errors) => errors.length === 0))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InspectionType } from '@dvsa/cvs-type-definitions/types/iva/defects/get';
import { InspectionType } from '@dvsa/cvs-type-definitions/types/required-standards/defects/get';

export interface TestResultRequiredStandard {
sectionNumber: string;
Expand Down
22 changes: 19 additions & 3 deletions src/app/services/documents/documents.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,26 @@ describe('DocumentsService', () => {

service.openDocumentFromResponse(fileName, responseBody);

expect(convertToBlobSpy).toHaveBeenCalledWith(responseBody);
expect(createFileLinkSpy).toHaveBeenCalledWith(fileName, new Blob());
expect(convertToBlobSpy).toHaveBeenCalledWith(responseBody, 'pdf');
expect(createFileLinkSpy).toHaveBeenCalledWith(fileName, new Blob(), 'pdf');
expect(simulateClickSpy).toHaveBeenCalledWith(fakeAnchor);
});

it('should download a document sent back with a signed url', () => {
const convertToBlobSpy = jest.spyOn(service, 'convertToBlob');
const createFileLinkSpy = jest.spyOn(service, 'createFileLink');
const simulateClickSpy = jest.spyOn(service, 'simulateClick');

const fakeAnchorZip = domParser
.parseFromString('<a download="fileName.zip" href="Response body" target="_blank" />', 'text/html')
.querySelector('a');

service.openDocumentFromResponse(fileName, responseBody, 'zip');

expect(convertToBlobSpy).not.toHaveBeenCalled();
expect(createFileLinkSpy).not.toHaveBeenCalled();
expect(simulateClickSpy).toHaveBeenCalledWith(fakeAnchorZip);
});
});

describe('convertToBlob', () => {
Expand All @@ -52,7 +68,7 @@ describe('DocumentsService', () => {

describe('createFileLink', () => {
it('should create a downloadable anchor link for a pdf file, using the file name and blob provided', () => {
expect(service.createFileLink(fileName, new Blob())).toEqual(fakeAnchor);
expect(service.createFileLink(fileName, new Blob(), 'pdf')).toEqual(fakeAnchor);
});
});

Expand Down
27 changes: 19 additions & 8 deletions src/app/services/documents/documents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,26 @@ import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DocumentsService {
openDocumentFromResponse(fileName: string, responseBody: unknown): void {
const blob = this.convertToBlob(responseBody);
openDocumentFromResponse(fileName: string, responseBody: unknown, fileType: string = 'pdf'): void {
if (fileType === 'zip') {
const link: HTMLAnchorElement = document.createElement('a');

const link = this.createFileLink(fileName, blob);
link.href = (responseBody as string).toString();
link.target = '_blank';
link.download = `${fileName}.${fileType}`;

this.simulateClick(link);
} else {
const blob = this.convertToBlob(responseBody, fileType);

const link = this.createFileLink(fileName, blob, fileType);

this.simulateClick(link);
}

this.simulateClick(link);
}

convertToBlob(data: unknown): Blob {
convertToBlob(data: unknown, fileType?: string): Blob {
if (typeof data !== 'string') throw new Error('Cannot convert to a blob. Data needs to be of type string');

const byteArray = new Uint8Array(
Expand All @@ -20,17 +31,17 @@ export class DocumentsService {
.map((char) => char.charCodeAt(0)),
);

return new Blob([byteArray], { type: 'application/pdf; charset=utf-8' });
return new Blob([byteArray], { type: `application/${fileType}; charset=utf-8` });
}

createFileLink(fileName: string, blob: Blob): HTMLAnchorElement {
createFileLink(fileName: string, blob: Blob, fileType?: string): HTMLAnchorElement {
const url = window.URL.createObjectURL(blob);

const link: HTMLAnchorElement = document.createElement('a');

link.href = url;
link.target = '_blank';
link.download = `${fileName}.pdf`;
link.download = `${fileName}.${fileType}`;

return link;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DefectGETIVA } from '@dvsa/cvs-type-definitions/types/iva/defects/get';
import { DefectGETRequiredStandards } from '@dvsa/cvs-type-definitions/types/required-standards/defects/get';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';

Expand All @@ -10,8 +10,8 @@ export class RequiredStandardsService {

constructor(private http: HttpClient) {}

getRequiredStandards(euVehicleCategory: string): Observable<DefectGETIVA> {
return this.http.get<DefectGETIVA>(`${this.url}?euVehicleCategory=${euVehicleCategory}`, { responseType: 'json' });
getRequiredStandards(euVehicleCategory: string): Observable<DefectGETRequiredStandards> {
return this.http.get<DefectGETRequiredStandards>(`${this.url}?euVehicleCategory=${euVehicleCategory}`, { responseType: 'json' });
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class RetrieveDocumentDirective {
@Input() fileName = '';
@Input() loading?: Boolean;
@Input() certNotNeeded: boolean = false;
@Input() fileType: string = 'pdf';

constructor(private documentRetrievalService: DocumentRetrievalService, private documentsService: DocumentsService, private store: Store<State>) { }

Expand All @@ -33,7 +34,7 @@ export class RetrieveDocumentDirective {
case HttpEventType.DownloadProgress:
break;
case HttpEventType.Response:
this.documentsService.openDocumentFromResponse(this.fileName, response.body);
this.documentsService.openDocumentFromResponse(this.fileName, response.body, this.fileType);
this.store.dispatch(setSpinnerState({ showSpinner: false }));
break;
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { GlobalError } from '@core/components/global-error/global-error.interface';
import { DefectGETIVA } from '@dvsa/cvs-type-definitions/types/iva/defects/get';
import { DefectGETRequiredStandards } from '@dvsa/cvs-type-definitions/types/required-standards/defects/get';
import { createAction, props } from '@ngrx/store';

const prefix = '[Required Standards]';

export const getRequiredStandards = createAction(`${prefix} getRequiredStandards`, props<{ euVehicleCategory: string }>());
export const getRequiredStandardsSuccess = createAction(`${prefix} getRequiredStandards Success`, props<{ requiredStandards: DefectGETIVA }>());
export const getRequiredStandardsSuccess = createAction(
`${prefix} getRequiredStandards Success`,
props<{ requiredStandards: DefectGETRequiredStandards }>(),
);
export const getRequiredStandardsFailure = createAction(`${prefix} getRequiredStandards Failure`, props<GlobalError>());
Loading

0 comments on commit 41be7d1

Please sign in to comment.