import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { HttpErrorResponse } from '@angular/common/http'; import { fakeAsync, tick } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; import { Resource, ResourceUri, getUrl } from '@ngxp/rest'; import { cold } from 'jest-marbles'; import { Observable, lastValueFrom, of, throwError } from 'rxjs'; import { createProblemDetail } from '../../../test//error'; import { singleCold, singleHot } from '../../../test/marbles'; import { createDummyResource } from '../../../test/resource'; import { HttpError, ProblemDetail } from '../tech.model'; import { LinkRelationName, ResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { ResourceService } from './resource.service'; import { StateResource, createEmptyStateResource, createErrorStateResource, createStateResource, } from './resource.util'; import * as ResourceUtil from './resource.util'; describe('ResourceService', () => { let service: DummyResourceService<Resource, Resource>; let config: ResourceServiceConfig<Resource>; let repository: Mock<ResourceRepository>; const configResource: Resource = createDummyResource(); const configStateResource: StateResource<Resource> = createStateResource(configResource); const configStateResource$: Observable<StateResource<Resource>> = of(configStateResource); const dummyResource: Resource = createDummyResource(); const dummyStateResource: StateResource<Resource> = createStateResource(dummyResource); const editLinkRel: string = 'dummyEditLinkRel'; const getLinkRel: LinkRelationName = 'dummyGetLinkRel'; const deleteLinkRel: LinkRelationName = 'dummyDeleteLinkRel'; beforeEach(() => { config = { resource: configStateResource$, getLinkRel, edit: { linkRel: editLinkRel }, delete: { linkRel: deleteLinkRel }, }; repository = mock(ResourceRepository); service = new DummyResourceService(config, useFromMock(repository)); }); it('should be created', () => { expect(service).toBeTruthy(); }); describe('get', () => { const stateResource: StateResource<Resource> = createStateResource(configResource); let isInvalidResourceCombinationSpy: jest.SpyInstance; beforeEach(() => { service.stateResource.next(stateResource); service.handleResourceChanges = jest.fn(); isInvalidResourceCombinationSpy = jest .spyOn(ResourceUtil, 'isInvalidResourceCombination') .mockReturnValue(true); }); it('should handle config resource changed', fakeAsync(() => { service.get().subscribe(); tick(); expect(service.handleResourceChanges).toHaveBeenCalledWith(stateResource, configResource); })); it('should call isInvalidResourceCombinationSpy', fakeAsync(() => { service.get().subscribe(); tick(); expect(isInvalidResourceCombinationSpy).toHaveBeenCalled(); })); it('should return initial value', () => { service.stateResource.asObservable = jest .fn() .mockReturnValue(singleHot(stateResource, '-a')); const apiRootStateResource$: Observable<StateResource<Resource>> = service.get(); expect(apiRootStateResource$).toBeObservable( cold('a', { a: createEmptyStateResource(true) }), ); }); }); describe('handle resource changes', () => { const stateResource: StateResource<Resource> = createStateResource(createDummyResource()); const changedConfigResource: Resource = createDummyResource(); describe('on different config resource', () => { beforeEach(() => { service.handleConfigResourceChanges = jest.fn(); }); it('should update state resource by config resource', () => { service.configResource = createDummyResource(); service.handleResourceChanges(stateResource, changedConfigResource); expect(service.handleConfigResourceChanges).toHaveBeenCalled(); }); }); describe('on same config resource', () => { const stateResource: StateResource<Resource> = createStateResource(createDummyResource()); beforeEach(() => { service.configResource = configResource; }); it('should call shouldLoadResource', () => { service.shouldLoadResource = jest.fn(); service.handleResourceChanges(stateResource, configResource); expect(service.shouldLoadResource).toHaveBeenCalledWith(stateResource, configResource); }); it('should load resource', () => { service.shouldLoadResource = jest.fn().mockReturnValue(true); service.loadResource = jest.fn(); service.handleResourceChanges(stateResource, configResource); expect(service.loadResource).toHaveBeenCalledWith(configResource); }); it('should NOT load resource', () => { service.loadResource = jest.fn(); service.shouldLoadResource = jest.fn().mockReturnValue(false); service.handleResourceChanges(stateResource, configResource); expect(service.loadResource).not.toHaveBeenCalled(); }); }); }); describe('handle config resource changes', () => { const stateResource: StateResource<Resource> = createStateResource(createDummyResource()); it('should update configresource', () => { service.configResource = createDummyResource(); service.handleResourceChanges(stateResource, configResource); expect(service.configResource).toBe(configResource); }); describe('on stable stateresource', () => { beforeEach(() => { jest.spyOn(ResourceUtil, 'isStateResoureStable').mockReturnValue(true); service.updateStateResourceByConfigResource = jest.fn(); }); it('should update stateresource by configresource', () => { service.handleResourceChanges(stateResource, configResource); expect(service.updateStateResourceByConfigResource).toHaveBeenCalledWith( stateResource, configResource, ); }); }); describe('on instable stateresource', () => { beforeEach(() => { jest.spyOn(ResourceUtil, 'isStateResoureStable').mockReturnValue(false); }); it('should NOT update stateresource by configresource', () => { service.updateStateResourceByConfigResource = jest.fn(); service.handleResourceChanges(stateResource, configResource); expect(service.updateStateResourceByConfigResource).not.toHaveBeenCalled(); }); }); }); describe('update stateresource by configresource', () => { it('should check if should clear stateresource', () => { service.shouldClearStateResource = jest.fn(); service.updateStateResourceByConfigResource(dummyStateResource, configResource); expect(service.shouldClearStateResource).toHaveBeenCalled(); }); it('should clear resource if should', () => { service.stateResource.next(createStateResource(createDummyResource())); service.shouldClearStateResource = jest.fn().mockReturnValue(true); service.updateStateResourceByConfigResource(dummyStateResource, configResource); expect(service.stateResource.value).toEqual(createEmptyStateResource()); }); describe('on NOT clearing stateresource', () => { beforeEach(() => { service.shouldClearStateResource = jest.fn().mockReturnValue(false); }); it('should load resource if link exists', () => { service.hasGetLink = jest.fn(); service.updateStateResourceByConfigResource(dummyStateResource, configResource); expect(service.hasGetLink).toHaveBeenCalledWith(configResource); }); }); }); describe('should clear stateresource', () => { describe('on existing stateresource', () => { beforeEach(() => { service.stateResource.next(dummyStateResource); }); it('should return true if configresource is null', () => { const shouldClear: boolean = service.shouldClearStateResource(dummyStateResource, null); expect(shouldClear).toBeTruthy(); }); it('should return true if configresource has no get link', () => { const shouldClear: boolean = service.shouldClearStateResource( dummyStateResource, createDummyResource(), ); expect(shouldClear).toBeTruthy(); }); }); describe('on empty stateresource', () => { it('should return false', () => { const shouldClear: boolean = service.shouldClearStateResource( createEmptyStateResource(), null, ); expect(shouldClear).toBeFalsy(); }); it('should return false if configresource has no get link', () => { const shouldClear: boolean = service.shouldClearStateResource( createEmptyStateResource(), createDummyResource(), ); expect(shouldClear).toBeFalsy(); }); }); }); describe('should load resource', () => { const resource: Resource = createDummyResource(); const stateResource: StateResource<Resource> = createStateResource(resource); let isLoadingRequiredSpy: jest.SpyInstance<boolean>; beforeEach(() => { isLoadingRequiredSpy = jest.spyOn(ResourceUtil, 'isLoadingRequired'); }); it('should return true on existing configresource and loading is required', () => { isLoadingRequiredSpy.mockReturnValue(true); const shouldLoad: boolean = service.shouldLoadResource(stateResource, configResource); expect(shouldLoad).toBeTruthy(); }); it('should call isLoadingRequired', () => { service.shouldLoadResource(stateResource, configResource); expect(isLoadingRequiredSpy).toBeCalledWith(stateResource); }); it('should return false if configresource exists but loading is NOT required', () => { isLoadingRequiredSpy.mockReturnValue(false); const shouldLoad: boolean = service.shouldLoadResource(stateResource, configResource); expect(shouldLoad).toBeFalsy(); }); }); describe('load resource', () => { const configResourceWithGetLinkRel: Resource = createDummyResource([getLinkRel]); it('should call do load resource', () => { service.doLoadResource = jest.fn(); service.loadResource(configResourceWithGetLinkRel); expect(service.doLoadResource).toHaveBeenCalledWith( getUrl(configResourceWithGetLinkRel, config.getLinkRel), ); }); }); describe('set state resource loading', () => { it('should set loading true', () => { service.stateResource.next(createStateResource(createDummyResource())); service.setStateResourceLoading(); expect(service.stateResource.value.loading).toBeTruthy(); }); it('should set reload false', () => { service.stateResource.next({ ...createStateResource(createDummyResource()), reload: true }); service.setStateResourceLoading(); expect(service.stateResource.value.reload).toBeFalsy(); }); }); describe('do load resource', () => { let resourceUri: ResourceUri; let loadedResource: Resource; beforeEach(() => { service.setStateResourceLoading = jest.fn(); resourceUri = faker.internet.url(); loadedResource = createDummyResource(); repository.getResource.mockReturnValue(of(loadedResource)); }); it('should set state resource to loading', () => { service.doLoadResource(resourceUri); expect(service.setStateResourceLoading).toHaveBeenCalled(); }); it('should call repository', () => { service.doLoadResource(resourceUri); expect(repository.getResource).toHaveBeenCalledWith(resourceUri); }); it('should update stateresource', () => { service.updateStateResource = jest.fn(); service.doLoadResource(resourceUri); expect(service.updateStateResource).toHaveBeenCalledWith(loadedResource); }); }); describe('loadByResourceUri', () => { it('should do load resource', () => { service.doLoadResource = jest.fn(); const resourceUri: ResourceUri = faker.internet.url(); service.loadByResourceUri(resourceUri); expect(service.doLoadResource).toHaveBeenCalledWith(resourceUri); }); }); describe('update stateresource', () => { const resourceToBeSet: Resource = createDummyResource(); it('should set resource as stateresource', () => { service.stateResource.next(createEmptyStateResource()); service.updateStateResource(resourceToBeSet); expect(service.stateResource.value).toEqual(createStateResource(resourceToBeSet)); }); }); describe('save', () => { const dummyToSave: unknown = {}; const loadedResource: Resource = createDummyResource(); const resourceWithEditLinkRel: Resource = createDummyResource([editLinkRel]); it('should do save', fakeAsync(() => { const stateResource: StateResource<Resource> = createStateResource(resourceWithEditLinkRel); service.stateResource.next(stateResource); const doSaveMock: jest.Mock = (service.doSave = jest.fn()).mockReturnValue( of(loadedResource), ); service.save(dummyToSave).subscribe(); tick(); expect(doSaveMock).toHaveBeenCalledWith(resourceWithEditLinkRel, dummyToSave); })); it('should return saved object', () => { service.stateResource.next(createStateResource(resourceWithEditLinkRel)); service.doSave = jest.fn().mockReturnValue(singleHot(loadedResource)); const saved: Observable<StateResource<Resource | HttpError>> = service.save(dummyToSave); expect(saved).toBeObservable(singleCold(createStateResource(loadedResource))); }); it('should call handleError', () => { service.stateResource.next(createStateResource(createDummyResource([config.edit.linkRel]))); const errorResponse: ProblemDetail = createProblemDetail(); service.doSave = jest.fn().mockReturnValue(throwError(() => errorResponse)); service.handleError = jest.fn(); service.save(<any>{}).subscribe(); expect(service.handleError).toHaveBeenCalledWith(errorResponse); }); it('should update state resource subject', fakeAsync(() => { service.stateResource.next(createStateResource(resourceWithEditLinkRel)); service.doSave = jest.fn().mockReturnValue(of(loadedResource)); service.save(dummyToSave).subscribe(); tick(); expect(service.stateResource.value).toEqual(createStateResource(loadedResource)); })); }); describe('handleError', () => { it('should return error stateresource on problem unprocessable entity', (done: jest.DoneCallback) => { const error: ProblemDetail = createProblemDetail(); service .handleError(<HttpErrorResponse>(<any>error)) .subscribe((responseError: StateResource<HttpError>) => { expect(responseError).toEqual(createErrorStateResource(error)); done(); }); }); it('should rethrow error', () => { const error: HttpErrorResponse = <HttpErrorResponse>{ status: 500, statusText: 'Internal Server Error', }; const thrownError$: Observable<StateResource<HttpError>> = service.handleError(error); expect.assertions(1); expect(lastValueFrom(thrownError$)).rejects.toThrowError('Internal Server Error'); }); }); describe('refresh', () => { beforeEach(() => { service.loadResource = jest.fn(); }); it('should set reload true on statresource', () => { service.stateResource.next(createStateResource(createDummyResource())); service.refresh(); expect(service.stateResource.value.reload).toBeTruthy(); }); }); describe('exist resource', () => { it('should return true on existing resource', () => { service.stateResource.next(createStateResource(createDummyResource())); const existResource$: Observable<boolean> = service.existResource(); expect(existResource$).toBeObservable(singleCold(true)); }); it('should return false on null resource', () => { service.stateResource.next(createEmptyStateResource()); const existResource$: Observable<boolean> = service.existResource(); expect(existResource$).toBeObservable(singleCold(false)); }); }); describe('select resource', () => { it('should return state resource', () => { service.stateResource.next(dummyStateResource); const resource$: Observable<StateResource<Resource>> = service.selectResource(); expect(resource$).toBeObservable(singleCold(dummyStateResource)); }); }); }); export class DummyResourceService<B extends Resource, T extends Resource> extends ResourceService< B, T > { constructor( protected config: ResourceServiceConfig<B>, protected repository: ResourceRepository, ) { super(config, repository); } doSave(resource: T, toSave: unknown): Observable<T> { return of(resource); } }