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.

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