parse-rest-client.test.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import assert from 'node:assert/strict';
  2. import test from 'node:test';
  3. import { ParseRestClient, ParseRestError } from '../src/db/parse-rest.client.js';
  4. test('Parse REST client keeps the master key server-side and encodes structured queries', async () => {
  5. const calls: Array<{ url: string; init: RequestInit }> = [];
  6. const fetchMock: typeof fetch = async (input, init = {}) => {
  7. calls.push({ url: String(input), init });
  8. return new Response(JSON.stringify({ results: [] }), {
  9. status: 200,
  10. headers: { 'Content-Type': 'application/json' },
  11. });
  12. };
  13. const client = new ParseRestClient({
  14. serverUrl: 'https://parse.example.test/parse/',
  15. appId: 'app-id',
  16. masterKey: 'master-secret',
  17. }, fetchMock);
  18. await client.find('VocProduct', {
  19. where: { workspaceId: 'demashi', platform: 'jd' },
  20. order: 'productKey',
  21. limit: 25,
  22. });
  23. await client.request('/health', { master: false });
  24. const queryUrl = new URL(calls[0]!.url);
  25. assert.equal(queryUrl.pathname, '/parse/classes/VocProduct');
  26. assert.deepEqual(JSON.parse(queryUrl.searchParams.get('where')!), { workspaceId: 'demashi', platform: 'jd' });
  27. assert.equal(queryUrl.searchParams.get('order'), 'productKey');
  28. assert.equal(queryUrl.searchParams.get('limit'), '25');
  29. assert.equal((calls[0]!.init.headers as Record<string, string>)['X-Parse-Master-Key'], 'master-secret');
  30. assert.equal((calls[0]!.init.headers as Record<string, string>)['Cache-Control'], 'no-cache, no-store, max-age=0');
  31. assert.equal((calls[0]!.init.headers as Record<string, string>).Pragma, 'no-cache');
  32. assert.equal('X-Parse-Master-Key' in (calls[1]!.init.headers as Record<string, string>), false);
  33. });
  34. test('Parse REST batch requests stay under the upstream limit and preserve the mount path', async () => {
  35. let requestBody: { requests: Array<{ path: string }> } | null = null;
  36. const fetchMock: typeof fetch = async (_input, init = {}) => {
  37. requestBody = JSON.parse(String(init.body)) as { requests: Array<{ path: string }> };
  38. return new Response(JSON.stringify([{ success: { objectId: 'one' } }]), {
  39. status: 200,
  40. headers: { 'Content-Type': 'application/json' },
  41. });
  42. };
  43. const client = new ParseRestClient({
  44. serverUrl: 'https://parse.example.test/custom/parse',
  45. appId: 'app-id',
  46. masterKey: 'master-secret',
  47. }, fetchMock);
  48. await client.batch([{ method: 'POST', path: '/classes/VocWorkspace', body: { publicId: 'demashi' } }]);
  49. assert.equal(requestBody!.requests[0]!.path, '/custom/parse/classes/VocWorkspace');
  50. await assert.rejects(
  51. () => client.batch(Array.from({ length: 51 }, () => ({ method: 'DELETE' as const, path: '/classes/VocProduct/id' }))),
  52. /limited to 50/,
  53. );
  54. });
  55. test('Parse REST errors expose status and Parse code without returning raw response text', async () => {
  56. const client = new ParseRestClient({
  57. serverUrl: 'https://parse.example.test/parse',
  58. appId: 'app-id',
  59. masterKey: 'master-secret',
  60. }, async () => new Response(JSON.stringify({ code: 119, error: 'Permission denied' }), {
  61. status: 400,
  62. headers: { 'Content-Type': 'application/json' },
  63. }));
  64. await assert.rejects(
  65. () => client.find('VocWorkspace'),
  66. (error: unknown) => error instanceof ParseRestError
  67. && error.status === 400
  68. && error.code === 119
  69. && error.message === 'Permission denied',
  70. );
  71. });
  72. test('Parse REST schema writes retry without required flags for older managed servers', async () => {
  73. const bodies: Array<{
  74. fields: Record<string, { type: string; required?: boolean }>;
  75. indexes?: Record<string, Record<string, number>>;
  76. }> = [];
  77. const client = new ParseRestClient({
  78. serverUrl: 'https://parse.example.test/parse',
  79. appId: 'app-id',
  80. masterKey: 'master-secret',
  81. }, async (_input, init = {}) => {
  82. bodies.push(JSON.parse(String(init.body)) as {
  83. fields: Record<string, { type: string; required?: boolean }>;
  84. indexes?: Record<string, Record<string, number>>;
  85. });
  86. if (bodies.length === 1) {
  87. return new Response(JSON.stringify({ error: 'unauthorized' }), {
  88. status: 403,
  89. headers: { 'Content-Type': 'application/json' },
  90. });
  91. }
  92. return new Response(JSON.stringify({ className: 'VocWorkspace' }), {
  93. status: 200,
  94. headers: { 'Content-Type': 'application/json' },
  95. });
  96. });
  97. await client.createSchema({
  98. className: 'VocWorkspace',
  99. fields: {
  100. publicId: { type: 'String', required: true },
  101. name: { type: 'String', required: false },
  102. },
  103. indexes: { voc_workspace_publicid_idx: { publicId: 1 } },
  104. });
  105. assert.equal(bodies.length, 2);
  106. assert.equal(bodies[0]!.fields.publicId!.required, true);
  107. assert.equal('required' in bodies[1]!.fields.publicId!, false);
  108. assert.equal('required' in bodies[1]!.fields.name!, false);
  109. assert.deepEqual(bodies[1]!.indexes, { voc_workspace_publicid_idx: { publicId: 1 } });
  110. });
  111. test('Parse REST schema index updates use the managed schema endpoint', async () => {
  112. let request: { url: string; method: string | undefined; body: string | undefined } | null = null;
  113. const client = new ParseRestClient({
  114. serverUrl: 'https://parse.example.test/parse',
  115. appId: 'app-id',
  116. masterKey: 'master-secret',
  117. }, async (input, init = {}) => {
  118. request = { url: String(input), method: init.method, body: String(init.body) };
  119. return new Response(JSON.stringify({ className: 'VocProduct' }), {
  120. status: 200,
  121. headers: { 'Content-Type': 'application/json' },
  122. });
  123. });
  124. await client.addSchemaIndexes('VocProduct', {
  125. voc_product_naturalkey_idx: { naturalKey: 1 },
  126. });
  127. assert.equal(request!.url, 'https://parse.example.test/parse/schemas/VocProduct');
  128. assert.equal(request!.method, 'PUT');
  129. assert.deepEqual(JSON.parse(request!.body!), {
  130. indexes: { voc_product_naturalkey_idx: { naturalKey: 1 } },
  131. });
  132. });
  133. test('Parse REST class requests retry transient master authorization drift', async () => {
  134. let attempts = 0;
  135. const client = new ParseRestClient({
  136. serverUrl: 'https://parse.example.test/parse',
  137. appId: 'app-id',
  138. masterKey: 'master-secret',
  139. }, async () => {
  140. attempts += 1;
  141. if (attempts < 3) {
  142. return new Response(JSON.stringify({ error: 'unauthorized' }), {
  143. status: 403,
  144. headers: { 'Content-Type': 'application/json' },
  145. });
  146. }
  147. return new Response(JSON.stringify({ results: [], count: 0 }), {
  148. status: 200,
  149. headers: { 'Content-Type': 'application/json' },
  150. });
  151. });
  152. const result = await client.find('VocWorkspace', { count: true });
  153. assert.equal(attempts, 3);
  154. assert.equal(result.count, 0);
  155. });