import { describe, test, it, expect } from 'vitest' import EntityManager from "~/services/data/entityManager"; import ApiResource from "~/models/ApiResource"; import ApiModel from "~/models/ApiModel"; import ApiRequestService from "~/services/data/apiRequestService"; import {Element, Repository} from "pinia-orm"; import models from "~/models/models"; class DummyApiResource extends ApiResource { static entity = 'dummyResource' } class DummyApiModel extends ApiModel { static entity = 'dummyModel' } let _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: EntityManager 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 EntityManager(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('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) entity.setModel = vi.fn((model: typeof ApiResource) => null) // @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, properties) 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, properties) 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 result = await entityManager.fetchCollection(DummyApiResource, null) expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource', null) expect(entityManager.newInstance).toHaveBeenCalledTimes(3) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 1}) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 2}) expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 3}) expect(result.items).toEqual([ new DummyApiResource({id: 1}), new DummyApiResource({id: 2}), new DummyApiResource({id: 3}) ]) expect(result.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) // @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', null) }) test('with a query', 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) }) await entityManager.fetchCollection(DummyApiResource, null, { page: 10 }) expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource', { page: 10 }) }) test('with pagination', async () => { const collection = { '@type': 'hydra:Collection', 'hydra:totalItems': 1000, 'hydra:member': [], 'hydra:view': { "@id": "/api/subdomains?organization=498&page=50", 'hydra:first': '/api/subdomains?organization=498&page=1', 'hydra:last': '/api/subdomains?organization=498&page=100', 'hydra:next': '/api/subdomains?organization=498&page=51', 'hydra:previous': '/api/subdomains?organization=498&page=49' } } // @ts-ignore apiRequestService.get = vi.fn(async (url: string) => collection) const result = await entityManager.fetchCollection(DummyApiResource, null) expect(result.totalItems).toEqual(1000) expect(result.pagination.first).toEqual(1) expect(result.pagination.last).toEqual(100) expect(result.pagination.previous).toEqual(49) expect(result.pagination.next).toEqual(51) }) }) 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'} }) // 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) 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!') }) })