api.spec.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import {Organization} 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, 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. devUi = window.__SENTRY_DEV_UI;
  86. OrganizationStore.onUpdate(Organization({features: ['frontend-domainsplit']}));
  87. ConfigStore.loadInitialData({
  88. ...configstate,
  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.__SENTRY_DEV_UI = devUi;
  98. OrganizationStore.onUpdate(orgstate.organization);
  99. ConfigStore.loadInitialData(configstate);
  100. });
  101. it('does nothing without feature', function () {
  102. // Org does not have the required feature.
  103. OrganizationStore.onUpdate(Organization());
  104. let result = resolveHostname(controlPath);
  105. expect(result).toBe(controlPath);
  106. // Explicit domains still work.
  107. result = resolveHostname(controlPath, 'https://sentry.io');
  108. expect(result).toBe(`https://sentry.io${controlPath}`);
  109. result = resolveHostname(regionPath, 'https://de.sentry.io');
  110. expect(result).toBe(`https://de.sentry.io${regionPath}`);
  111. });
  112. it('adds domains when feature enabled', function () {
  113. let result = resolveHostname(regionPath);
  114. expect(result).toBe('https://us.sentry.io/api/0/organizations/slug/issues/');
  115. result = resolveHostname(controlPath);
  116. expect(result).toBe('https://sentry.io/api/0/broadcasts/');
  117. });
  118. it('matches if querystrings are in path', function () {
  119. const result = resolveHostname(
  120. '/api/0/organizations/acme/sentry-app-components/?projectId=123'
  121. );
  122. expect(result).toBe(
  123. 'https://sentry.io/api/0/organizations/acme/sentry-app-components/?projectId=123'
  124. );
  125. });
  126. it('uses paths for region silo in dev-ui', function () {
  127. window.__SENTRY_DEV_UI = true;
  128. let result = resolveHostname(regionPath);
  129. expect(result).toBe('/region/us/api/0/organizations/slug/issues/');
  130. result = resolveHostname(controlPath);
  131. expect(result).toBe('/api/0/broadcasts/');
  132. });
  133. it('removes sentryUrl from dev-ui mode requests', function () {
  134. window.__SENTRY_DEV_UI = true;
  135. let result = resolveHostname(regionPath, 'https://sentry.io');
  136. expect(result).toBe('/api/0/organizations/slug/issues/');
  137. result = resolveHostname(controlPath, 'https://sentry.io');
  138. expect(result).toBe('/api/0/broadcasts/');
  139. });
  140. it('removes sentryUrl from dev-ui mode requests when feature is off', function () {
  141. window.__SENTRY_DEV_UI = true;
  142. // Org does not have the required feature.
  143. OrganizationStore.onUpdate(Organization());
  144. let result = resolveHostname(controlPath);
  145. expect(result).toBe(controlPath);
  146. // control silo shaped URLs don't get a host
  147. result = resolveHostname(controlPath, 'https://sentry.io');
  148. expect(result).toBe(controlPath);
  149. result = resolveHostname(regionPath, 'https://de.sentry.io');
  150. expect(result).toBe(`/region/de${regionPath}`);
  151. });
  152. it('preserves host parameters', function () {
  153. const result = resolveHostname(regionPath, 'https://de.sentry.io');
  154. expect(result).toBe('https://de.sentry.io/api/0/organizations/slug/issues/');
  155. });
  156. });
  157. describe('isSimilarOrigin', function () {
  158. test.each([
  159. // Same domain
  160. ['https://sentry.io', 'https://sentry.io', true],
  161. ['https://example.io', 'https://example.io', true],
  162. // Not the same
  163. ['https://example.io', 'https://sentry.io', false],
  164. ['https://sentry.io', 'https://io.sentry', false],
  165. // Sibling domains
  166. ['https://us.sentry.io', 'https://sentry.sentry.io', true],
  167. ['https://us.sentry.io', 'https://acme.sentry.io', true],
  168. ['https://us.sentry.io', 'https://eu.sentry.io', true],
  169. ['https://woof.sentry.io', 'https://woof-org.sentry.io', true],
  170. ['https://woof.sentry.io/issues/1234/', 'https://woof-org.sentry.io', true],
  171. // Subdomain
  172. ['https://sentry.io/api/0/broadcasts/', 'https://woof.sentry.io', true],
  173. ['https://sentry.io/api/0/users/', 'https://sentry.sentry.io', true],
  174. ['https://sentry.io/api/0/users/', 'https://io.sentry.io', true],
  175. // request to subdomain from parent
  176. ['https://us.sentry.io/api/0/users/', 'https://sentry.io', true],
  177. // Not siblings
  178. ['https://sentry.io/api/0/broadcasts/', 'https://sentry.example.io', false],
  179. ['https://acme.sentry.io', 'https://acme.sent.ryio', false],
  180. ['https://woof.example.io', 'https://woof.sentry.io', false],
  181. ['https://woof.sentry.io', 'https://sentry.woof.io', false],
  182. ])('allows sibling domains %s and %s is %s', (target, origin, expected) => {
  183. expect(isSimilarOrigin(target, origin)).toBe(expected);
  184. });
  185. });