hydraNormalizer.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  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. // @ts-ignore
  102. const initialDenormalizeItem = HydraNormalizer.denormalizeItem
  103. afterEach(() => {
  104. // @ts-ignore
  105. HydraNormalizer.denormalizeItem = initialDenormalizeItem
  106. })
  107. test('With collection', () => {
  108. const data = {
  109. '@context': '/api/contexts/Foo',
  110. '@id': '/api/foo',
  111. '@type': 'Collection',
  112. member: ['foo'],
  113. }
  114. const model = DummyApiModel
  115. // @ts-ignore
  116. HydraNormalizer.denormalizeItem = vi.fn((data, model) => data)
  117. // @ts-ignore
  118. expect(HydraNormalizer.getData(data, model)).toEqual(['foo'])
  119. })
  120. test('With item', () => {
  121. const data = {
  122. '@context': '/api/contexts/Foo',
  123. '@id': '/api/foo',
  124. '@type': 'Foo',
  125. param1: 'a',
  126. }
  127. const model = DummyApiModel
  128. // @ts-ignore
  129. HydraNormalizer.denormalizeItem = vi.fn((data, model) => data)
  130. // @ts-ignore
  131. expect(HydraNormalizer.getData(data, model)).toEqual(data)
  132. })
  133. })
  134. describe('getMetadata', () => {
  135. test('With valid collection metadata', () => {
  136. const data = {
  137. '@context': '/api/contexts/Foo',
  138. '@id': '/api/foo',
  139. '@type': 'Collection',
  140. member: ['foo'],
  141. totalItems: 10,
  142. view: {
  143. '@id': '/api/foo?page=3',
  144. '@type': 'PartialCollectionView',
  145. first: '/api/foo?page=1',
  146. last: '/api/foo?page=5',
  147. next: '/api/foo?page=4',
  148. previous: '/api/foo?page=2',
  149. },
  150. }
  151. // @ts-ignore
  152. const metadata = HydraNormalizer.getMetadata(data)
  153. expect(metadata.totalItems).toEqual(10)
  154. expect(metadata.first).toEqual(1)
  155. expect(metadata.last).toEqual(5)
  156. expect(metadata.next).toEqual(4)
  157. expect(metadata.previous).toEqual(2)
  158. expect(metadata.type).toEqual(METADATA_TYPE.COLLECTION)
  159. })
  160. test('With partial collection metadata', () => {
  161. const data = {
  162. '@context': '/api/contexts/Foo',
  163. '@id': '/api/foo',
  164. '@type': 'Collection',
  165. member: ['foo'],
  166. totalItems: 10,
  167. view: {
  168. '@id': '/api/foo?page=3',
  169. '@type': 'PartialCollectionView',
  170. first: '/api/foo?page=1',
  171. },
  172. }
  173. // @ts-ignore
  174. const metadata = HydraNormalizer.getMetadata(data)
  175. expect(metadata.totalItems).toEqual(10)
  176. expect(metadata.first).toEqual(1)
  177. expect(metadata.last).toEqual(1)
  178. expect(metadata.next).toEqual(undefined)
  179. expect(metadata.previous).toEqual(undefined)
  180. })
  181. test('With item metadata', () => {
  182. // @ts-ignore
  183. const metadata = HydraNormalizer.getMetadata({})
  184. expect(metadata.type).toEqual(METADATA_TYPE.ITEM)
  185. })
  186. })
  187. describe('denormalizeItem', () => {
  188. // @ts-ignore
  189. const initialDenormalizeEntity = HydraNormalizer.denormalizeEntity
  190. // @ts-ignore
  191. const initialDenormalizeEnum = HydraNormalizer.denormalizeEnum
  192. // @ts-ignore
  193. const initialParseEntityIRI = HydraNormalizer.parseEntityIRI
  194. const initialConsoleError = console.error
  195. afterEach(() => {
  196. // @ts-ignore
  197. HydraNormalizer.denormalizeEntity = initialDenormalizeEntity
  198. // @ts-ignore
  199. HydraNormalizer.denormalizeEnum = initialDenormalizeEnum
  200. // @ts-ignore
  201. HydraNormalizer.parseEntityIRI = initialParseEntityIRI
  202. console.error = initialConsoleError
  203. })
  204. test('With provided model', () => {
  205. const item = {
  206. '@id': '/api/dummyModel/1',
  207. id: 1,
  208. name: 'foo',
  209. }
  210. const model = DummyApiModel
  211. const expected = new DummyApiModel({
  212. '@id': '/api/dummyModel/1',
  213. id: 1,
  214. name: 'foo',
  215. oneToOneRelation: null,
  216. oneToManyRelation: [],
  217. })
  218. // @ts-ignore
  219. const result = HydraNormalizer.denormalizeItem(item, model)
  220. expect(result).toEqual(expected)
  221. })
  222. test('with no @id prop', () => {
  223. const item = {
  224. id: 1,
  225. name: 'foo',
  226. }
  227. // @ts-ignore
  228. const result = HydraNormalizer.denormalizeItem(item)
  229. expect(result).toEqual(item)
  230. })
  231. test('with enum', () => {
  232. const item = {
  233. '@id': '/api/enum/abc',
  234. A: 1,
  235. B: 2,
  236. C: 3,
  237. }
  238. // @ts-ignore
  239. const result = HydraNormalizer.denormalizeItem(item)
  240. expect(result).toEqual(item)
  241. })
  242. test('with unparsable iri', () => {
  243. const item = {
  244. '@id': 'some_invalid_iri',
  245. id: 1,
  246. name: 'foo',
  247. }
  248. expect(
  249. // @ts-ignore
  250. () => HydraNormalizer.denormalizeItem(item),
  251. ).toThrowError()
  252. })
  253. test('With valid iri and existing model', () => {
  254. const item = {
  255. '@id': '/api/dummyModel/1',
  256. id: 1,
  257. name: 'foo',
  258. }
  259. expect(
  260. // @ts-ignore
  261. () => HydraNormalizer.denormalizeItem(item),
  262. ).toThrowError()
  263. })
  264. test('With valid iri and un-existing model', () => {
  265. const item = {
  266. '@id': '/api/unknownModel/1',
  267. id: 1,
  268. name: 'foo',
  269. }
  270. expect(
  271. // @ts-ignore
  272. () => HydraNormalizer.denormalizeItem(item),
  273. ).toThrowError()
  274. })
  275. })
  276. describe('denormalizeEntity', () => {
  277. // @ts-ignore
  278. const initialGetIdFromEntityIriMethod = HydraNormalizer.getIdFromEntityIri
  279. afterEach(() => {
  280. // @ts-ignore
  281. HydraNormalizer.getIdFromEntityIri = initialGetIdFromEntityIriMethod
  282. })
  283. test('should denormalize a Json object into an entity', () => {
  284. const data = {
  285. id: 7351,
  286. name: null,
  287. oneToOneRelation: '/api/dummyChild/99',
  288. oneToManyRelation: [
  289. '/api/dummyChild/123',
  290. '/api/dummyChild/124',
  291. '/api/dummyChild/125',
  292. ],
  293. }
  294. // @ts-ignore
  295. HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
  296. return {
  297. '/api/dummyChild/99': 99,
  298. '/api/dummyChild/123': 123,
  299. '/api/dummyChild/124': 124,
  300. '/api/dummyChild/125': 125,
  301. }[iri]
  302. })
  303. // @ts-ignore
  304. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  305. expect(result).toStrictEqual(
  306. new DummyApiModel({
  307. id: 7351,
  308. name: null,
  309. oneToOneRelation: 99,
  310. oneToManyRelation: [123, 124, 125],
  311. }),
  312. )
  313. })
  314. test('should ignore relations fields when missing in the incoming data', () => {
  315. const data = {
  316. id: 7351,
  317. name: 'Bob',
  318. oneToManyRelation: [
  319. '/api/dummyChild/123',
  320. '/api/dummyChild/124',
  321. '/api/dummyChild/125',
  322. ],
  323. }
  324. // @ts-ignore
  325. HydraNormalizer.getIdFromEntityIri = vi.fn((iri) => {
  326. return {
  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: 'Bob',
  338. oneToOneRelation: null,
  339. oneToManyRelation: [123, 124, 125],
  340. }),
  341. )
  342. })
  343. test('should handle relations with empty values', () => {
  344. const data = {
  345. id: 7351,
  346. name: null,
  347. oneToOneRelation: null,
  348. oneToManyRelation: [],
  349. }
  350. // @ts-ignore
  351. HydraNormalizer.getIriEncodedFields = vi.fn((entity) => {
  352. return {
  353. oneToOneRelation: DummyApiChild,
  354. oneToManyRelation: DummyApiChild,
  355. }
  356. })
  357. // @ts-ignore
  358. const result = HydraNormalizer.denormalizeEntity(DummyApiModel, data)
  359. expect(result).toStrictEqual(
  360. new DummyApiModel({
  361. id: 7351,
  362. name: null,
  363. oneToOneRelation: null,
  364. oneToManyRelation: [],
  365. }),
  366. )
  367. })
  368. })
  369. describe('denormalizeEnum', () => {
  370. test('does nothing', () => {
  371. const item = {
  372. '@id': '/api/enum/abc',
  373. A: 1,
  374. B: 2,
  375. C: 3,
  376. }
  377. // @ts-ignore
  378. expect(HydraNormalizer.denormalizeEnum(item)).toStrictEqual(item)
  379. })
  380. })
  381. describe('parseEntityIRI', () => {
  382. test('valid iri', () => {
  383. const iri = '/api/someEntity/456'
  384. const expected = {
  385. entity: 'someEntity',
  386. id: 456,
  387. }
  388. // @ts-ignore
  389. expect(HydraNormalizer.parseEntityIRI(iri)).toStrictEqual(expected)
  390. })
  391. test('invalid iri', () => {
  392. const iri = 'some_invalid_iri'
  393. // @ts-ignore
  394. expect(() => HydraNormalizer.parseEntityIRI(iri)).toThrowError(
  395. 'could not parse the IRI : "some_invalid_iri"',
  396. )
  397. })
  398. })
  399. describe('getIdFromEntityIri', () => {
  400. // @ts-ignore
  401. const initialParseEntityIRI = HydraNormalizer.parseEntityIRI
  402. afterEach(() => {
  403. // @ts-ignore
  404. HydraNormalizer.parseEntityIRI = initialParseEntityIRI
  405. })
  406. test('valid iri', () => {
  407. const iri = '/api/someEntity/456'
  408. // @ts-ignore
  409. const result = HydraNormalizer.getIdFromEntityIri(iri, 'someEntity')
  410. expect(result).toEqual(456)
  411. })
  412. test('entity not matching', () => {
  413. const iri = '/api/someEntity/456'
  414. // @ts-ignore
  415. expect(
  416. // @ts-ignore
  417. () => HydraNormalizer.getIdFromEntityIri(iri, 'otherEntity'),
  418. ).toThrowError(
  419. "IRI entity does not match the field's target entity (someEntity != otherEntity)",
  420. )
  421. })
  422. })