api.spec.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import {Request, resolveHostname} from 'sentry/api';
  2. import {PROJECT_MOVED} from 'sentry/constants/apiErrorCodes';
  3. import ConfigStore from './stores/configStore';
  4. import OrganizationStore from './stores/organizationStore';
  5. jest.unmock('sentry/api');
  6. describe('api', function () {
  7. let api;
  8. beforeEach(function () {
  9. api = new MockApiClient();
  10. });
  11. describe('Client', function () {
  12. describe('cancel()', function () {
  13. it('should abort any open XHR requests', function () {
  14. const abort1 = jest.fn();
  15. const abort2 = jest.fn();
  16. const req1 = new Request(new Promise(() => null), {
  17. abort: abort1,
  18. } as any);
  19. const req2 = new Request(new Promise(() => null), {abort: abort2} as any);
  20. api.activeRequests = {
  21. 1: req1,
  22. 2: req2,
  23. };
  24. api.clear();
  25. expect(req1.aborter?.abort).toHaveBeenCalledTimes(1);
  26. expect(req2.aborter?.abort).toHaveBeenCalledTimes(1);
  27. });
  28. });
  29. });
  30. it('does not call success callback if 302 was returned because of a project slug change', function () {
  31. const successCb = jest.fn();
  32. api.activeRequests = {id: {alive: true}};
  33. api.wrapCallback(
  34. 'id',
  35. successCb
  36. )({
  37. responseJSON: {
  38. detail: {
  39. code: PROJECT_MOVED,
  40. message: '...',
  41. extra: {
  42. slug: 'new-slug',
  43. },
  44. },
  45. },
  46. });
  47. expect(successCb).not.toHaveBeenCalled();
  48. });
  49. it('handles error callback', function () {
  50. jest.spyOn(api, 'wrapCallback').mockImplementation((_id, func) => func);
  51. const errorCb = jest.fn();
  52. const args = ['test', true, 1];
  53. api.handleRequestError(
  54. {
  55. id: 'test',
  56. path: 'test',
  57. requestOptions: {error: errorCb},
  58. },
  59. ...args
  60. );
  61. expect(errorCb).toHaveBeenCalledWith(...args);
  62. });
  63. it('handles undefined error callback', function () {
  64. expect(() =>
  65. api.handleRequestError(
  66. {
  67. id: 'test',
  68. path: 'test',
  69. requestOptions: {},
  70. },
  71. {},
  72. {}
  73. )
  74. ).not.toThrow();
  75. });
  76. });
  77. describe('resolveHostname', function () {
  78. let devUi, orgstate, configstate;
  79. const controlPath = '/api/0/broadcasts/';
  80. const regionPath = '/api/0/organizations/slug/issues/';
  81. beforeEach(function () {
  82. orgstate = OrganizationStore.get();
  83. configstate = ConfigStore.getState();
  84. devUi = window.__SENTRY_DEV_UI;
  85. OrganizationStore.onUpdate(
  86. TestStubs.Organization({features: ['frontend-domainsplit']})
  87. );
  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.__SENTRY_DEV_UI = devUi;
  99. OrganizationStore.onUpdate(orgstate.organization);
  100. ConfigStore.loadInitialData(configstate);
  101. });
  102. it('does nothing without feature', function () {
  103. // Org does not have the required feature.
  104. OrganizationStore.onUpdate(TestStubs.Organization());
  105. let result = resolveHostname(controlPath);
  106. expect(result).toBe(controlPath);
  107. // Explicit domains still work.
  108. result = resolveHostname(controlPath, 'https://sentry.io');
  109. expect(result).toBe(`https://sentry.io${controlPath}`);
  110. result = resolveHostname(regionPath, 'https://de.sentry.io');
  111. expect(result).toBe(`https://de.sentry.io${regionPath}`);
  112. });
  113. it('adds domains when feature enabled', function () {
  114. let result = resolveHostname(regionPath);
  115. expect(result).toBe('https://us.sentry.io/api/0/organizations/slug/issues/');
  116. result = resolveHostname(controlPath);
  117. expect(result).toBe('https://sentry.io/api/0/broadcasts/');
  118. });
  119. it('uses paths for region silo in dev-ui', function () {
  120. window.__SENTRY_DEV_UI = true;
  121. let result = resolveHostname(regionPath);
  122. expect(result).toBe('/region/us/api/0/organizations/slug/issues/');
  123. result = resolveHostname(controlPath);
  124. expect(result).toBe('/api/0/broadcasts/');
  125. });
  126. it('removes sentryUrl from dev-ui mode requests', function () {
  127. window.__SENTRY_DEV_UI = true;
  128. let result = resolveHostname(regionPath, 'https://sentry.io');
  129. expect(result).toBe('/api/0/organizations/slug/issues/');
  130. result = resolveHostname(controlPath, 'https://sentry.io');
  131. expect(result).toBe('/api/0/broadcasts/');
  132. });
  133. it('removes sentryUrl from dev-ui mode requests when feature is off', function () {
  134. window.__SENTRY_DEV_UI = true;
  135. // Org does not have the required feature.
  136. OrganizationStore.onUpdate(TestStubs.Organization());
  137. let result = resolveHostname(controlPath);
  138. expect(result).toBe(controlPath);
  139. // control silo shaped URLs don't get a host
  140. result = resolveHostname(controlPath, 'https://sentry.io');
  141. expect(result).toBe(controlPath);
  142. result = resolveHostname(regionPath, 'https://de.sentry.io');
  143. expect(result).toBe(`/region/de${regionPath}`);
  144. });
  145. it('preserves host parameters', function () {
  146. const result = resolveHostname(regionPath, 'https://de.sentry.io');
  147. expect(result).toBe('https://de.sentry.io/api/0/organizations/slug/issues/');
  148. });
  149. });