api.spec.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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, orgstate, configstate;
  80. const controlPath = '/api/0/broadcasts/';
  81. const regionPath = '/api/0/organizations/slug/issues/';
  82. beforeEach(function () {
  83. orgstate = OrganizationStore.get();
  84. configstate = ConfigStore.getState();
  85. location = window.location;
  86. devUi = window.__SENTRY_DEV_UI;
  87. OrganizationStore.onUpdate(OrganizationFixture({features: ['frontend-domainsplit']}));
  88. ConfigStore.loadInitialData({
  89. ...configstate,
  90. links: {
  91. organizationUrl: 'https://acme.sentry.io',
  92. sentryUrl: 'https://sentry.io',
  93. regionUrl: 'https://us.sentry.io',
  94. },
  95. });
  96. });
  97. afterEach(() => {
  98. window.location = location;
  99. window.__SENTRY_DEV_UI = devUi;
  100. OrganizationStore.onUpdate(orgstate.organization);
  101. ConfigStore.loadInitialData(configstate);
  102. });
  103. it('does nothing without feature', function () {
  104. // Org does not have the required feature.
  105. OrganizationStore.onUpdate(OrganizationFixture());
  106. let result = resolveHostname(controlPath);
  107. expect(result).toBe(controlPath);
  108. // Explicit domains still work.
  109. result = resolveHostname(controlPath, 'https://sentry.io');
  110. expect(result).toBe(`https://sentry.io${controlPath}`);
  111. result = resolveHostname(regionPath, 'https://de.sentry.io');
  112. expect(result).toBe(`https://de.sentry.io${regionPath}`);
  113. });
  114. it('does not override region in _admin', function () {
  115. Object.defineProperty(window, 'location', {
  116. configurable: true,
  117. enumerable: true,
  118. value: new URL('https://sentry.io/_admin/'),
  119. });
  120. // Adds domain to control paths
  121. let result = resolveHostname(controlPath);
  122. expect(result).toBe('https://sentry.io/api/0/broadcasts/');
  123. // Doesn't add domain to region paths
  124. result = resolveHostname(regionPath);
  125. expect(result).toBe(regionPath);
  126. // Explicit domains still work.
  127. result = resolveHostname(controlPath, 'https://sentry.io');
  128. expect(result).toBe(`https://sentry.io${controlPath}`);
  129. result = resolveHostname(regionPath, 'https://de.sentry.io');
  130. expect(result).toBe(`https://de.sentry.io${regionPath}`);
  131. });
  132. it('adds domains when feature enabled', function () {
  133. let result = resolveHostname(regionPath);
  134. expect(result).toBe('https://us.sentry.io/api/0/organizations/slug/issues/');
  135. result = resolveHostname(controlPath);
  136. expect(result).toBe('https://sentry.io/api/0/broadcasts/');
  137. });
  138. it('matches if querystrings are in path', function () {
  139. const result = resolveHostname(
  140. '/api/0/organizations/acme/sentry-app-components/?projectId=123'
  141. );
  142. expect(result).toBe(
  143. 'https://sentry.io/api/0/organizations/acme/sentry-app-components/?projectId=123'
  144. );
  145. });
  146. it('uses paths for region silo in dev-ui', function () {
  147. window.__SENTRY_DEV_UI = true;
  148. let result = resolveHostname(regionPath);
  149. expect(result).toBe('/region/us/api/0/organizations/slug/issues/');
  150. result = resolveHostname(controlPath);
  151. expect(result).toBe('/api/0/broadcasts/');
  152. });
  153. it('removes sentryUrl from dev-ui mode requests', function () {
  154. window.__SENTRY_DEV_UI = true;
  155. let result = resolveHostname(regionPath, 'https://sentry.io');
  156. expect(result).toBe('/api/0/organizations/slug/issues/');
  157. result = resolveHostname(controlPath, 'https://sentry.io');
  158. expect(result).toBe('/api/0/broadcasts/');
  159. });
  160. it('removes sentryUrl from dev-ui mode requests when feature is off', function () {
  161. window.__SENTRY_DEV_UI = true;
  162. // Org does not have the required feature.
  163. OrganizationStore.onUpdate(OrganizationFixture());
  164. let result = resolveHostname(controlPath);
  165. expect(result).toBe(controlPath);
  166. // control silo shaped URLs don't get a host
  167. result = resolveHostname(controlPath, 'https://sentry.io');
  168. expect(result).toBe(controlPath);
  169. result = resolveHostname(regionPath, 'https://de.sentry.io');
  170. expect(result).toBe(`/region/de${regionPath}`);
  171. });
  172. it('preserves host parameters', function () {
  173. const result = resolveHostname(regionPath, 'https://de.sentry.io');
  174. expect(result).toBe('https://de.sentry.io/api/0/organizations/slug/issues/');
  175. });
  176. });
  177. describe('isSimilarOrigin', function () {
  178. test.each([
  179. // Same domain
  180. ['https://sentry.io', 'https://sentry.io', true],
  181. ['https://example.io', 'https://example.io', true],
  182. // Not the same
  183. ['https://example.io', 'https://sentry.io', false],
  184. ['https://sentry.io', 'https://io.sentry', false],
  185. // Sibling domains
  186. ['https://us.sentry.io', 'https://sentry.sentry.io', true],
  187. ['https://us.sentry.io', 'https://acme.sentry.io', true],
  188. ['https://us.sentry.io', 'https://eu.sentry.io', true],
  189. ['https://woof.sentry.io', 'https://woof-org.sentry.io', true],
  190. ['https://woof.sentry.io/issues/1234/', 'https://woof-org.sentry.io', true],
  191. // Subdomain
  192. ['https://sentry.io/api/0/broadcasts/', 'https://woof.sentry.io', true],
  193. ['https://sentry.io/api/0/users/', 'https://sentry.sentry.io', true],
  194. ['https://sentry.io/api/0/users/', 'https://io.sentry.io', true],
  195. // request to subdomain from parent
  196. ['https://us.sentry.io/api/0/users/', 'https://sentry.io', true],
  197. // Not siblings
  198. ['https://sentry.io/api/0/broadcasts/', 'https://sentry.example.io', false],
  199. ['https://acme.sentry.io', 'https://acme.sent.ryio', false],
  200. ['https://woof.example.io', 'https://woof.sentry.io', false],
  201. ['https://woof.sentry.io', 'https://sentry.woof.io', false],
  202. ])('allows sibling domains %s and %s is %s', (target, origin, expected) => {
  203. expect(isSimilarOrigin(target, origin)).toBe(expected);
  204. });
  205. });