diff --git a/goofy-client/libs/binary-file-shared/src/index.ts b/goofy-client/libs/binary-file-shared/src/index.ts index 98668608fffec2553c79e128f2e106b925cdefa1..bffe195cf7eb6a011c7efeefa83563e153bea2ad 100644 --- a/goofy-client/libs/binary-file-shared/src/index.ts +++ b/goofy-client/libs/binary-file-shared/src/index.ts @@ -1,4 +1,7 @@ +export * from './lib/+state/binary-file.actions'; +export * from './lib/+state/binary-file.reducer'; export * from './lib/binary-file-shared.module'; export * from './lib/binary-file.linkrel'; export * from './lib/binary-file.model'; export * from './lib/binary-file.service'; + diff --git a/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.actions.ts b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0a6f96f0ea9b47204178b519a985c3ec33b577f --- /dev/null +++ b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.actions.ts @@ -0,0 +1,21 @@ +import { ApiError, ApiErrorAction, TypedActionCreatorWithProps } from '@goofy-client/tech-shared'; +import { createAction, props } from '@ngrx/store'; +import { TypedAction } from '@ngrx/store/src/models'; +import { ResourceUri } from '@ngxp/rest'; + +export interface SaveBinaryFileAsPdfAction { + fileData: Blob + fileName: string +} + +export interface DownloadBinaryFileAsPdfAction { + uri: ResourceUri + fileName: string, + successAction: () => DownloadBinaryFileSuccessAction & TypedAction<string>, + failureAction: (apiError: ApiError) => ApiErrorAction & TypedAction<string>, +} + +export interface DownloadBinaryFileSuccessAction { } + +export const downloadPdf: TypedActionCreatorWithProps<DownloadBinaryFileAsPdfAction> = createAction('[BinaryFile] Download pdf file', props<DownloadBinaryFileAsPdfAction>()); +export const saveAsPdf: TypedActionCreatorWithProps<SaveBinaryFileAsPdfAction> = createAction('[BinaryFile/API] Save file as pdf', props<SaveBinaryFileAsPdfAction>()); \ No newline at end of file diff --git a/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.effects.spec.ts b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.effects.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a20c2721819ee1ac79c6945a5e6a2c34073b3fa --- /dev/null +++ b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.effects.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; +import faker from '@faker-js/faker'; +import { TypedActionCreator } from '@goofy-client/tech-shared'; +import { Mock, mock } from '@goofy-client/test-utils'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Action, createAction } from '@ngrx/store'; +import { provideMockStore } from '@ngrx/store/testing'; +import { ResourceUri } from '@ngxp/rest'; +import { NxModule } from '@nrwl/angular'; +import { hot } from 'jasmine-marbles'; +import { cold } from 'jest-marbles'; +import { Observable, of } from 'rxjs'; +import { BinaryFileRepository } from '../binary-file.repository'; +import { BinaryFileEffects } from './binary-file.effects'; + +import * as BinaryFileActions from './binary-file.actions'; + +describe('BinaryFileEffects', () => { + let actions: Observable<Action>; + let effects: BinaryFileEffects; + + const binaryFileRepository: Mock<BinaryFileRepository> = mock(BinaryFileRepository); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NxModule.forRoot()], + providers: [ + BinaryFileEffects, + provideMockActions(() => actions), + provideMockStore(), + { + provide: BinaryFileRepository, + useValue: binaryFileRepository + }, + ], + }); + + effects = TestBed.inject(BinaryFileEffects); + }); + + describe('downloadPdf', () => { + + const uri: ResourceUri = faker.internet.url(); + const fileName: string = faker.random.alphaNumeric(); + const fileData: Blob = new Blob(); + + const downloadAsPdfSuccess: TypedActionCreator = createAction('[Test Action] Download Success'); + //const downloadAsPdfFailure: TypedActionCreatorWithProps<ApiErrorAction>;// = createAction('[BinaryFile/Test] Download Failure', props<ApiErrorAction>()); + + const downloadPdfAction = BinaryFileActions.downloadPdf({ successAction: downloadAsPdfSuccess, failureAction: null, fileName, uri }); + + it('should call repository', () => { + actions = hot('-a', { a: downloadPdfAction }); + + effects.downloadPdf$.subscribe(() => { + expect(binaryFileRepository.downloadPdf).toHaveBeenCalledWith(uri); + }); + }); + + it('should return actions on success', () => { + binaryFileRepository.downloadPdf.mockReturnValue(of(fileData)); + + actions = hot('-a', { a: downloadPdfAction }); + + const expected = cold('-(bc)', { + b: BinaryFileActions.saveAsPdf({ fileName, fileData }), + c: downloadAsPdfSuccess() + }); + expect(effects.downloadPdf$).toBeObservable(expected); + }); + + it('should return actions on failure', () => { + + }) + }); + + describe('saveAsPdf', () => { + + const fileName: string = faker.random.alphaNumeric(); + const fileData: Blob = new Blob(); + + it('should save file', () => { + effects.save = jest.fn(); + + actions = hot('a', { a: BinaryFileActions.saveAsPdf({ fileData, fileName }) }); + + effects.saveAsPdf$.subscribe(() => { + expect(effects.save).toHaveBeenCalledWith(fileData, fileName); + }) + }) + }) +}); diff --git a/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.effects.ts b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d0673cd46750ecc52cc70b595a42ef315e310fb --- /dev/null +++ b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.effects.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { mergeMap, switchMap, tap } from 'rxjs/operators'; +import { BinaryFileRepository } from '../binary-file.repository'; + +import * as saveAs from 'file-saver'; +import * as BinaryFileActions from './binary-file.actions'; +import { DownloadBinaryFileAsPdfAction, SaveBinaryFileAsPdfAction } from './binary-file.actions'; + +@Injectable() +export class BinaryFileEffects { + + constructor(private readonly actions$: Actions, private readonly binaryFileRepository: BinaryFileRepository) { } + + downloadPdf$ = createEffect(() => + this.actions$.pipe( + ofType(BinaryFileActions.downloadPdf), + switchMap((action: DownloadBinaryFileAsPdfAction) => this.binaryFileRepository.downloadPdf(action.uri).pipe( + mergeMap((fileData: Blob) => [BinaryFileActions.saveAsPdf({ fileData, fileName: action.fileName }), action.successAction()]) + //catchError(error => of(action.failureAction(getApiErrorFromHttpErrorResponse(error)))) + )) + ) + ); + + saveAsPdf$ = createEffect(() => + this.actions$.pipe( + ofType(BinaryFileActions.saveAsPdf), + tap((action: SaveBinaryFileAsPdfAction) => this.save(action.fileData, action.fileName)) + ), { dispatch: false }) + + + save(fileData: Blob, name: string): void { + saveAs(fileData, name); + } +} \ No newline at end of file diff --git a/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.reducer.ts b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..780e0f16490e8c7ea469de48c1b347cb52b3b40d --- /dev/null +++ b/goofy-client/libs/binary-file-shared/src/lib/+state/binary-file.reducer.ts @@ -0,0 +1,17 @@ +import { Action, createReducer } from '@ngrx/store'; + +export const BINARY_FILE_FEATURE_KEY = 'BinaryFileState'; + +export interface BinaryFileState { } + +export interface BinaryFilePartialState { + readonly [BINARY_FILE_FEATURE_KEY]: BinaryFileState; +} + +export const initialBinaryFileState: BinaryFileState = {}; + +const reducer = createReducer(initialBinaryFileState); + +export function binaryFileReducer(state: BinaryFileState, action: Action) { + return reducer(state, action); +} \ No newline at end of file diff --git a/goofy-client/libs/binary-file-shared/src/lib/binary-file-shared.module.ts b/goofy-client/libs/binary-file-shared/src/lib/binary-file-shared.module.ts index 524504acd5a8541590f7224ece64dd0dbfe46dec..4da60471e937dc30cb7e217667b88c69984e1e6e 100644 --- a/goofy-client/libs/binary-file-shared/src/lib/binary-file-shared.module.ts +++ b/goofy-client/libs/binary-file-shared/src/lib/binary-file-shared.module.ts @@ -1,7 +1,18 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreModule } from '@ngrx/store'; +import { BinaryFileEffects } from './+state/binary-file.effects'; +import * as fromBinaryFile from './+state/binary-file.reducer'; @NgModule({ - imports: [CommonModule], + imports: [ + CommonModule, + StoreModule.forFeature( + fromBinaryFile.BINARY_FILE_FEATURE_KEY, + fromBinaryFile.binaryFileReducer + ), + EffectsModule.forFeature([BinaryFileEffects]), + ] }) export class BinaryFileSharedModule { } diff --git a/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts b/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts index 6a145a53fc721d4e8aafcd07109daa9f84d40095..67944b877d0694b0c9f2fe8d917c489412d08f1a 100644 --- a/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts +++ b/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts @@ -7,7 +7,7 @@ import { cold, hot } from 'jest-marbles'; import { DummyLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyListResource, createDummyResource } from 'libs/tech-shared/test/resource'; import { of } from 'rxjs'; -import { BinaryFileRepository, GetRequestOptions } from './binary-file.repository'; +import { BinaryFileRepository, ContentType, GetRequestOptions } from './binary-file.repository'; describe('BinaryFileRepository', () => { let repository: BinaryFileRepository; @@ -96,14 +96,61 @@ describe('BinaryFileRepository', () => { function getExpectedRequestOptions(): HttpHeaders { let headers = new HttpHeaders(); - headers = headers.set('Accept', ['application/*', 'images/*']); + headers = headers.set('Accept', [ContentType.APPLICATION_ALL, ContentType.IMAGES_ALL]); + return headers; + } + }) + + describe('downloadPdf', () => { + + const blob = {}; + const uri: ResourceUri = faker.internet.url(); + + beforeEach(() => { + httpClient.get.mockReturnValue(hot('a', { a: blob })); + }) + + it('should call httpClient', () => { + const requestOptions = {}; + repository.buildPdfRequestOptions = jest.fn(); + (<any>repository.buildPdfRequestOptions).mockReturnValue(requestOptions); + + repository.downloadPdf(uri); + + expect(httpClient.get).toHaveBeenCalledWith(uri, requestOptions); + }) + + it('should return value', () => { + const result = repository.downloadPdf(uri); + + expect(result).toBeObservable(cold('b', { b: blob })); + }) + + describe('buildPdfRequestOptions', () => { + + it('should return httpHeaders', () => { + const result: GetRequestOptions = repository.buildPdfRequestOptions(); + + expect(result.responseType).toEqual('blob'); + }) + + it('should return responseType', () => { + const result: GetRequestOptions = repository.buildPdfRequestOptions(); + + expect(result.headers).toEqual(getExpectedRequestOptions()); + }) + }) + + function getExpectedRequestOptions(): HttpHeaders { + let headers = new HttpHeaders(); + headers = headers.set('Accept', [ContentType.APPLICATION_PDF]); return headers; } }) describe('get file', () => { - const uri: ResourceUri = getUrl(createDummyResource()); + const uri: ResourceUri = faker.internet.url(); it('should call repository factory', () => { repository.getFile(uri); @@ -139,21 +186,4 @@ describe('BinaryFileRepository', () => { expect(resourceWrapper.get).toHaveBeenCalledWith(DummyLinkRel.DUMMY); }) }) - - describe('get file', () => { - - const uri: ResourceUri = faker.internet.url(); - - it('should call repository factory', () => { - repository.getFile(uri); - - expect(resourceFactory.fromId).toHaveBeenCalledWith(uri); - }) - - it('should call repository wrapper', () => { - repository.getFile(uri); - - expect(resourceWrapper.get).toHaveBeenCalled(); - }) - }) }) \ No newline at end of file diff --git a/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.ts b/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.ts index ebe0ae6c610676eab25797728efe670ebe064cf5..5ef153ffac5efcd06493c8c892b04d587a795ff3 100644 --- a/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.ts +++ b/goofy-client/libs/binary-file-shared/src/lib/binary-file.repository.ts @@ -14,16 +14,32 @@ export class BinaryFileRepository { formData.append('file', file, file.name); - return this.httpClient.post(getUrl(resource, linkRel), formData, { observe: 'response' }) + return this.httpClient.post(getUrl(resource, linkRel), formData, { observe: 'response' }); } public download(fileResource: Resource, linkRel: string): Observable<Blob> { - return this.httpClient.get<Blob>(getUrl(fileResource, linkRel), this.buildRequestOptions()); + return this.doDownload(getUrl(fileResource, linkRel), this.buildRequestOptions()); } buildRequestOptions(): GetRequestOptions { + return this.buildBaseRequestOptions([ContentType.APPLICATION_ALL, ContentType.IMAGES_ALL]); + } + + public downloadPdf(uri: ResourceUri): Observable<Blob> { + return this.doDownload(uri, this.buildPdfRequestOptions()); + } + + private doDownload(uri: ResourceUri, requestOptions: GetRequestOptions): Observable<Blob> { + return this.httpClient.get<Blob>(uri, requestOptions); + } + + buildPdfRequestOptions(): GetRequestOptions { + return this.buildBaseRequestOptions([ContentType.APPLICATION_PDF]); + } + + buildBaseRequestOptions(contentTypes: ContentType[]): GetRequestOptions { let headers = new HttpHeaders(); - headers = headers.set('Accept', ['application/*', 'images/*']); + headers = headers.set('Accept', contentTypes); return { headers, responseType: 'blob' as 'json' }; } @@ -37,6 +53,12 @@ export class BinaryFileRepository { } export interface GetRequestOptions { - headers?: HttpHeaders | { [header: string]: string | string[] }; - responseType?: 'json'; + headers: HttpHeaders | { [header: string]: string | string[] }; + responseType: 'json'; +} + +export enum ContentType { + APPLICATION_PDF = 'application/pdf', + IMAGES_ALL = 'images/*', + APPLICATION_ALL = 'application/*' } \ No newline at end of file