api.spec.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {isSimilarOrigin, Request, resolveHostname} from 'sentry/api';
  3. import {PROJECT_MOVED} from 'sentry/constants/apiErrorCodes';
  4. import ConfigStore from './stores/configStore';
  5. import OrganizationStore from './stores/organizationStore';
  6. jest.unmock('sentry/api');
  7. describe('api', function () {
  8. let api;
  9. beforeEach(function () {
  10. api = new MockApiClient();
  11. });
  12. describe('Client', function () {
  13. describe('cancel()', function () {
  14. it('should abort any open XHR requests', function () {
  15. const abort1 = jest.fn();
  16. const abort2 = jest.fn();
  17. const req1 = new Request(new Promise(() => null), {
  18. abort: abort1,
  19. } as any);
  20. const req2 = new Request(new Promise(() => null), {abort: abort2} as any);
  21. api.activeRequests = {
  22. 1: req1,
  23. 2: req2,
  24. };
  25. api.clear();
  26. expect(req1.aborter?.abort).toHaveBeenCalledTimes(1);
  27. expect(req2.aborter?.abort).toHaveBeenCalledTimes(1);
  28. });
  29. });
  30. });
  31. it('does not call success callback if 302 was returned because of a project slug change', function () {
  32. const successCb = jest.fn();
  33. api.activeRequests = {id: {alive: true}};
  34. api.wrapCallback(
  35. 'id',
  36. successCb
  37. )({
  38. responseJSON: {
  39. detail: {
  40. code: PROJECT_MOVED,
  41. message: '...',
  42. extra: {
  43. slug: 'new-slug',
  44. },
  45. },
  46. },
  47. });
  48. expect(successCb).not.toHaveBeenCalled();
  49. });
  50. it('handles error callback', function () {
  51. jest.spyOn(api, 'wrapCallback').mockImplementation((_id, func) => func);
  52. const errorCb = jest.fn();
  53. const args = ['test', true, 1];
  54. api.handleRequestError(
  55. {
  56. id: 'test',
  57. path: 'test',
  58. requestOptions: {error: errorCb},
  59. },
  60. ...args
  61. );
  62. expect(errorCb).toHaveBeenCalledWith(...args);
  63. });
  64. it('handles undefined error callback', function () {
  65. expect(() =>
  66. api.handleRequestError(
  67. {
  68. id: 'test',
  69. path: 'test',
  70. requestOptions: {},
  71. },
  72. {},
  73. {}
  74. )
  75. ).not.toThrow();
  76. });
  77. });
  78. describe('resolveHostname', function () {
  79. let devUi, location, configstate;
  80. const controlPath = '/api/0/broadcasts/';
  81. const regionPath = '/api/0/organizations/slug/issues/';
  82. beforeEach(function () {
  83. configstate = ConfigStore.getState();
  84. location = window.location;
  85. devUi = window.__SENTRY_DEV_UI;
  86. ConfigStore.loadInitialData({
  87. ...configstate,
  88. features: ['system:multi-region'],
  89. links: {
  90. organizationUrl: 'https://acme.sentry.io',
  91. sentryUrl: 'https://sentry.io',
  92. regionUrl: 'https://us.sentry.io',
  93. },
  94. });
  95. });
  96. afterEach(() => {
  97. window.location = location;
  98. window.__SENTRY_DEV_UI = devUi;
  99. ConfigStore.loadInitialData(configstate);
  100. });
  101. it('does nothing without feature', function () {
  102. ConfigStore.loadInitialData({
  103. ...configstate,
  104. // Remove the feature flag
  105. features: [],
  106. });
  107. let result = resolveHostname(controlPath);
  108. expect(result).toBe(controlPath);
  109. // Explicit domains still work.
  110. result = resolveHostname(controlPath, 'https://sentry.io');
  111. expect(result).toBe(`https://sentry.io${controlPath}`);
  112. result = resolveHostname(regionPath, 'https://de.sentry.io');
  113. expect(result).toBe(`https://de.sentry.io${regionPath}`);
  114. });
  115. it('does not override region in _admin', function () {
  116. Object.defineProperty(window, 'location', {
  117. configurable: true,
  118. enumerable: true,
  119. value: new URL('https://sentry.io/_admin/'),
  120. });
  121. // Adds domain to control paths
  122. let result = resolveHostname(controlPath);
  123. expect(result).toBe('https://sentry.io/api/0/broadcasts/');
  124. // Doesn't add domain to region paths
  125. result = resolveHostname(regionPath);
  126. expect(result).toBe(regionPath);
  127. // Explicit domains still work.
  128. result = resolveHostname(controlPath, 'https://sentry.io');
  129. expect(result).toBe(`https://sentry.io${controlPath}`);
  130. result = resolveHostname(regionPath, 'https://de.sentry.io');
  131. expect(result).toBe(`https://de.sentry.io${regionPath}`);
  132. });
  133. it('adds domains when feature enabled', function () {
  134. let result = resolveHostname(regionPath);
  135. expect(result).toBe('https://us.sentry.io/api/0/organizations/slug/issues/');
  136. result = resolveHostname(controlPath);
  137. expect(result).toBe('https://sentry.io/api/0/broadcasts/');
  138. });
  139. it('matches if querystrings are in path', function () {
  140. const result = resolveHostname(
  141. '/api/0/organizations/acme/sentry-app-components/?projectId=123'
  142. );
  143. expect(result).toBe(
  144. 'https://sentry.io/api/0/organizations/acme/sentry-app-components/?projectId=123'
  145. );
  146. });
  147. it('uses paths for region silo in dev-ui', function () {
  148. window.__SENTRY_DEV_UI = true;
  149. let result = resolveHostname(regionPath);
  150. expect(result).toBe('/region/us/api/0/organizations/slug/issues/');
  151. result = resolveHostname(controlPath);
  152. expect(result).toBe('/api/0/broadcasts/');
  153. });
  154. it('removes sentryUrl from dev-ui mode requests', function () {
  155. window.__SENTRY_DEV_UI = true;
  156. let result = resolveHostname(regionPath, 'https://sentry.io');
  157. expect(result).toBe('/api/0/organizations/slug/issues/');
  158. result = resolveHostname(controlPath, 'https://sentry.io');
  159. expect(result).toBe('/api/0/broadcasts/');
  160. });
  161. it('removes sentryUrl from dev-ui mode requests when feature is off', function () {
  162. window.__SENTRY_DEV_UI = true;
  163. // Org does not have the required feature.
  164. OrganizationStore.onUpdate(OrganizationFixture());
  165. let result = resolveHostname(controlPath);
  166. expect(result).toBe(controlPath);
  167. // control silo shaped URLs don't get a host
  168. result = resolveHostname(controlPath, 'https://sentry.io');
  169. expect(result).toBe(controlPath);
  170. result = resolveHostname(regionPath, 'https://de.sentry.io');
  171. expect(result).toBe(`/region/de${regionPath}`);
  172. });
  173. it('preserves host parameters', function () {
  174. const result = resolveHostname(regionPath, 'https://de.sentry.io');
  175. expect(result).toBe('https://de.sentry.io/api/0/organizations/slug/issues/');
  176. });
  177. });
  178. describe('isSimilarOrigin', function () {
  179. test.each([
  180. // Same domain
  181. ['https://sentry.io', 'https://sentry.io', true],
  182. ['https://example.io', 'https://example.io', true],
  183. // Not the same
  184. ['https://example.io', 'https://sentry.io', false],
  185. ['https://sentry.io', 'https://io.sentry', false],
  186. // Sibling domains
  187. ['https://us.sentry.io', 'https://sentry.sentry.io', true],
  188. ['https://us.sentry.io', 'https://acme.sentry.io', true],
  189. ['https://us.sentry.io', 'https://eu.sentry.io', true],
  190. ['https://woof.sentry.io', 'https://woof-org.sentry.io', true],
  191. ['https://woof.sentry.io/issues/1234/', 'https://woof-org.sentry.io', true],
  192. // Subdomain
  193. ['https://sentry.io/api/0/broadcasts/', 'https://woof.sentry.io', true],
  194. ['https://sentry.io/api/0/users/', 'https://sentry.sentry.io', true],
  195. ['https://sentry.io/api/0/users/', 'https://io.sentry.io', true],
  196. // request to subdomain from parent
  197. ['https://us.sentry.io/api/0/users/', 'https://sentry.io', true],
  198. // Not siblings
  199. ['https://sentry.io/api/0/broadcasts/', 'https://sentry.example.io', false],
  200. ['https://acme.sentry.io', 'https://acme.sent.ryio', false],
  201. ['https://woof.example.io', 'https://woof.sentry.io', false],
  202. ['https://woof.sentry.io', 'https://sentry.woof.io', false],
  203. ])('allows sibling domains %s and %s is %s', (target, origin, expected) => {
  204. expect(isSimilarOrigin(target, origin)).toBe(expected);
  205. });
  206. });