HATEOAS-first backend framework.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

234 строки
6.0 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. 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()),
  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. data
  121. ])
  122. );
  123. expect(newItem).toEqual(data);
  124. });
  125. });
  126. describe('delete', () => {
  127. it('works', async () => {
  128. await ds.delete('1');
  129. expect(mockWriteFile).toBeCalledWith(
  130. expect.any(String),
  131. toJsonl(dummyItems.filter((d) => d[ID_ATTR] !== 1)),
  132. );
  133. });
  134. });
  135. describe('emplace', () => {
  136. it('replaces existing data', async () => {
  137. const data = {
  138. [ID_ATTR]: 2,
  139. name: 'foo',
  140. };
  141. const { id, ...etcData } = data;
  142. const newItem = await ds.emplace(
  143. id.toString(),
  144. etcData,
  145. );
  146. expect(mockWriteFile).toBeCalledWith(
  147. expect.any(String),
  148. toJsonl(dummyItems.map((d) =>
  149. d[ID_ATTR] === data[ID_ATTR]
  150. // ID will be defined first, since we are just writing to file, we need strict ordering
  151. ? { [ID_ATTR]: id, ...etcData }
  152. : d
  153. )),
  154. );
  155. expect(newItem).toEqual([data, false]);
  156. });
  157. it('creates new data', async () => {
  158. const data = {
  159. [ID_ATTR]: 4,
  160. name: 'quux',
  161. };
  162. const { [ID_ATTR]: 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([
  170. ...dummyItems,
  171. data
  172. ]),
  173. );
  174. expect(newItem).toEqual([data, true]);
  175. });
  176. });
  177. describe('patch', () => {
  178. it('works', async () => {
  179. const data = {
  180. [ID_ATTR]: 2,
  181. name: 'foo',
  182. };
  183. const { id, ...etcData } = data;
  184. const newItem = await ds.emplace(
  185. id.toString(),
  186. etcData,
  187. );
  188. expect(mockWriteFile).toBeCalledWith(
  189. expect.any(String),
  190. toJsonl(dummyItems.map((d) =>
  191. d[ID_ATTR] === data[ID_ATTR]
  192. // ID will be defined first, since we are just writing to file, we need strict ordering
  193. ? { [ID_ATTR]: id, ...etcData }
  194. : d
  195. )),
  196. );
  197. expect(newItem).toBeDefined();
  198. });
  199. });
  200. describe('newId', () => {
  201. it('works', async () => {
  202. const v = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
  203. mockGenerationStrategy.mockResolvedValueOnce(v);
  204. const id = await ds.newId();
  205. expect(id).toBe(v);
  206. });
  207. });
  208. });
  209. });