network.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { afterEach, describe, expect, mock, test } from 'bun:test';
  2. import {
  3. buildAllowedOrigins,
  4. buildConditionalHeaders,
  5. fetchWithRedirects,
  6. } from './network';
  7. describe('smartfetch/network', () => {
  8. const originalFetch = globalThis.fetch;
  9. afterEach(() => {
  10. globalThis.fetch = originalFetch;
  11. mock.restore();
  12. });
  13. test('collects unique allowed origins from permission patterns', () => {
  14. const origins = [
  15. ...buildAllowedOrigins([
  16. 'https://docs.example.com/page',
  17. 'https://docs.example.com/llms.txt',
  18. 'https://cdn.example.com/asset',
  19. 'not-a-url',
  20. ]),
  21. ].sort();
  22. expect(origins).toEqual([
  23. 'https://cdn.example.com',
  24. 'https://docs.example.com',
  25. ]);
  26. });
  27. test('follows permitted same-origin redirects', async () => {
  28. const fetchMock = mock(async (input: string | URL | Request) => {
  29. const url = typeof input === 'string' ? input : input.toString();
  30. if (url === 'https://docs.example.com/start') {
  31. return new Response('', {
  32. status: 302,
  33. headers: { location: '/next' },
  34. });
  35. }
  36. if (url === 'https://docs.example.com/next') {
  37. return new Response('ok', {
  38. status: 200,
  39. headers: { 'content-type': 'text/plain' },
  40. });
  41. }
  42. throw new Error(`Unexpected fetch URL: ${url}`);
  43. });
  44. globalThis.fetch = fetchMock as unknown as typeof fetch;
  45. const result = await fetchWithRedirects(
  46. 'https://docs.example.com/start',
  47. 1_000,
  48. 'markdown',
  49. new AbortController().signal,
  50. );
  51. expect('blockedRedirect' in result).toBe(false);
  52. if ('blockedRedirect' in result) {
  53. throw new Error('Expected redirect to be followed');
  54. }
  55. expect(result.finalUrl).toBe('https://docs.example.com/next');
  56. expect(result.redirectChain).toEqual([
  57. {
  58. from: 'https://docs.example.com/start',
  59. to: 'https://docs.example.com/next',
  60. status: 302,
  61. },
  62. ]);
  63. });
  64. test('blocks cross-origin redirects when the origin is not allowed', async () => {
  65. const fetchMock = mock(async (input: string | URL | Request) => {
  66. const url = typeof input === 'string' ? input : input.toString();
  67. if (url === 'https://docs.example.com/start') {
  68. return new Response('', {
  69. status: 302,
  70. headers: { location: 'https://other.example.com/landing' },
  71. });
  72. }
  73. throw new Error(`Unexpected fetch URL: ${url}`);
  74. });
  75. globalThis.fetch = fetchMock as unknown as typeof fetch;
  76. const result = await fetchWithRedirects(
  77. 'https://docs.example.com/start',
  78. 1_000,
  79. 'markdown',
  80. new AbortController().signal,
  81. );
  82. expect(result).toEqual({
  83. blockedRedirect: true,
  84. redirectUrl: 'https://other.example.com/landing',
  85. statusCode: 302,
  86. redirectChain: [
  87. {
  88. from: 'https://docs.example.com/start',
  89. to: 'https://other.example.com/landing',
  90. status: 302,
  91. },
  92. ],
  93. });
  94. });
  95. test('allows redirects to explicitly allowed origins', async () => {
  96. const fetchMock = mock(async (input: string | URL | Request) => {
  97. const url = typeof input === 'string' ? input : input.toString();
  98. if (url === 'https://docs.example.com/start') {
  99. return new Response('', {
  100. status: 302,
  101. headers: { location: 'https://cdn.example.com/asset' },
  102. });
  103. }
  104. if (url === 'https://cdn.example.com/asset') {
  105. return new Response('ok', {
  106. status: 200,
  107. headers: { 'content-type': 'text/plain' },
  108. });
  109. }
  110. throw new Error(`Unexpected fetch URL: ${url}`);
  111. });
  112. globalThis.fetch = fetchMock as unknown as typeof fetch;
  113. const result = await fetchWithRedirects(
  114. 'https://docs.example.com/start',
  115. 1_000,
  116. 'markdown',
  117. new AbortController().signal,
  118. undefined,
  119. 'GET',
  120. new Set(['https://docs.example.com', 'https://cdn.example.com']),
  121. );
  122. expect('blockedRedirect' in result).toBe(false);
  123. if ('blockedRedirect' in result) {
  124. throw new Error('Expected redirect to be allowed');
  125. }
  126. expect(result.finalUrl).toBe('https://cdn.example.com/asset');
  127. });
  128. test('builds conditional headers from etag and last-modified without binary branching', () => {
  129. expect(buildConditionalHeaders(undefined)).toBeUndefined();
  130. expect(
  131. buildConditionalHeaders({
  132. requestedUrl: 'https://example.com/file',
  133. finalUrl: 'https://example.com/file',
  134. statusCode: 200,
  135. contentType: 'application/pdf',
  136. charset: undefined,
  137. etag: '"abc"',
  138. lastModified: 'Wed, 01 Jan 2025 00:00:00 GMT',
  139. contentLength: 42,
  140. filename: 'file.pdf',
  141. canonicalUrl: 'https://example.com/file',
  142. redirectChain: [],
  143. upgradedToHttps: false,
  144. truncated: false,
  145. binary: true,
  146. binaryKind: 'pdf',
  147. }),
  148. ).toEqual({
  149. 'If-None-Match': '"abc"',
  150. 'If-Modified-Since': 'Wed, 01 Jan 2025 00:00:00 GMT',
  151. });
  152. });
  153. });