session-and-requests.test.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import vm from 'node:vm';
  4. import { build } from 'esbuild';
  5. // Exercise the actual request/composable code against a controlled uni transport, without sending business requests.
  6. const bundled = await build({
  7. stdin: {
  8. contents: `export { session, clearSession } from './src/stores/state'; export { get } from './src/api/client'; export { useCommand } from './src/composables/useCommand'; export { useResource } from './src/composables/useResource'; export { usePaged } from './src/composables/usePaged';`,
  9. resolveDir: process.cwd(),
  10. loader: 'ts',
  11. },
  12. bundle: true,
  13. write: false,
  14. platform: 'node',
  15. format: 'cjs',
  16. define: {
  17. 'import.meta.env.VITE_API_BASE_URL': '""',
  18. 'process.env.NODE_ENV': '"production"',
  19. },
  20. });
  21. function fixture() {
  22. const calls = [];
  23. const storage = new Map();
  24. const transport = {
  25. request: (options) => calls.push(options),
  26. getStorageSync: (key) => (storage.has(key) ? JSON.parse(storage.get(key)) : ''),
  27. setStorageSync: (key, value) => storage.set(key, JSON.stringify(value)),
  28. removeStorageSync: (key) => storage.delete(key),
  29. stopPullDownRefresh() {},
  30. showToast() {},
  31. };
  32. const context = vm.createContext({
  33. module: { exports: {} },
  34. exports: {},
  35. uni: transport,
  36. console,
  37. setTimeout,
  38. clearTimeout,
  39. });
  40. vm.runInContext(bundled.outputFiles[0].text, context);
  41. const app = context.module.exports;
  42. Object.assign(app.session, {
  43. token: 'a'.repeat(64),
  44. userId: '11',
  45. active: { role: 'enterprise_admin', resource_id: 4, name: 'fixture' },
  46. });
  47. return { ...app, calls, storage };
  48. }
  49. test('protected requests carry mini token; public requests do not', async () => {
  50. const app = fixture();
  51. const publicRequest = app.get('/config', {}, false);
  52. assert.equal(app.calls[0].header.Authorization, undefined);
  53. app.calls[0].success({
  54. statusCode: 200,
  55. data: { code: 0, data: { enabled: true } },
  56. });
  57. await publicRequest;
  58. const protectedRequest = app.get('/me');
  59. assert.equal(app.calls[1].header.Authorization, 'Bearer ' + 'a'.repeat(64));
  60. app.calls[1].success({
  61. statusCode: 200,
  62. data: { code: 0, data: { user_id: '11' } },
  63. });
  64. assert.equal((await protectedRequest).user_id, '11');
  65. });
  66. test('HTTP 401 clears the current session and rejects the request', async () => {
  67. const app = fixture();
  68. const request = app.get('/me');
  69. app.calls[0].success({
  70. statusCode: 401,
  71. data: { code: 40101, message: '请先登录' },
  72. });
  73. await assert.rejects(request, /请先登录/);
  74. assert.equal(app.session.token, '');
  75. assert.equal(app.session.active, null);
  76. });
  77. test('a late response from an old identity cannot update data or invalidate a new login', async () => {
  78. const app = fixture();
  79. const request = app.get('/me');
  80. app.session.epoch++;
  81. app.session.token = 'b'.repeat(64);
  82. app.calls[0].success({
  83. statusCode: 401,
  84. data: { code: 40101, message: 'old token' },
  85. });
  86. await assert.rejects(request, /身份已切换/);
  87. assert.equal(app.session.token, 'b'.repeat(64));
  88. });
  89. test('logout synchronously clears loaded private data and list items', async () => {
  90. const app = fixture();
  91. const detail = app.useResource(async () => ({ name: 'private' }));
  92. const list = app.usePaged(async () => ({
  93. items: [{ id: 1 }],
  94. has_more: false,
  95. total: 1,
  96. }));
  97. await detail.load();
  98. await list.load(true);
  99. assert.equal(detail.data.value.name, 'private');
  100. assert.equal(list.items.value.length, 1);
  101. app.clearSession();
  102. assert.equal(detail.data.value, null);
  103. assert.equal(list.items.value.length, 0);
  104. });
  105. test('a slow old filter cannot overwrite a newer paginated result', async () => {
  106. const app = fixture();
  107. const pending = [];
  108. const list = app.usePaged(() => new Promise((resolve) => pending.push(resolve)));
  109. const first = list.load(true);
  110. const second = list.load(true);
  111. pending[1]({ items: [{ id: 2 }], total: 1, has_more: false });
  112. await second;
  113. pending[0]({ items: [{ id: 1 }], total: 1, has_more: false });
  114. await first;
  115. assert.equal(list.items.value[0].id, 2);
  116. assert.equal(list.data.value.items[0].id, 2);
  117. });
  118. test('pagination appends each page, stops at the end, and clears metadata on logout', async () => {
  119. const app = fixture();
  120. const pages = [];
  121. const list = app.usePaged(async (page) => {
  122. pages.push(page);
  123. return { items: [{ id: page }], total: 2, has_more: page < 2, total_cents: 266400 };
  124. });
  125. await list.load(true);
  126. assert.equal(list.data.value.total_cents, 266400);
  127. await list.load();
  128. await list.load();
  129. assert.deepEqual(pages, [1, 2]);
  130. assert.equal(list.items.value.length, 2);
  131. assert.equal(list.hasMore.value, false);
  132. app.clearSession();
  133. assert.equal(list.data.value, null);
  134. assert.equal(list.items.value.length, 0);
  135. });
  136. test('double taps are ignored, network retries reuse persisted key, and success is delivered once', async () => {
  137. const app = fixture();
  138. let completions = 0;
  139. const command = app.useCommand(
  140. () => '/enterprises/4/deliveries',
  141. () => {
  142. completions++;
  143. },
  144. );
  145. const first = command.execute({ sku_id: 2, quantity: 8 });
  146. await command.execute({ sku_id: 2, quantity: 8 });
  147. assert.equal(app.calls.length, 1);
  148. const key = app.calls[0].data.request_key;
  149. app.calls[0].fail();
  150. await first;
  151. assert.equal(command.pending, true);
  152. assert.equal(app.storage.size, 1);
  153. const retry = command.retry();
  154. assert.equal(app.calls[1].data.request_key, key);
  155. app.calls[1].success({ statusCode: 200, data: { code: 0, data: { id: 9 } } });
  156. await retry;
  157. assert.equal(command.pending, false);
  158. assert.equal(app.storage.size, 0);
  159. assert.equal(completions, 1);
  160. });
  161. test('checkout submission repairs a stored payload missing its request key and keeps it on retry', async () => {
  162. const app = fixture();
  163. const path = '/enterprises/49/checkouts';
  164. const payload = { items: [{ package_id: 1, quantity: 2 }], expected_amount_cents: 1000, agreement_accepted: true, invoice: null };
  165. app.storage.set(`water.command:11:${path}`, JSON.stringify(payload));
  166. const command = app.useCommand(() => path, () => {});
  167. const attempt = command.execute(payload);
  168. const key = app.calls[0].data.request_key;
  169. assert.match(key, /^[A-Za-z0-9_-]{8,80}$/);
  170. app.calls[0].fail();
  171. await attempt;
  172. const retry = command.retry();
  173. assert.equal(app.calls[1].data.request_key, key);
  174. app.calls[1].success({ statusCode: 200, data: { code: 0, data: { id: 9 } } });
  175. await retry;
  176. assert.equal(app.storage.size, 0);
  177. });
  178. test('an unresolved command survives page recreation and cannot be replaced with edited input', async () => {
  179. const app = fixture();
  180. const path = () => '/enterprises/4/deliveries';
  181. const first = app.useCommand(path, () => {});
  182. const attempt = first.execute({ quantity: 8 });
  183. app.calls[0].fail();
  184. await attempt;
  185. const reopened = app.useCommand(path, () => {});
  186. await reopened.execute({ quantity: 9 });
  187. assert.equal(app.calls.length, 1);
  188. assert.equal(reopened.pending, true);
  189. const retry = reopened.retry();
  190. assert.equal(app.calls[1].data.quantity, 8);
  191. app.calls[1].success({
  192. statusCode: 422,
  193. data: { code: 42201, message: 'test rejection' },
  194. });
  195. await retry;
  196. assert.equal(app.storage.size, 0);
  197. });