import { describe, test, vi, expect, beforeEach, afterEach } from 'vitest' import { Repository } from 'pinia-orm' import type { Element } from 'pinia-orm' import { Str, Uid } from 'pinia-orm/dist/decorators' import ApiResource from '~/models/ApiResource' import ApiModel from '~/models/ApiModel' import ApiRequestService from '~/services/data/apiRequestService' import EntityManager from '~/services/data/entityManager' class TestableEntityManager extends EntityManager { public removeTempAfterPersist( model: typeof ApiResource, tempInstanceId: number | string, ): void { return super.removeTempAfterPersist(model, tempInstanceId) } } class DummyApiResource extends ApiResource { static entity = 'dummyResource' @Uid() declare id: number | string @Str(null) declare name: string } class DummyApiModel extends ApiModel { static entity = 'dummyModel' @Uid() declare id: number | string @Str(null) declare name: string } const _console: any = { log: console.log, warn: console.warn, error: console.error, } vi.mock('~/models/models', async () => { class MyModel { static entity = 'myModel' } const models: Record = { myModel: MyModel } return { default: models, } }) let apiRequestService: ApiRequestService let entityManager: TestableEntityManager let repo: Repository let _getRepo: (model: typeof ApiResource) => Repository beforeEach(() => { // @ts-ignore repo = vi.fn() as Repository // @ts-ignore apiRequestService = vi.fn() as ApiRequestService _getRepo = vi.fn((model: typeof ApiResource) => repo) entityManager = new TestableEntityManager(apiRequestService, _getRepo) }) afterEach(() => { // Reset console methods after mock console.log = _console.log console.warn = _console.warn console.error = _console.error }) describe('getRepository', () => { test('simple call', () => { entityManager.getRepository(DummyApiResource) expect(_getRepo).toHaveBeenCalledWith(DummyApiResource) }) }) describe('getQuery', () => { test('simple call', () => { // @ts-ignore const repo = vi.fn() // @ts-ignore entityManager.getRepository = vi.fn(() => repo) const query = vi.fn() // @ts-ignore repo.where = vi.fn((method) => { // Query method is supposed to exclude NaN ids values expect(method({ id: 123 })).toBeTruthy() expect(method({ id: 'abc' })).toBeFalsy() return query }) const result = entityManager.getQuery(DummyApiResource) expect(result).toEqual(query) }) }) describe('cast', () => { test('simple cast', () => { // @ts-ignore const result = entityManager.cast(DummyApiResource, { id: 1 }) expect(result instanceof DummyApiResource).toEqual(true) }) }) describe('getModelFor', () => { test('simple call', () => { expect(entityManager.getModelFor('myModel').entity).toEqual('myModel') }) }) describe('getModelFromIri', () => { test('simple call', () => { // @ts-ignore entityManager.getModelFor = vi.fn((entityName: string) => entityName === 'dummy' ? DummyApiResource : null, ) // @ts-ignore const result = entityManager.getModelFromIri('/api/dummy/123') expect(result).toEqual(DummyApiResource) }) test('invalide Iri', () => { expect(() => entityManager.getModelFromIri('/invalid')).toThrowError( 'cannot parse the IRI', ) }) }) describe('newInstance', () => { test('simple call', () => { const properties = { id: 1 } // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore const entity = new DummyApiResource(properties) // @ts-ignore repo.make = vi.fn((properties: object) => { // @ts-ignore entity.id = properties.id return entity }) // @ts-ignore entityManager.save = vi.fn( (model: typeof ApiResource, entity: ApiResource, permanent: boolean) => entity, ) const result = entityManager.newInstance(DummyApiResource, properties) expect(repo.make).toHaveBeenCalledWith(properties) expect(entityManager.save).toHaveBeenCalledWith( DummyApiResource, entity, true, ) expect(result.id).toEqual(properties.id) }) test('with no id provided', () => { const properties = { name: 'bob' } // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore entityManager.saveInitialState = vi.fn( (model: typeof ApiResource, entity: ApiResource) => null, ) // @ts-ignore const entity = new DummyApiResource(properties) // @ts-ignore repo.make = vi.fn((properties: object) => { // @ts-ignore entity.name = properties.name return entity }) // @ts-ignore repo.save = vi.fn((record: Element) => entity) const result = entityManager.newInstance(DummyApiResource, properties) expect(result.id, "id is 'tmp' followed by a valid uuid-V4").toMatch( /tmp[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}/, ) expect(result.name).toEqual(properties.name) }) }) describe('save', () => { test('simple call', () => { // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.save = vi.fn((record: Element) => entity) const entity = new DummyApiResource() entityManager.save(DummyApiResource, entity) expect(repo.save).toHaveBeenCalledWith(entity) }) }) describe('find', () => { test('simple call', () => { // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) const entity = new DummyApiResource({ id: 1 }) // @ts-ignore repo.find = vi.fn((id: string | number) => entity) entityManager.find(DummyApiResource, 1) expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiResource) expect(repo.find).toHaveBeenCalledWith(1) }) }) describe('fetch', () => { test('not in store, no force refresh', async () => { const properties = { id: 1 } const entity = new DummyApiResource({ id: 1 }) // @ts-ignore entityManager.find = vi.fn( (model: typeof ApiResource, id: number) => undefined, ) // @ts-ignore apiRequestService.get = vi.fn(async (url: string) => properties) // @ts-ignore entityManager.newInstance = vi.fn( (model: typeof ApiResource, props: object) => entity, ) const result = await entityManager.fetch(DummyApiResource, 1) expect(entityManager.find).toHaveBeenCalledWith(DummyApiResource, 1) expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource/1') expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, { id: 1, name: null, _model: undefined, }) expect(result).toEqual(entity) }) test('in store, no force refresh', async () => { const properties = { id: 1 } const entity = new DummyApiResource({ id: 1 }) // @ts-ignore entityManager.find = vi.fn( (model: typeof ApiResource, id: number) => entity, ) const result = await entityManager.fetch(DummyApiResource, 1) expect(entityManager.find).toHaveBeenCalledWith(DummyApiResource, 1) expect(result).toEqual(entity) }) test('in store, but with force refresh', async () => { const properties = { id: 1 } const entity = new DummyApiResource({ id: 1 }) // @ts-ignore entityManager.find = vi.fn( (model: typeof ApiResource, id: number) => undefined, ) // @ts-ignore apiRequestService.get = vi.fn(async (url: string) => properties) // @ts-ignore entityManager.newInstance = vi.fn( (model: typeof ApiResource, props: object) => entity, ) const result = await entityManager.fetch(DummyApiResource, 1, true) expect(entityManager.find).toHaveBeenCalledTimes(0) expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource/1') expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, { id: 1, name: null, _model: undefined, }) expect(result).toEqual(entity) }) }) describe('fetchCollection', () => { test('simple call', async () => { const collection = { '@type': 'hydra:Collection', 'hydra:totalItems': 3, 'hydra:member': [{ id: 1 }, { id: 2 }, { id: 3 }], } // @ts-ignore apiRequestService.get = vi.fn(async (url: string) => collection) // @ts-ignore entityManager.newInstance = vi.fn( (model: typeof ApiResource, props: object) => { return new DummyApiResource(props) }, ) const piniaOrmQuery = vi.fn() // @ts-ignore entityManager.getQuery = vi.fn((model: typeof ApiResource) => piniaOrmQuery) const result = await entityManager.fetchCollection(DummyApiResource, null) expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource') expect(entityManager.newInstance).toHaveBeenCalledTimes(3) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, { id: 1, name: null, _model: undefined, }) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, { id: 2, name: null, _model: undefined, }) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, { id: 3, name: null, _model: undefined, }) // @ts-ignore piniaOrmQuery.get = vi.fn(() => [ new DummyApiResource({ id: 1 }), new DummyApiResource({ id: 2 }), new DummyApiResource({ id: 3 }), ]) expect(result.value.items).toEqual([ new DummyApiResource({ id: 1 }), new DummyApiResource({ id: 2 }), new DummyApiResource({ id: 3 }), ]) expect(result.value.pagination, 'default pagination').toEqual({ first: 1, last: 1, next: undefined, previous: undefined, }) }) test('with a parent', async () => { const collection = { '@type': 'hydra:Collection', 'hydra:totalItems': 3, 'hydra:member': [{ id: 1 }, { id: 2 }, { id: 3 }], } // @ts-ignore apiRequestService.get = vi.fn(async (url: string) => collection) const piniaOrmQuery = vi.fn() // @ts-ignore entityManager.getQuery = vi.fn((model: typeof ApiResource) => piniaOrmQuery) // @ts-ignore entityManager.newInstance = vi.fn( (model: typeof ApiResource, props: object) => { return new DummyApiResource(props) }, ) const parent = new DummyApiModel() parent.id = 100 parent.entity = 'dummyModel' // TODO: je ne comprend pas pqoi cette ligne est nécessaire... await entityManager.fetchCollection(DummyApiResource, parent) expect(apiRequestService.get).toHaveBeenCalledWith( 'api/dummyModel/100/dummyResource', ) }) test('with a query', async () => { const collection = { '@type': 'hydra:Collection', 'hydra:totalItems': 3, 'hydra:member': [{ id: 1 }, { id: 2 }, { id: 3 }], } const query = vi.fn() // @ts-ignore query.getUrlQuery = vi.fn(() => 'foo=bar') // @ts-ignore query.applyToPiniaOrmQuery = vi.fn((q) => q) const piniaOrmQuery = vi.fn() // @ts-ignore entityManager.getQuery = vi.fn((model: typeof ApiResource) => piniaOrmQuery) // @ts-ignore apiRequestService.get = vi.fn(async (url: string) => collection) // @ts-ignore entityManager.newInstance = vi.fn( (model: typeof ApiResource, props: object) => { return new DummyApiResource(props) }, ) // @ts-ignore await entityManager.fetchCollection(DummyApiResource, null, query) expect(apiRequestService.get).toHaveBeenCalledWith( 'api/dummyResource?foo=bar', ) // @ts-ignore expect(query.getUrlQuery).toHaveBeenCalledWith() // @ts-ignore expect(query.applyToPiniaOrmQuery).toHaveBeenCalledWith(piniaOrmQuery) }) }) describe('persist', () => { test('new entity (POST)', async () => { const instance = new DummyApiModel({ id: 'tmp1', name: 'bob' }) instance.isNew = vi.fn(() => true) // @ts-ignore instance.$toJson = vi.fn(() => { return { id: 'tmp1', name: 'bob' } }) // @ts-ignore entityManager.cast = vi.fn( (model: typeof ApiResource, entity: ApiResource): ApiResource => entity, ) const response = { id: 1, name: 'bob' } // @ts-ignore apiRequestService.post = vi.fn((url, data) => response) // @ts-ignore entityManager.newInstance = vi.fn((model, response) => { const newEntity = new DummyApiModel(response) // @ts-ignore newEntity.id = response.id // @ts-ignore newEntity.name = response.name return newEntity }) // @ts-ignore entityManager.removeTempAfterPersist = vi.fn() const result = await entityManager.persist(DummyApiModel, instance) // temp id should have been purged from the posted data expect(apiRequestService.post).toHaveBeenCalledWith('api/dummyModel', { name: 'bob', }) expect(entityManager.newInstance).toHaveBeenCalledWith( DummyApiModel, response, ) // @ts-ignore expect(entityManager.removeTempAfterPersist).toHaveBeenCalledWith( DummyApiModel, instance.id, ) expect(result.id).toEqual(1) expect(result.name).toEqual('bob') }) test('existing entity (PUT)', async () => { const props = { id: 1, name: 'bob' } const entity = new DummyApiModel(props) entity.id = 1 entity.isNew = vi.fn(() => false) // @ts-ignore entity.$toJson = vi.fn(() => props) // TODO: attendre de voir si cet appel est nécessaire dans l'entity manager // entityManager.cast = vi.fn((model: typeof ApiResource, entity: ApiResource): ApiResource => entity) // @ts-ignore apiRequestService.put = vi.fn((url, data) => props) // @ts-ignore entityManager.newInstance = vi.fn((model, response) => { const newEntity = new DummyApiModel(response) // @ts-ignore newEntity.id = response.id // @ts-ignore newEntity.name = response.name return newEntity }) // @ts-ignore entityManager.removeTempAfterPersist = vi.fn() const result = await entityManager.persist(DummyApiModel, entity) expect(apiRequestService.put).toHaveBeenCalledWith('api/dummyModel/1', { id: 1, name: 'bob', }) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiModel, props) // @ts-ignore expect(entityManager.removeTempAfterPersist).toHaveBeenCalledTimes(0) expect(result.id).toEqual(1) expect(result.name).toEqual('bob') }) }) describe('patch', () => { test('simple call', async () => { const props = { id: 1, name: 'bobby' } // @ts-ignore apiRequestService.put = vi.fn((url, data) => props) // @ts-ignore entityManager.newInstance = vi.fn((model, response) => { const newEntity = new DummyApiModel(response) // @ts-ignore newEntity.id = response.id // @ts-ignore newEntity.name = response.name return newEntity }) const result = await entityManager.patch(DummyApiModel, 1, { name: 'bobby', }) expect(apiRequestService.put).toHaveBeenCalledWith( 'api/dummyModel/1', '{"name":"bobby"}', ) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiModel, { id: 1, name: 'bobby', }) expect(result.id).toEqual(1) expect(result.name).toEqual('bobby') }) }) describe('delete', () => { test('delete non persisted entity', () => { const entity = new DummyApiModel() entity.isNew = vi.fn(() => true) entity.id = 'tmp123' // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) apiRequestService.delete = vi.fn() // @ts-ignore repo.destroy = vi.fn((id: number) => null) entityManager.delete(DummyApiModel, entity) expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel) expect(apiRequestService.delete).toHaveBeenCalledTimes(0) expect(repo.destroy).toHaveBeenCalledWith('tmp123') }) test('delete persisted entity', async () => { const entity = new DummyApiModel() entity.isNew = vi.fn(() => false) entity.id = 1 // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) // @ts-ignore apiRequestService.delete = vi.fn((id: number) => null) // @ts-ignore repo.destroy = vi.fn((id: number) => null) await entityManager.delete(DummyApiModel, entity) expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel) expect(apiRequestService.delete).toHaveBeenCalledWith('api/dummyModel/1') expect(repo.destroy).toHaveBeenCalledWith(1) }) }) describe('reset', () => { test('simple call', () => { const entity = new DummyApiModel() entity.id = 1 entity.name = 'paul' const initialEntity = new DummyApiModel() initialEntity.id = 1 initialEntity.name = 'serges' // @ts-ignore entityManager.getInitialStateOf = vi.fn( (model: typeof ApiResource, id: string | number) => initialEntity, ) // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) // @ts-ignore repo.save = vi.fn((data: any) => null) const result = entityManager.reset(DummyApiModel, entity) // @ts-ignore expect(entityManager.getInitialStateOf).toHaveBeenCalledWith( DummyApiModel, 1, ) expect(repo.save).toHaveBeenCalledWith(initialEntity) expect(result).toEqual(initialEntity) }) test('no initial state stored', () => { const entity = new DummyApiModel() entity.id = 1 // @ts-ignore entityManager.getInitialStateOf = vi.fn( (model: typeof ApiResource, id: string | number) => null, ) expect(() => entityManager.reset(DummyApiModel, entity)).toThrowError( 'no initial state recorded for this object - abort [dummyModel/1]', ) }) }) describe('flush', () => { test('simple call', () => { // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) repo.flush = vi.fn() entityManager.flush(DummyApiModel) expect(repo.flush).toHaveBeenCalled() }) }) describe('isNewEntity', () => { test('with new entity', () => { const entity = new DummyApiModel() entity.isNew = vi.fn(() => true) // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number) => entity) const result = entityManager.isNewInstance(DummyApiModel, 1) expect(result).toBeTruthy() }) test('with existing entity', () => { const entity = new DummyApiModel() entity.isNew = vi.fn(() => false) // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number) => entity) const result = entityManager.isNewInstance(DummyApiModel, 1) expect(result).toBeFalsy() }) test('non-existing entity', () => { // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiModel ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number) => null) console.error = vi.fn() const result = entityManager.isNewInstance(DummyApiModel, 1) expect(result).toBeFalsy() expect(console.error).toHaveBeenCalledWith('dummyModel/1 does not exist!') }) }) describe('saveInitialState', () => { test('simple call', () => { // @ts-ignore const entity = { id: 1, name: 'bob' } as DummyApiResource // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.save = vi.fn((record: Element) => null) // @ts-ignore entityManager.saveInitialState(DummyApiResource, entity) expect(repo.save).toHaveBeenCalledWith({ id: '_clone_1', name: 'bob' }) expect(entity.id).toEqual(1) }) }) describe('getInitialStateOf', () => { test('with initial state', () => { // @ts-ignore const entity = { id: 1, name: 'bob' } as DummyApiResource // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number | string) => { // @ts-ignore return { id: 1, name: 'robert' } as DummyApiResource }) // @ts-ignore const result = entityManager.getInitialStateOf( DummyApiResource, 1, ) as DummyApiResource expect(repo.find).toHaveBeenCalledWith('_clone_1') expect(result.id).toEqual(1) expect(result.name).toEqual('robert') }) test('without initial state', () => { // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number | string) => null) // @ts-ignore const result = entityManager.getInitialStateOf( DummyApiResource, 1, ) as DummyApiResource expect(repo.find).toHaveBeenCalledWith('_clone_1') expect(result).toEqual(null) }) }) describe('removeTempAfterPersist', () => { test('simple call', () => { // @ts-ignore const entity = new DummyApiResource() entity.id = 'tmp123' entity.isNew = vi.fn(() => true) // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number | string) => entity) // @ts-ignore repo.destroy = vi.fn() // @ts-ignore entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123') expect(repo.destroy).toHaveBeenCalledWith('tmp123') expect(repo.destroy).toHaveBeenCalledWith('_clone_tmp123') }) test('entity is not temporary', () => { // @ts-ignore const entity = new DummyApiResource() entity.id = 1 entity.isNew = vi.fn(() => false) // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number | string) => entity) // @ts-ignore repo.destroy = vi.fn() // @ts-ignore expect(() => entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123'), ).toThrowError('Error: Can not remove a non-temporary model instance') expect(repo.destroy).toHaveBeenCalledTimes(0) }) test('entity does not exist', () => { // @ts-ignore const repo = vi.fn() as Repository // @ts-ignore entityManager.getRepository = vi.fn((model: typeof ApiResource) => { return model === DummyApiResource ? repo : null }) // @ts-ignore repo.find = vi.fn((id: number | string) => null) // @ts-ignore repo.destroy = vi.fn() console.error = vi.fn() // @ts-ignore entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123') expect(repo.destroy).toHaveBeenCalledTimes(0) expect(console.error).toHaveBeenCalledWith( 'dummyResource/tmp123 does not exist!', ) }) })