HATEOAS-first backend framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
5.2 KiB

  1. import {describe, it, expect, vi, Mock, beforeAll, beforeEach} from 'vitest';
  2. import { readFile, writeFile } from 'fs/promises';
  3. import { JsonLinesDataSource } from '../src';
  4. import { resource, validation as v } from '@modal-sh/yasumi';
  5. import {DataSource} from '@modal-sh/yasumi/dist/types/backend';
  6. vi.mock('fs/promises');
  7. describe('prepareResource', () => {
  8. beforeAll(() => {
  9. const mockWriteFile = writeFile as Mock;
  10. mockWriteFile.mockImplementation(() => { /* noop */ })
  11. });
  12. it('works', () => {
  13. const schema = v.object({});
  14. const r = resource(schema);
  15. const ds = new JsonLinesDataSource<typeof schema>();
  16. expect(() => ds.prepareResource(r)).not.toThrow();
  17. });
  18. });
  19. const toJsonl = (dummyItems: unknown[]) => dummyItems.map((i) => JSON.stringify(i)).join('\n')
  20. describe('methods', () => {
  21. const dummyItems = [
  22. {
  23. id: 1,
  24. name: 'foo',
  25. },
  26. {
  27. id: 2,
  28. name: 'bar',
  29. },
  30. {
  31. id: 3,
  32. name: 'baz',
  33. },
  34. ];
  35. const schema = v.object({
  36. name: v.string(),
  37. });
  38. let ds: DataSource<v.Output<typeof schema>>;
  39. let mockGenerationStrategy: Mock;
  40. beforeEach(() => {
  41. mockGenerationStrategy = vi.fn();
  42. const r = resource(schema)
  43. .id('id', {
  44. generationStrategy: mockGenerationStrategy,
  45. schema: v.any(),
  46. serialize: (id) => id.toString(),
  47. deserialize: (id) => Number(id.toString()),
  48. });
  49. ds = new JsonLinesDataSource<typeof schema>();
  50. ds.prepareResource(r);
  51. });
  52. beforeEach(() => {
  53. const mockReadFile = readFile as Mock;
  54. mockReadFile.mockReturnValueOnce(toJsonl(dummyItems));
  55. });
  56. let mockWriteFile: Mock;
  57. beforeEach(() => {
  58. mockWriteFile = writeFile as Mock;
  59. mockWriteFile.mockImplementationOnce(() => { /* noop */ });
  60. });
  61. describe('initialize', () => {
  62. it('works', async () => {
  63. try {
  64. await ds.initialize();
  65. } catch {
  66. expect.fail('Could not initialize data source.');
  67. }
  68. });
  69. });
  70. describe('operations', () => {
  71. beforeEach(async () => {
  72. await ds.initialize();
  73. });
  74. describe('getTotalCount', () => {
  75. it('works', async () => {
  76. if (typeof ds.getTotalCount !== 'function') {
  77. return;
  78. }
  79. const totalCount = await ds.getTotalCount();
  80. expect(totalCount).toBe(dummyItems.length);
  81. });
  82. });
  83. describe('getMultiple', () => {
  84. it('works', async () => {
  85. const items = await ds.getMultiple();
  86. expect(items).toEqual(dummyItems);
  87. });
  88. });
  89. describe('getById', () => {
  90. it('works', async () => {
  91. const item = await ds.getById('2'); // ID is always a string because it originates from URLs
  92. const expected = dummyItems.find((i) => i.id === 2);
  93. expect(item).toEqual(expected);
  94. });
  95. });
  96. describe('getSingle', () => {
  97. it('works', async () => {
  98. if (typeof ds.getSingle !== 'function') {
  99. // skip if data source doesn't offer this
  100. return;
  101. }
  102. const item = await ds.getSingle();
  103. const expected = dummyItems[0];
  104. expect(item).toEqual(expected);
  105. });
  106. });
  107. describe('create', () => {
  108. it('works', async () => {
  109. const data = {
  110. // notice we don't have IDs here, as it is expected to be generated by newId()
  111. name: 'foo'
  112. };
  113. const newItem = await ds.create(data);
  114. expect(mockWriteFile).toBeCalledWith(
  115. expect.any(String),
  116. toJsonl([
  117. ...dummyItems,
  118. data
  119. ])
  120. );
  121. expect(newItem).toEqual(data);
  122. });
  123. });
  124. describe('delete', () => {
  125. it('works', async () => {
  126. await ds.delete('1');
  127. expect(mockWriteFile).toBeCalledWith(
  128. expect.any(String),
  129. toJsonl(dummyItems.filter((d) => d.id !== 1)),
  130. );
  131. });
  132. });
  133. describe('emplace', () => {
  134. it('works', async () => {
  135. const data = {
  136. id: 2,
  137. name: 'foo',
  138. };
  139. const { id, ...etcData } = data;
  140. const newItem = await ds.emplace(
  141. id.toString(),
  142. etcData,
  143. );
  144. expect(mockWriteFile).toBeCalledWith(
  145. expect.any(String),
  146. toJsonl(dummyItems.map((d) =>
  147. d.id === 2
  148. // ID will be defined last, since we are just writing to file, we need strict ordering
  149. ? { ...etcData, id }
  150. : d
  151. )),
  152. );
  153. expect(newItem).toEqual([data, false]);
  154. });
  155. });
  156. describe('patch', () => {
  157. it('works', async () => {
  158. const data = {
  159. id: 2,
  160. name: 'foo',
  161. };
  162. const { id, ...etcData } = data;
  163. const newItem = await ds.emplace(
  164. id.toString(),
  165. etcData,
  166. );
  167. expect(mockWriteFile).toBeCalledWith(
  168. expect.any(String),
  169. toJsonl(dummyItems.map((d) =>
  170. d.id === 2
  171. ? { ...etcData, id }
  172. : d
  173. )),
  174. );
  175. expect(newItem).toBeDefined();
  176. });
  177. });
  178. describe('newId', () => {
  179. it('works', async () => {
  180. const v = 'newId';
  181. mockGenerationStrategy.mockResolvedValueOnce(v);
  182. const id = await ds.newId();
  183. expect(id).toBe(v);
  184. });
  185. });
  186. });
  187. });