routes.spec.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import type {RouteObject} from 'react-router-dom';
  2. import * as constants from 'sentry/constants';
  3. import {buildRoutes} from 'sentry/routes';
  4. import {buildReactRouter6Routes} from 'sentry/utils/reactRouter6Compat/router';
  5. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  6. // Setup a module mock so that we can replace
  7. // USING_CUSTOMER_DOMAIN with a getter.
  8. jest.mock('sentry/constants', () => {
  9. const originalModule = jest.requireActual('sentry/constants');
  10. return {
  11. __esModule: true,
  12. ...originalModule,
  13. get USING_CUSTOMER_DOMAIN() {
  14. return false;
  15. },
  16. };
  17. });
  18. type RouteMetadata = {
  19. leadingPath: string;
  20. route: RouteObject;
  21. };
  22. function extractRoutes(rootRoute: RouteObject[]): Set<string> {
  23. const routes = new Set<string>();
  24. // A queue of routes we need to visit
  25. const visitQueue: RouteMetadata[] = [{leadingPath: '', route: rootRoute[0]}];
  26. while (visitQueue.length > 0) {
  27. const current = visitQueue.pop();
  28. if (!current) {
  29. break;
  30. }
  31. const leading = current.leadingPath;
  32. const currentPath = `${leading}${current.route.path ?? ''}`.replace('//', '/');
  33. if (current.route.children) {
  34. for (const childRoute of current.route.children ?? []) {
  35. visitQueue.push({
  36. leadingPath: currentPath,
  37. route: childRoute,
  38. });
  39. }
  40. }
  41. if (
  42. current.route.element &&
  43. (
  44. current.route.element as React.ReactElement<any, React.NamedExoticComponent>
  45. ).type.displayName?.endsWith('Redirect')
  46. ) {
  47. // Redirect routes are not relevant to us.
  48. continue;
  49. }
  50. // We are on a terminal route in the tree. Add to the map of route components.
  51. // We are less interested in container route components.
  52. if (current.route.element) {
  53. routes.add(currentPath);
  54. }
  55. }
  56. return routes;
  57. }
  58. describe('buildRoutes()', function () {
  59. // Until customer-domains is enabled for single-tenant, self-hosted and path
  60. // based slug routes are removed we need to ensure
  61. // that each orgId route also has slugless path.
  62. test('orgId routes also have domain routes', function () {
  63. const spy = jest.spyOn(constants, 'USING_CUSTOMER_DOMAIN', 'get');
  64. // Get routes for with customer domains off.
  65. spy.mockReturnValue(false);
  66. const routes = extractRoutes(buildReactRouter6Routes(buildRoutes()));
  67. // Get routes with customer domains on.
  68. spy.mockReturnValue(true);
  69. const domainRoutes = extractRoutes(buildReactRouter6Routes(buildRoutes()));
  70. // All routes that exist under orgId path slugs should
  71. // have a sibling under customer-domains.
  72. const mismatch: Array<{domain: string; slug: string}> = [];
  73. for (const path of routes) {
  74. // Normalize the URLs so that we know the path we're looking for.
  75. const domainPath = normalizeUrl(path, {forceCustomerDomain: true});
  76. // Path is not different under customer domains.
  77. if (domainPath === path) {
  78. continue;
  79. }
  80. if (!domainRoutes.has(domainPath)) {
  81. mismatch.push({slug: path, domain: domainPath});
  82. }
  83. }
  84. if (mismatch.length > 0) {
  85. const routelist = mismatch
  86. .map(item => `- slug: ${item.slug}\n domain: ${item.domain}`)
  87. .join('\n');
  88. throw new Error(
  89. `Unable to find matching URLs for the following ${mismatch.length} routes:\n\n` +
  90. routelist +
  91. '\n\nEach route with the :orgId parameter is expected to have corresponding domain based route as well. ' +
  92. 'If you need help with this drop by #proj-hybrid-cloud.'
  93. );
  94. }
  95. });
  96. });