withDomainRequired.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import {RouteComponentProps} from 'react-router';
  2. import {Location, LocationDescriptor, LocationDescriptorObject} from 'history';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {render, screen} from 'sentry-test/reactTestingLibrary';
  5. import withDomainRequired, {normalizeUrl} from 'sentry/utils/withDomainRequired';
  6. describe('normalizeUrl', function () {
  7. let result;
  8. beforeEach(function () {
  9. window.__initialData = {
  10. customerDomain: {
  11. subdomain: 'albertos-apples',
  12. organizationUrl: 'https://albertos-apples.sentry.io',
  13. sentryUrl: 'https://sentry.io',
  14. },
  15. } as any;
  16. });
  17. it('replaces paths in strings', function () {
  18. const location = TestStubs.location();
  19. const cases = [
  20. // input, expected
  21. ['/settings/', '/settings/'],
  22. // Organization settings views.
  23. ['/settings/acme/', '/settings/organization/'],
  24. ['/settings/organization', '/settings/organization/'],
  25. ['/settings/sentry/members/', '/settings/members/'],
  26. ['/settings/sentry/members/3/', '/settings/members/3/'],
  27. ['/settings/sentry/teams/peeps/', '/settings/teams/peeps/'],
  28. ['/settings/sentry/billing/receipts/', '/settings/billing/receipts/'],
  29. [
  30. '/settings/acme/developer-settings/release-bot/',
  31. '/settings/developer-settings/release-bot/',
  32. ],
  33. [
  34. '/settings/sentry/integrations/vercel/12345/?next=something',
  35. '/settings/integrations/vercel/12345/?next=something',
  36. ],
  37. // Account settings should stay the same
  38. ['/settings/account/', '/settings/account/'],
  39. ['/settings/account/security/', '/settings/account/security/'],
  40. ['/settings/account/details/', '/settings/account/details/'],
  41. ['/join-request/acme', '/join-request/'],
  42. ['/join-request/acme/', '/join-request/'],
  43. ['/onboarding/acme/', '/onboarding/'],
  44. ['/onboarding/acme/project/', '/onboarding/project/'],
  45. ['/organizations/new/', '/organizations/new/'],
  46. ['/organizations/albertos-apples/issues/', '/issues/'],
  47. ['/organizations/albertos-apples/issues/?_q=all#hash', '/issues/?_q=all#hash'],
  48. ['/acme/project-slug/getting-started/', '/getting-started/project-slug/'],
  49. [
  50. '/acme/project-slug/getting-started/python',
  51. '/getting-started/project-slug/python',
  52. ],
  53. ['/settings/projects/python/filters/', '/settings/projects/python/filters/'],
  54. [
  55. '/settings/projects/python/filters/discarded/',
  56. '/settings/projects/python/filters/discarded/',
  57. ],
  58. // Team settings links in breadcrumbs can be pre-normalized from breadcrumbs
  59. ['/settings/teams/peeps/', '/settings/teams/peeps/'],
  60. ];
  61. for (const [input, expected] of cases) {
  62. result = normalizeUrl(input);
  63. expect(result).toEqual(expected);
  64. result = normalizeUrl(input, location);
  65. expect(result).toEqual(expected);
  66. result = normalizeUrl(input, {forceCustomerDomain: false});
  67. expect(result).toEqual(expected);
  68. result = normalizeUrl(input, location, {forceCustomerDomain: false});
  69. expect(result).toEqual(expected);
  70. }
  71. // Normalizes urls if options.customerDomain is true and orgslug.sentry.io isn't being used
  72. window.__initialData.customerDomain = null;
  73. for (const [input, expected] of cases) {
  74. result = normalizeUrl(input, {forceCustomerDomain: true});
  75. expect(result).toEqual(expected);
  76. result = normalizeUrl(input, location, {forceCustomerDomain: true});
  77. expect(result).toEqual(expected);
  78. }
  79. // No effect if customerDomain isn't defined
  80. window.__initialData.customerDomain = null;
  81. for (const [input, _expected] of cases) {
  82. result = normalizeUrl(input);
  83. expect(result).toEqual(input);
  84. result = normalizeUrl(input, location);
  85. expect(result).toEqual(input);
  86. }
  87. });
  88. it('replaces pathname in objects', function () {
  89. const location = TestStubs.location();
  90. result = normalizeUrl({pathname: '/settings/acme/'}, location);
  91. expect(result.pathname).toEqual('/settings/organization/');
  92. result = normalizeUrl({pathname: '/settings/acme/'}, location, {
  93. forceCustomerDomain: false,
  94. });
  95. expect(result.pathname).toEqual('/settings/organization/');
  96. result = normalizeUrl({pathname: '/settings/sentry/members'}, location);
  97. expect(result.pathname).toEqual('/settings/members');
  98. result = normalizeUrl({pathname: '/organizations/albertos-apples/issues'}, location);
  99. expect(result.pathname).toEqual('/issues');
  100. result = normalizeUrl(
  101. {
  102. pathname: '/organizations/sentry/profiling/profile/sentry/abc123/',
  103. query: {sorting: 'call order'},
  104. },
  105. location
  106. );
  107. expect(result.pathname).toEqual('/profiling/profile/sentry/abc123/');
  108. result = normalizeUrl(
  109. {
  110. pathname: '/organizations/albertos-apples/issues',
  111. query: {q: 'all'},
  112. },
  113. location
  114. );
  115. expect(result.pathname).toEqual('/issues');
  116. // Normalizes urls if options.customerDomain is true and orgslug.sentry.io isn't being used
  117. window.__initialData.customerDomain = null;
  118. result = normalizeUrl({pathname: '/settings/acme/'}, location, {
  119. forceCustomerDomain: true,
  120. });
  121. expect(result.pathname).toEqual('/settings/organization/');
  122. result = normalizeUrl(
  123. {
  124. pathname: '/organizations/albertos-apples/issues',
  125. query: {q: 'all'},
  126. },
  127. location,
  128. {
  129. forceCustomerDomain: true,
  130. }
  131. );
  132. expect(result.pathname).toEqual('/issues');
  133. });
  134. it('replaces pathname in function callback', function () {
  135. const location = TestStubs.location();
  136. function objectCallback(_loc: Location): LocationDescriptorObject {
  137. return {pathname: '/settings/'};
  138. }
  139. result = normalizeUrl(objectCallback, location);
  140. // @ts-ignore
  141. expect(result.pathname).toEqual('/settings/');
  142. function stringCallback(_loc: Location): LocationDescriptor {
  143. return '/organizations/a-long-slug/discover/';
  144. }
  145. result = normalizeUrl(stringCallback, location);
  146. expect(result).toEqual('/discover/');
  147. // Normalizes urls if options.customerDomain is true and orgslug.sentry.io isn't being used
  148. window.__initialData.customerDomain = null;
  149. function objectCallback2(_loc: Location): LocationDescriptorObject {
  150. return {pathname: '/settings/'};
  151. }
  152. result = normalizeUrl(objectCallback2, location, {forceCustomerDomain: true});
  153. // @ts-ignore
  154. expect(result.pathname).toEqual('/settings/');
  155. function stringCallback2(_loc: Location): LocationDescriptor {
  156. return '/organizations/a-long-slug/discover/';
  157. }
  158. result = normalizeUrl(stringCallback2, location, {forceCustomerDomain: true});
  159. expect(result).toEqual('/discover/');
  160. });
  161. it('errors on functions without location', function () {
  162. function objectCallback(_loc: Location): LocationDescriptorObject {
  163. return {pathname: '/settings/organization'};
  164. }
  165. expect(() => normalizeUrl(objectCallback)).toThrow();
  166. });
  167. });
  168. const originalLocation = window.location;
  169. describe('withDomainRequired', function () {
  170. type Props = RouteComponentProps<{orgId: string}, {}>;
  171. const MyComponent = (props: Props) => {
  172. const {params} = props;
  173. return <div>Org slug: {params.orgId ?? 'no org slug'}</div>;
  174. };
  175. beforeEach(function () {
  176. Object.defineProperty(window, 'location', {
  177. writable: true,
  178. value: {
  179. replace: jest.fn(),
  180. pathname: '/organizations/albertos-apples/issues/',
  181. search: '?q=123',
  182. hash: '#hash',
  183. },
  184. });
  185. window.__initialData = {
  186. customerDomain: {
  187. subdomain: 'albertos-apples',
  188. organizationUrl: 'https://albertos-apples.sentry.io',
  189. sentryUrl: 'https://sentry.io',
  190. },
  191. links: {
  192. organizationUrl: null,
  193. regionUrl: null,
  194. sentryUrl: 'https://sentry.io',
  195. },
  196. } as any;
  197. });
  198. afterEach(function () {
  199. window.location = originalLocation;
  200. });
  201. it('redirects to sentryUrl in non-customer domain world', function () {
  202. window.__initialData = {
  203. customerDomain: null,
  204. features: ['organizations:customer-domains'],
  205. links: {
  206. organizationUrl: null,
  207. regionUrl: null,
  208. sentryUrl: 'https://sentry.io',
  209. },
  210. } as any;
  211. const organization = TestStubs.Organization({
  212. slug: 'albertos-apples',
  213. features: [],
  214. });
  215. const params = {
  216. orgId: 'albertos-apples',
  217. };
  218. const {router, route, routerContext} = initializeOrg({
  219. ...initializeOrg(),
  220. organization,
  221. router: {
  222. params,
  223. },
  224. });
  225. const WrappedComponent = withDomainRequired(MyComponent);
  226. const {container} = render(
  227. <WrappedComponent
  228. router={router}
  229. location={router.location}
  230. params={params}
  231. routes={router.routes}
  232. routeParams={router.params}
  233. route={route}
  234. />,
  235. {context: routerContext}
  236. );
  237. expect(container).toBeEmptyDOMElement();
  238. expect(window.location.replace).toHaveBeenCalledTimes(1);
  239. expect(window.location.replace).toHaveBeenCalledWith(
  240. 'https://sentry.io/organizations/albertos-apples/issues/?q=123#hash'
  241. );
  242. });
  243. it('redirects to sentryUrl if customer-domains is omitted', function () {
  244. window.__initialData = {
  245. customerDomain: {
  246. subdomain: 'albertos-apples',
  247. organizationUrl: 'https://albertos-apples.sentry.io',
  248. sentryUrl: 'https://sentry.io',
  249. },
  250. features: [],
  251. links: {
  252. organizationUrl: null,
  253. regionUrl: null,
  254. sentryUrl: 'https://sentry.io',
  255. },
  256. } as any;
  257. const organization = TestStubs.Organization({
  258. slug: 'albertos-apples',
  259. features: [],
  260. });
  261. const params = {
  262. orgId: 'albertos-apples',
  263. };
  264. const {router, route, routerContext} = initializeOrg({
  265. ...initializeOrg(),
  266. organization,
  267. router: {
  268. params,
  269. },
  270. });
  271. const WrappedComponent = withDomainRequired(MyComponent);
  272. const {container} = render(
  273. <WrappedComponent
  274. router={router}
  275. location={router.location}
  276. params={params}
  277. routes={router.routes}
  278. routeParams={router.params}
  279. route={route}
  280. />,
  281. {context: routerContext}
  282. );
  283. expect(container).toBeEmptyDOMElement();
  284. expect(window.location.replace).toHaveBeenCalledTimes(1);
  285. expect(window.location.replace).toHaveBeenCalledWith(
  286. 'https://sentry.io/organizations/albertos-apples/issues/?q=123#hash'
  287. );
  288. });
  289. it('renders when window.__initialData.customerDomain and customer-domains feature is present', function () {
  290. window.__initialData = {
  291. customerDomain: {
  292. subdomain: 'albertos-apples',
  293. organizationUrl: 'https://albertos-apples.sentry.io',
  294. sentryUrl: 'https://sentry.io',
  295. },
  296. features: ['organizations:customer-domains'],
  297. links: {
  298. organizationUrl: 'https://albertos-apples.sentry.io',
  299. regionUrl: 'https://eu.sentry.io',
  300. sentryUrl: 'https://sentry.io',
  301. },
  302. } as any;
  303. const organization = TestStubs.Organization({
  304. slug: 'albertos-apples',
  305. features: [],
  306. });
  307. const params = {
  308. orgId: 'albertos-apples',
  309. };
  310. const {router, route, routerContext} = initializeOrg({
  311. ...initializeOrg(),
  312. organization,
  313. router: {
  314. params,
  315. },
  316. });
  317. const WrappedComponent = withDomainRequired(MyComponent);
  318. render(
  319. <WrappedComponent
  320. router={router}
  321. location={router.location}
  322. params={params}
  323. routes={router.routes}
  324. routeParams={router.params}
  325. route={route}
  326. />,
  327. {context: routerContext}
  328. );
  329. expect(screen.getByText('Org slug: albertos-apples')).toBeInTheDocument();
  330. expect(window.location.replace).toHaveBeenCalledTimes(0);
  331. });
  332. });