hydraNormalizer.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  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 entity = 'dummyChild'
  12. @Uid()
  13. declare id: number | string
  14. }
  15. class DummyApiModel extends ApiModel {
  16. static 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': 'hydra:Collection',
  106. 'hydra: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.firstPage).toEqual(1)
  143. expect(metadata.lastPage).toEqual(5)
  144. expect(metadata.nextPage).toEqual(4)
  145. expect(metadata.previousPage).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.firstPage).toEqual(1)
  165. expect(metadata.lastPage).toEqual(1)
  166. expect(metadata.nextPage).toEqual(undefined)
  167. expect(metadata.previousPage).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. HydraNormalizer.models = {
  263. dummyModel: DummyApiModel,
  264. }
  265. const item = {
  266. '@id': '/api/dummyModel/1',
  267. id: 1,
  268. name: 'foo',
  269. }
  270. // @ts-ignore
  271. HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
  272. return { entity: 'dummyModel' }
  273. })
  274. const expected = new DummyApiModel(item)
  275. // @ts-ignore
  276. HydraNormalizer.denormalizeEntity = vi.fn((model, item) => {
  277. return expected
  278. })
  279. // @ts-ignore
  280. const result = HydraNormalizer.denormalizeItem(item)
  281. expect(result).toEqual(expected)
  282. // @ts-ignore
  283. expect(HydraNormalizer.denormalizeEntity).toHaveBeenCalledWith(
  284. DummyApiModel,
  285. item,
  286. )
  287. })
  288. test('With valid iri and un-existing model', () => {
  289. HydraNormalizer.models = {
  290. dummyModel: DummyApiModel,
  291. }
  292. const item = {
  293. '@id': '/api/unknownModel/1',
  294. id: 1,
  295. name: 'foo',
  296. }
  297. // @ts-ignore
  298. HydraNormalizer.parseEntityIRI = vi.fn((iri) => {
  299. return 'unknownModel'
  300. })
  301. expect(
  302. // @ts-ignore
  303. () => HydraNormalizer.denormalizeItem(item),
  304. ).toThrowError()
  305. })
  306. })
  307. describe('denormalizeEntity', () => {
  308. // @ts-ignore
  309. const initialGetIdFromEntityIriMethod = HydraNormalizer.getIdFromEntityIri
  310. afterEach(() => {
  311. // @ts-ignore
  312. HydraNormalizer.getIdFromEntityIri = initialGetIdFromEntityIriMethod
  313. })
  314. test('should denormalize a Json object into an entity', () => {
  315. const data = {
  316. id: 7351,
  317. name: null,
  318. oneToOneRelation: '/api/dummyChild/99',
  319. oneToManyRelation: [
  320. '/api/dummyChild/123',
  321. '/api/dummyChild/124',
  322. '/api/dummyChild/125',
  323. ],
  324. }
  325. // @ts-ignore
  326. HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
  327. return {
  328. '/api/dummyChild/99': 99,
  329. '/api/dummyChild/123': 123,
  330. '/api/dummyChild/124': 124,
  331. '/api/dummyChild/125': 125,
  332. }[iri]
  333. })
  334. // @ts-ignore
  335. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  336. expect(result).toStrictEqual(
  337. new DummyApiModel({
  338. id: 7351,
  339. name: null,
  340. oneToOneRelation: 99,
  341. oneToManyRelation: [123, 124, 125],
  342. }),
  343. )
  344. })
  345. test('should ignore relations fields when missing in the incoming data', () => {
  346. const data = {
  347. id: 7351,
  348. name: 'Bob',
  349. oneToManyRelation: [
  350. '/api/dummyChild/123',
  351. '/api/dummyChild/124',
  352. '/api/dummyChild/125',
  353. ],
  354. }
  355. // @ts-ignore
  356. HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
  357. return {
  358. '/api/dummyChild/123': 123,
  359. '/api/dummyChild/124': 124,
  360. '/api/dummyChild/125': 125,
  361. }[iri]
  362. })
  363. // @ts-ignore
  364. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  365. expect(result).toStrictEqual(
  366. new DummyApiModel({
  367. id: 7351,
  368. name: 'Bob',
  369. oneToOneRelation: null,
  370. oneToManyRelation: [123, 124, 125],
  371. }),
  372. )
  373. })
  374. test('should handle relations with empty values', () => {
  375. const data = {
  376. id: 7351,
  377. name: null,
  378. oneToOneRelation: null,
  379. oneToManyRelation: [],
  380. }
  381. // @ts-ignore
  382. HydraNormalizer.getIriEncodedFields = vi.fn((entity) => {
  383. return {
  384. oneToOneRelation: DummyApiChild,
  385. oneToManyRelation: DummyApiChild,
  386. }
  387. })
  388. // @ts-ignore
  389. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  390. expect(result).toStrictEqual(
  391. new DummyApiModel({
  392. id: 7351,
  393. name: null,
  394. oneToOneRelation: null,
  395. oneToManyRelation: [],
  396. }),
  397. )
  398. })
  399. })
  400. describe('denormalizeEnum', () => {
  401. test('does nothing', () => {
  402. const item = {
  403. '@id': '/api/enum/abc',
  404. A: 1,
  405. B: 2,
  406. C: 3,
  407. }
  408. // @ts-ignore
  409. expect(HydraNormalizer.denormalizeEnum(item)).toStrictEqual(item)
  410. })
  411. })
  412. describe('parseEntityIRI', () => {
  413. test('valid iri', () => {
  414. const iri = '/api/someEntity/456'
  415. const expected = {
  416. entity: 'someEntity',
  417. id: 456,
  418. }
  419. // @ts-ignore
  420. expect(HydraNormalizer.parseEntityIRI(iri)).toStrictEqual(expected)
  421. })
  422. test('invalid iri', () => {
  423. const iri = 'some_invalid_iri'
  424. // @ts-ignore
  425. expect(() => HydraNormalizer.parseEntityIRI(iri)).toThrowError(
  426. 'could not parse the IRI : "some_invalid_iri"',
  427. )
  428. })
  429. })
  430. describe('getIdFromEntityIri', () => {
  431. // @ts-ignore
  432. const initialParseEntityIRI = HydraNormalizer.parseEntityIRI
  433. afterEach(() => {
  434. // @ts-ignore
  435. HydraNormalizer.parseEntityIRI = initialParseEntityIRI
  436. })
  437. test('valid iri', () => {
  438. const iri = '/api/someEntity/456'
  439. // @ts-ignore
  440. const result = HydraNormalizer.getIdFromEntityIri(iri, 'someEntity')
  441. expect(result).toEqual(456)
  442. })
  443. test('entity not matching', () => {
  444. const iri = '/api/someEntity/456'
  445. // @ts-ignore
  446. expect(
  447. // @ts-ignore
  448. () => HydraNormalizer.getIdFromEntityIri(iri, 'otherEntity'),
  449. ).toThrowError(
  450. "IRI entity does not match the field's target entity (someEntity != otherEntity)",
  451. )
  452. })
  453. })