import { vi, describe, test, it, expect, afterEach } from 'vitest' import { Str, Uid, Attr } from 'pinia-orm/dist/decorators' import type { AnyJson } from '~/types/data' import HydraNormalizer from '~/services/data/normalizer/hydraNormalizer' import { METADATA_TYPE } from '~/types/enum/data' import ApiModel from '~/models/ApiModel' import { IriEncoded } from '~/models/decorators' import UrlUtils from '~/services/utils/urlUtils' import ApiResource from '~/models/ApiResource' class DummyApiChild extends ApiModel { static entity = 'dummyChild' @Uid() declare id: number | string } class DummyApiModel extends ApiModel { static entity = 'dummyModel' @Uid() declare id: number | string @Attr(null) @IriEncoded(DummyApiChild) declare oneToOneRelation: number | null @Attr([]) @IriEncoded(DummyApiChild) declare oneToManyRelation: [] @Str(null) declare name: null } afterEach(() => { vi.clearAllMocks() vi.resetAllMocks() }) describe('normalize', () => { const initialMakeUriMethod = UrlUtils.makeIRI afterEach(() => { UrlUtils.makeIRI = initialMakeUriMethod }) test('should normalize an entity into a JSON Object', () => { const entity: DummyApiModel = new DummyApiModel({ id: 7351, oneToOneRelation: 99, oneToManyRelation: [123, 124, 125], }) // @ts-ignore UrlUtils.makeIRI = vi.fn((targetEntity, id) => { return { 99: '/api/dummyChild/99', 123: '/api/dummyChild/123', 124: '/api/dummyChild/124', 125: '/api/dummyChild/125', }[id] }) const result = HydraNormalizer.normalizeEntity(entity) const expected = { id: 7351, name: null, oneToOneRelation: '/api/dummyChild/99', oneToManyRelation: [ '/api/dummyChild/123', '/api/dummyChild/124', '/api/dummyChild/125', ], } expect(result).toStrictEqual(expected) }) }) describe('denormalize', () => { // @ts-ignore const initialGetDataMethod = HydraNormalizer.getData // @ts-ignore const initialGetMetadataMethod = HydraNormalizer.getMetadata afterEach(() => { // @ts-ignore HydraNormalizer.getData = initialGetDataMethod // @ts-ignore HydraNormalizer.getMetadata = initialGetMetadataMethod }) test('should parse a API Item response and return a JSON Object', () => { const data: AnyJson = { id: 1, name: 'foo' } // @ts-ignore HydraNormalizer.getData = vi.fn((data, model) => { return data }) // @ts-ignore HydraNormalizer.getMetadata = vi.fn((data, model) => { return { type: METADATA_TYPE.ITEM } }) const result = HydraNormalizer.denormalize(data, DummyApiModel) const expected = { data: { id: 1, name: 'foo', }, metadata: { type: METADATA_TYPE.ITEM, }, } expect(result).toEqual(expected) }) }) describe('getData', () => { test('With collection', () => { const data = { '@context': '/api/contexts/Foo', '@id': '/api/foo', '@type': 'hydra:Collection', 'hydra:member': ['foo'], } // @ts-ignore expect(HydraNormalizer.getData(data)).toEqual(['foo']) }) test('With item', () => { const data = { '@context': '/api/contexts/Foo', '@id': '/api/foo', '@type': 'Foo', param1: 'a', } // @ts-ignore expect(HydraNormalizer.getData(data)).toEqual(data) }) }) describe('getMetadata', () => { test('With valid collection metadata', () => { const data = { '@context': '/api/contexts/Foo', '@id': '/api/foo', '@type': 'hydra:Collection', 'hydra:member': ['foo'], 'hydra:totalItems': 10, 'hydra:view': { '@id': '/api/foo?page=3', '@type': 'hydra:PartialCollectionView', 'hydra:first': '/api/foo?page=1', 'hydra:last': '/api/foo?page=5', 'hydra:next': '/api/foo?page=4', 'hydra:previous': '/api/foo?page=2', }, } // @ts-ignore const metadata = HydraNormalizer.getMetadata(data) expect(metadata.totalItems).toEqual(10) expect(metadata.firstPage).toEqual(1) expect(metadata.lastPage).toEqual(5) expect(metadata.nextPage).toEqual(4) expect(metadata.previousPage).toEqual(2) expect(metadata.type).toEqual(METADATA_TYPE.COLLECTION) }) test('With partial collection metadata', () => { const data = { '@context': '/api/contexts/Foo', '@id': '/api/foo', '@type': 'hydra:Collection', 'hydra:member': ['foo'], 'hydra:totalItems': 10, 'hydra:view': { '@id': '/api/foo?page=3', '@type': 'hydra:PartialCollectionView', 'hydra:first': '/api/foo?page=1', }, } // @ts-ignore const metadata = HydraNormalizer.getMetadata(data) expect(metadata.totalItems).toEqual(10) expect(metadata.firstPage).toEqual(1) expect(metadata.lastPage).toEqual(1) expect(metadata.nextPage).toEqual(undefined) expect(metadata.previousPage).toEqual(undefined) }) test('With item metadata', () => { // @ts-ignore const metadata = HydraNormalizer.getMetadata({}) expect(metadata.type).toEqual(METADATA_TYPE.ITEM) }) }) describe('denormalizeItem', () => { // @ts-ignore const initialDenormalizeEntity = HydraNormalizer.denormalizeEntity // @ts-ignore const initialDenormalizeEnum = HydraNormalizer.denormalizeEnum // @ts-ignore const initialParseEntityIRI = HydraNormalizer.parseEntityIRI const initialConsoleError = console.error afterEach(() => { // @ts-ignore HydraNormalizer.denormalizeEntity = initialDenormalizeEntity // @ts-ignore HydraNormalizer.denormalizeEnum = initialDenormalizeEnum // @ts-ignore HydraNormalizer.parseEntityIRI = initialParseEntityIRI console.error = initialConsoleError }) test('With provided model', () => { const item = { '@id': '/api/dummyModel/1', id: 1, name: 'foo', } const model = DummyApiModel const expected = new DummyApiModel(item) // @ts-ignore HydraNormalizer.denormalizeEntity = vi.fn((model, item) => { return expected }) // @ts-ignore const result = HydraNormalizer.denormalizeItem(item, model) expect(result).toEqual(expected) // @ts-ignore expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith(model, item) }) test('with no @id prop', () => { const item = { id: 1, name: 'foo', } console.error = vi.fn((msg) => {}) // @ts-ignore const result = HydraNormalizer.denormalizeItem(item) expect(result).toEqual(item) expect(console.error).toHaveBeenCalledWith( 'De-normalization error : the item is not hydra formatted', item, ) }) test('with enum', () => { const item = { '@id': '/api/enum/abc', A: 1, B: 2, C: 3, } // @ts-ignore HydraNormalizer.denormalizeEnum = vi.fn((item) => { return item }) // @ts-ignore const result = HydraNormalizer.denormalizeItem(item) expect(result).toEqual(item) // @ts-ignore expect(HydraNormalizer.denormalizeEnum).toHaveBeenCalledWith(item) }) test('with unparsable iri', () => { const item = { '@id': 'some_invalid_iri', id: 1, name: 'foo', } // @ts-ignore HydraNormalizer.parseEntityIRI = vi.fn((iri) => { throw new Error('parsing error') }) console.error = vi.fn((msg) => {}) // @ts-ignore const result = HydraNormalizer.denormalizeItem(item) expect(result).toEqual(item) // @ts-ignore expect(console.error).toHaveBeenCalledWith( 'De-normalization error : could not parse the IRI', item, ) }) test('With valid iri and existing model', () => { HydraNormalizer.models = { dummyModel: DummyApiModel, } const item = { '@id': '/api/dummyModel/1', id: 1, name: 'foo', } // @ts-ignore HydraNormalizer.parseEntityIRI = vi.fn((iri) => { return { entity: 'dummyModel' } }) const expected = new DummyApiModel(item) // @ts-ignore HydraNormalizer.denormalizeEntity = vi.fn((model, item) => { return expected }) // @ts-ignore const result = HydraNormalizer.denormalizeItem(item) expect(result).toEqual(expected) // @ts-ignore expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith( DummyApiModel, item, ) }) test('With valid iri and un-existing model', () => { HydraNormalizer.models = { dummyModel: DummyApiModel, } const item = { '@id': '/api/unknownModel/1', id: 1, name: 'foo', } // @ts-ignore HydraNormalizer.parseEntityIRI = vi.fn((iri) => { return 'unknownModel' }) expect( // @ts-ignore () => HydraNormalizer.denormalizeItem(item), ).toThrowError() }) }) describe('denormalizeEntity', () => { // @ts-ignore const initialGetIdFromEntityIriMethod = HydraNormalizer.getIdFromEntityIri afterEach(() => { // @ts-ignore HydraNormalizer.getIdFromEntityIri = initialGetIdFromEntityIriMethod }) test('should denormalize a Json object into an entity', () => { const data = { id: 7351, name: null, oneToOneRelation: '/api/dummyChild/99', oneToManyRelation: [ '/api/dummyChild/123', '/api/dummyChild/124', '/api/dummyChild/125', ], } // @ts-ignore HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => { return { '/api/dummyChild/99': 99, '/api/dummyChild/123': 123, '/api/dummyChild/124': 124, '/api/dummyChild/125': 125, }[iri] }) // @ts-ignore const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data) expect(result).toStrictEqual( new DummyApiModel({ id: 7351, name: null, oneToOneRelation: 99, oneToManyRelation: [123, 124, 125], }), ) }) test('should ignore relations fields when missing in the incoming data', () => { const data = { id: 7351, name: 'Bob', oneToManyRelation: [ '/api/dummyChild/123', '/api/dummyChild/124', '/api/dummyChild/125', ], } // @ts-ignore HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => { return { '/api/dummyChild/123': 123, '/api/dummyChild/124': 124, '/api/dummyChild/125': 125, }[iri] }) // @ts-ignore const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data) expect(result).toStrictEqual( new DummyApiModel({ id: 7351, name: 'Bob', oneToOneRelation: null, oneToManyRelation: [123, 124, 125], }), ) }) test('should handle relations with empty values', () => { const data = { id: 7351, name: null, oneToOneRelation: null, oneToManyRelation: [], } // @ts-ignore HydraNormalizer.getIriEncodedFields = vi.fn((entity) => { return { oneToOneRelation: DummyApiChild, oneToManyRelation: DummyApiChild, } }) // @ts-ignore const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data) expect(result).toStrictEqual( new DummyApiModel({ id: 7351, name: null, oneToOneRelation: null, oneToManyRelation: [], }), ) }) }) describe('denormalizeEnum', () => { test('does nothing', () => { const item = { '@id': '/api/enum/abc', A: 1, B: 2, C: 3, } // @ts-ignore expect(HydraNormalizer.denormalizeEnum(item)).toStrictEqual(item) }) }) describe('parseEntityIRI', () => { test('valid iri', () => { const iri = '/api/someEntity/456' const expected = { entity: 'someEntity', id: 456, } // @ts-ignore expect(HydraNormalizer.parseEntityIRI(iri)).toStrictEqual(expected) }) test('invalid iri', () => { const iri = 'some_invalid_iri' // @ts-ignore expect(() => HydraNormalizer.parseEntityIRI(iri)).toThrowError( 'could not parse the IRI : "some_invalid_iri"', ) }) }) describe('getIdFromEntityIri', () => { // @ts-ignore const initialParseEntityIRI = HydraNormalizer.parseEntityIRI afterEach(() => { // @ts-ignore HydraNormalizer.parseEntityIRI = initialParseEntityIRI }) test('valid iri', () => { const iri = '/api/someEntity/456' // @ts-ignore const result = HydraNormalizer.getIdFromEntityIri(iri, 'someEntity') expect(result).toEqual(456) }) test('entity not matching', () => { const iri = '/api/someEntity/456' // @ts-ignore expect( // @ts-ignore () => HydraNormalizer.getIdFromEntityIri(iri, 'otherEntity'), ).toThrowError( "IRI entity does not match the field's target entity (someEntity != otherEntity)", ) }) })