hydraNormalizer.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. import { vi, describe, test, it, expect, afterEach } from 'vitest'
  2. import { Str, Uid, Attr } from 'pinia-orm/dist/decorators'
  3. import type { AnyJson } from '~/types/data'
  4. import HydraNormalizer from '~/services/data/normalizer/hydraNormalizer'
  5. import { METADATA_TYPE } from '~/types/enum/data'
  6. import ApiModel from '~/models/ApiModel'
  7. import { IriEncoded } from '~/models/decorators'
  8. import UrlUtils from '~/services/utils/urlUtils'
  9. import ApiResource from '~/models/ApiResource'
  10. class DummyApiChild extends ApiModel {
  11. static override entity = 'dummyChild'
  12. @Uid()
  13. declare id: number | string
  14. }
  15. class DummyApiModel extends ApiModel {
  16. static override entity = 'dummyModel'
  17. @Uid()
  18. declare id: number | string
  19. @Attr(null)
  20. @IriEncoded(DummyApiChild)
  21. declare oneToOneRelation: number | null
  22. @Attr([])
  23. @IriEncoded(DummyApiChild)
  24. declare oneToManyRelation: []
  25. @Str(null)
  26. declare name: null
  27. }
  28. afterEach(() => {
  29. vi.clearAllMocks()
  30. vi.resetAllMocks()
  31. })
  32. describe('normalize', () => {
  33. const initialMakeUriMethod = UrlUtils.makeIRI
  34. afterEach(() => {
  35. UrlUtils.makeIRI = initialMakeUriMethod
  36. })
  37. test('should normalize an entity into a JSON Object', () => {
  38. const entity: DummyApiModel = new DummyApiModel({
  39. id: 7351,
  40. oneToOneRelation: 99,
  41. oneToManyRelation: [123, 124, 125],
  42. })
  43. // @ts-ignore
  44. UrlUtils.makeIRI = vi.fn((targetEntity, id) => {
  45. return {
  46. 99: '/api/dummyChild/99',
  47. 123: '/api/dummyChild/123',
  48. 124: '/api/dummyChild/124',
  49. 125: '/api/dummyChild/125',
  50. }[id]
  51. })
  52. const result = HydraNormalizer.normalizeEntity(entity)
  53. const expected = {
  54. id: 7351,
  55. name: null,
  56. oneToOneRelation: '/api/dummyChild/99',
  57. oneToManyRelation: [
  58. '/api/dummyChild/123',
  59. '/api/dummyChild/124',
  60. '/api/dummyChild/125',
  61. ],
  62. }
  63. expect(result).toStrictEqual(expected)
  64. })
  65. })
  66. describe('denormalize', () => {
  67. // @ts-ignore
  68. const initialGetDataMethod = HydraNormalizer.getData
  69. // @ts-ignore
  70. const initialGetMetadataMethod = HydraNormalizer.getMetadata
  71. afterEach(() => {
  72. // @ts-ignore
  73. HydraNormalizer.getData = initialGetDataMethod
  74. // @ts-ignore
  75. HydraNormalizer.getMetadata = initialGetMetadataMethod
  76. })
  77. test('should parse a API Item response and return a JSON Object', () => {
  78. const data: AnyJson = { id: 1, name: 'foo' }
  79. // @ts-ignore
  80. HydraNormalizer.getData = vi.fn((data, model) => {
  81. return data
  82. })
  83. // @ts-ignore
  84. HydraNormalizer.getMetadata = vi.fn((data, model) => {
  85. return { type: METADATA_TYPE.ITEM }
  86. })
  87. const result = HydraNormalizer.denormalize(data, DummyApiModel)
  88. const expected = {
  89. data: {
  90. id: 1,
  91. name: 'foo',
  92. },
  93. metadata: {
  94. type: METADATA_TYPE.ITEM,
  95. },
  96. }
  97. expect(result).toEqual(expected)
  98. })
  99. })
  100. describe('getData', () => {
  101. test('With collection', () => {
  102. const data = {
  103. '@context': '/api/contexts/Foo',
  104. '@id': '/api/foo',
  105. '@type': 'Collection',
  106. member: ['foo'],
  107. }
  108. // @ts-ignore
  109. expect(HydraNormalizer.getData(data)).toEqual(['foo'])
  110. })
  111. test('With item', () => {
  112. const data = {
  113. '@context': '/api/contexts/Foo',
  114. '@id': '/api/foo',
  115. '@type': 'Foo',
  116. param1: 'a',
  117. }
  118. // @ts-ignore
  119. expect(HydraNormalizer.getData(data)).toEqual(data)
  120. })
  121. })
  122. describe('getMetadata', () => {
  123. test('With valid collection metadata', () => {
  124. const data = {
  125. '@context': '/api/contexts/Foo',
  126. '@id': '/api/foo',
  127. '@type': 'hydra:Collection',
  128. 'hydra:member': ['foo'],
  129. 'hydra:totalItems': 10,
  130. 'hydra:view': {
  131. '@id': '/api/foo?page=3',
  132. '@type': 'hydra:PartialCollectionView',
  133. 'hydra:first': '/api/foo?page=1',
  134. 'hydra:last': '/api/foo?page=5',
  135. 'hydra:next': '/api/foo?page=4',
  136. 'hydra:previous': '/api/foo?page=2',
  137. },
  138. }
  139. // @ts-ignore
  140. const metadata = HydraNormalizer.getMetadata(data)
  141. expect(metadata.totalItems).toEqual(10)
  142. expect(metadata.first).toEqual(1)
  143. expect(metadata.last).toEqual(5)
  144. expect(metadata.next).toEqual(4)
  145. expect(metadata.previous).toEqual(2)
  146. expect(metadata.type).toEqual(METADATA_TYPE.COLLECTION)
  147. })
  148. test('With partial collection metadata', () => {
  149. const data = {
  150. '@context': '/api/contexts/Foo',
  151. '@id': '/api/foo',
  152. '@type': 'hydra:Collection',
  153. 'hydra:member': ['foo'],
  154. 'hydra:totalItems': 10,
  155. 'hydra:view': {
  156. '@id': '/api/foo?page=3',
  157. '@type': 'hydra:PartialCollectionView',
  158. 'hydra:first': '/api/foo?page=1',
  159. },
  160. }
  161. // @ts-ignore
  162. const metadata = HydraNormalizer.getMetadata(data)
  163. expect(metadata.totalItems).toEqual(10)
  164. expect(metadata.first).toEqual(1)
  165. expect(metadata.last).toEqual(1)
  166. expect(metadata.next).toEqual(undefined)
  167. expect(metadata.previous).toEqual(undefined)
  168. })
  169. test('With item metadata', () => {
  170. // @ts-ignore
  171. const metadata = HydraNormalizer.getMetadata({})
  172. expect(metadata.type).toEqual(METADATA_TYPE.ITEM)
  173. })
  174. })
  175. describe('denormalizeItem', () => {
  176. // @ts-ignore
  177. const initialDenormalizeEntity = HydraNormalizer.denormalizeEntity
  178. // @ts-ignore
  179. const initialDenormalizeEnum = HydraNormalizer.denormalizeEnum
  180. // @ts-ignore
  181. const initialParseEntityIRI = HydraNormalizer.parseEntityIRI
  182. const initialConsoleError = console.error
  183. afterEach(() => {
  184. // @ts-ignore
  185. HydraNormalizer.denormalizeEntity = initialDenormalizeEntity
  186. // @ts-ignore
  187. HydraNormalizer.denormalizeEnum = initialDenormalizeEnum
  188. // @ts-ignore
  189. HydraNormalizer.parseEntityIRI = initialParseEntityIRI
  190. console.error = initialConsoleError
  191. })
  192. test('With provided model', () => {
  193. const item = {
  194. '@id': '/api/dummyModel/1',
  195. id: 1,
  196. name: 'foo',
  197. }
  198. const model = DummyApiModel
  199. const expected = new DummyApiModel(item)
  200. // @ts-ignore
  201. HydraNormalizer.denormalizeEntity = vi.fn((model, item) => {
  202. return expected
  203. })
  204. // @ts-ignore
  205. const result = HydraNormalizer.denormalizeItem(item, model)
  206. expect(result).toEqual(expected)
  207. // @ts-ignore
  208. expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith(model, item)
  209. })
  210. test('with no @id prop', () => {
  211. const item = {
  212. id: 1,
  213. name: 'foo',
  214. }
  215. console.error = vi.fn((msg) => {})
  216. // @ts-ignore
  217. const result = HydraNormalizer.denormalizeItem(item)
  218. expect(result).toEqual(item)
  219. expect(console.error).toHaveBeenCalledWith(
  220. 'De-normalization error : the item is not hydra formatted',
  221. item,
  222. )
  223. })
  224. test('with enum', () => {
  225. const item = {
  226. '@id': '/api/enum/abc',
  227. A: 1,
  228. B: 2,
  229. C: 3,
  230. }
  231. // @ts-ignore
  232. HydraNormalizer.denormalizeEnum = vi.fn((item) => {
  233. return item
  234. })
  235. // @ts-ignore
  236. const result = HydraNormalizer.denormalizeItem(item)
  237. expect(result).toEqual(item)
  238. // @ts-ignore
  239. expect(HydraNormalizer.denormalizeEnum).toHaveBeenCalledWith(item)
  240. })
  241. test('with unparsable iri', () => {
  242. const item = {
  243. '@id': 'some_invalid_iri',
  244. id: 1,
  245. name: 'foo',
  246. }
  247. // @ts-ignore
  248. HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
  249. throw new Error('parsing error')
  250. })
  251. console.error = vi.fn((msg) => {})
  252. // @ts-ignore
  253. const result = HydraNormalizer.denormalizeItem(item)
  254. expect(result).toEqual(item)
  255. // @ts-ignore
  256. expect(console.error).toHaveBeenCalledWith(
  257. 'De-normalization error : could not parse the IRI',
  258. item,
  259. )
  260. })
  261. test('With valid iri and existing model', () => {
  262. //@ts-expect-error problème de typage sans conséquence
  263. HydraNormalizer.models = { dummyModel: DummyApiModel }
  264. const item = {
  265. '@id': '/api/dummyModel/1',
  266. id: 1,
  267. name: 'foo',
  268. }
  269. // @ts-ignore
  270. HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
  271. return { entity: 'dummyModel' }
  272. })
  273. const expected = new DummyApiModel(item)
  274. // @ts-ignore
  275. HydraNormalizer.denormalizeEntity = vi.fn((model, item) => {
  276. return expected
  277. })
  278. // @ts-ignore
  279. const result = HydraNormalizer.denormalizeItem(item)
  280. expect(result).toEqual(expected)
  281. // @ts-ignore
  282. expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith(
  283. DummyApiModel,
  284. item,
  285. )
  286. })
  287. test('With valid iri and un-existing model', () => {
  288. //@ts-expect-error problème de typage sans conséquence
  289. HydraNormalizer.models = { dummyModel: DummyApiModel }
  290. const item = {
  291. '@id': '/api/unknownModel/1',
  292. id: 1,
  293. name: 'foo',
  294. }
  295. // @ts-ignore
  296. HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
  297. return 'unknownModel'
  298. })
  299. expect(
  300. // @ts-ignore
  301. () => HydraNormalizer.denormalizeItem(item),
  302. ).toThrowError()
  303. })
  304. })
  305. describe('denormalizeEntity', () => {
  306. // @ts-ignore
  307. const initialGetIdFromEntityIriMethod = HydraNormalizer.getIdFromEntityIri
  308. afterEach(() => {
  309. // @ts-ignore
  310. HydraNormalizer.getIdFromEntityIri = initialGetIdFromEntityIriMethod
  311. })
  312. test('should denormalize a Json object into an entity', () => {
  313. const data = {
  314. id: 7351,
  315. name: null,
  316. oneToOneRelation: '/api/dummyChild/99',
  317. oneToManyRelation: [
  318. '/api/dummyChild/123',
  319. '/api/dummyChild/124',
  320. '/api/dummyChild/125',
  321. ],
  322. }
  323. // @ts-ignore
  324. HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
  325. return {
  326. '/api/dummyChild/99': 99,
  327. '/api/dummyChild/123': 123,
  328. '/api/dummyChild/124': 124,
  329. '/api/dummyChild/125': 125,
  330. }[iri]
  331. })
  332. // @ts-ignore
  333. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  334. expect(result).toStrictEqual(
  335. new DummyApiModel({
  336. id: 7351,
  337. name: null,
  338. oneToOneRelation: 99,
  339. oneToManyRelation: [123, 124, 125],
  340. }),
  341. )
  342. })
  343. test('should ignore relations fields when missing in the incoming data', () => {
  344. const data = {
  345. id: 7351,
  346. name: 'Bob',
  347. oneToManyRelation: [
  348. '/api/dummyChild/123',
  349. '/api/dummyChild/124',
  350. '/api/dummyChild/125',
  351. ],
  352. }
  353. // @ts-ignore
  354. HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
  355. return {
  356. '/api/dummyChild/123': 123,
  357. '/api/dummyChild/124': 124,
  358. '/api/dummyChild/125': 125,
  359. }[iri]
  360. })
  361. // @ts-ignore
  362. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  363. expect(result).toStrictEqual(
  364. new DummyApiModel({
  365. id: 7351,
  366. name: 'Bob',
  367. oneToOneRelation: null,
  368. oneToManyRelation: [123, 124, 125],
  369. }),
  370. )
  371. })
  372. test('should handle relations with empty values', () => {
  373. const data = {
  374. id: 7351,
  375. name: null,
  376. oneToOneRelation: null,
  377. oneToManyRelation: [],
  378. }
  379. // @ts-ignore
  380. HydraNormalizer.getIriEncodedFields = vi.fn((entity) => {
  381. return {
  382. oneToOneRelation: DummyApiChild,
  383. oneToManyRelation: DummyApiChild,
  384. }
  385. })
  386. // @ts-ignore
  387. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  388. expect(result).toStrictEqual(
  389. new DummyApiModel({
  390. id: 7351,
  391. name: null,
  392. oneToOneRelation: null,
  393. oneToManyRelation: [],
  394. }),
  395. )
  396. })
  397. })
  398. describe('denormalizeEnum', () => {
  399. test('does nothing', () => {
  400. const item = {
  401. '@id': '/api/enum/abc',
  402. A: 1,
  403. B: 2,
  404. C: 3,
  405. }
  406. // @ts-ignore
  407. expect(HydraNormalizer.denormalizeEnum(item)).toStrictEqual(item)
  408. })
  409. })
  410. describe('parseEntityIRI', () => {
  411. test('valid iri', () => {
  412. const iri = '/api/someEntity/456'
  413. const expected = {
  414. entity: 'someEntity',
  415. id: 456,
  416. }
  417. // @ts-ignore
  418. expect(HydraNormalizer.parseEntityIRI(iri)).toStrictEqual(expected)
  419. })
  420. test('invalid iri', () => {
  421. const iri = 'some_invalid_iri'
  422. // @ts-ignore
  423. expect(() => HydraNormalizer.parseEntityIRI(iri)).toThrowError(
  424. 'could not parse the IRI : "some_invalid_iri"',
  425. )
  426. })
  427. })
  428. describe('getIdFromEntityIri', () => {
  429. // @ts-ignore
  430. const initialParseEntityIRI = HydraNormalizer.parseEntityIRI
  431. afterEach(() => {
  432. // @ts-ignore
  433. HydraNormalizer.parseEntityIRI = initialParseEntityIRI
  434. })
  435. test('valid iri', () => {
  436. const iri = '/api/someEntity/456'
  437. // @ts-ignore
  438. const result = HydraNormalizer.getIdFromEntityIri(iri, 'someEntity')
  439. expect(result).toEqual(456)
  440. })
  441. test('entity not matching', () => {
  442. const iri = '/api/someEntity/456'
  443. // @ts-ignore
  444. expect(
  445. // @ts-ignore
  446. () => HydraNormalizer.getIdFromEntityIri(iri, 'otherEntity'),
  447. ).toThrowError(
  448. "IRI entity does not match the field's target entity (someEntity != otherEntity)",
  449. )
  450. })
  451. })