routes.spec.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import type {RouteComponent} from 'react-router';
  2. import {createRoutes} from 'react-router';
  3. import * as constants from 'sentry/constants';
  4. import {buildRoutes} from 'sentry/routes';
  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. // Workaround react-router PlainRoute type not covering redirect routes.
  19. type RouteShape = {
  20. childRoutes?: RouteShape[];
  21. component?: RouteComponent;
  22. from?: string;
  23. path?: string;
  24. };
  25. type RouteMetadata = {
  26. leadingPath: string;
  27. route: RouteShape;
  28. };
  29. function extractRoutes(rootRoute: any): Record<string, RouteComponent> {
  30. const routeTree = createRoutes(rootRoute);
  31. const routeMap: Record<string, RouteComponent> = {};
  32. // A queue of routes we need to visit
  33. const visitQueue: RouteMetadata[] = [{leadingPath: '', route: routeTree[0]}];
  34. while (visitQueue.length > 0) {
  35. const current = visitQueue.pop();
  36. if (!current) {
  37. break;
  38. }
  39. let leading = current.leadingPath;
  40. if (current.route.path?.startsWith('/')) {
  41. leading = '';
  42. }
  43. const currentPath = `${leading}${current.route.path ?? ''}`.replace('//', '/');
  44. if (current.route.childRoutes) {
  45. for (const childRoute of current.route.childRoutes ?? []) {
  46. visitQueue.push({
  47. leadingPath: currentPath,
  48. route: childRoute,
  49. });
  50. }
  51. } else {
  52. if (current.route.from) {
  53. // Redirect routes are not relevant to us.
  54. continue;
  55. }
  56. // We are on a terminal route in the tree. Add to the map of route components.
  57. // We are less interested in container route components.
  58. if (current.route.component) {
  59. routeMap[currentPath] = current.route.component;
  60. }
  61. }
  62. }
  63. return routeMap;
  64. }
  65. describe('buildRoutes()', function () {
  66. // Until customer-domains is enabled for single-tenant, self-hosted and path
  67. // based slug routes are removed we need to ensure
  68. // that each orgId route also has slugless path.
  69. test('orgId routes also have domain routes', function () {
  70. const spy = jest.spyOn(constants, 'USING_CUSTOMER_DOMAIN', 'get');
  71. // Get routes for with customer domains off.
  72. spy.mockReturnValue(false);
  73. const routeMap = extractRoutes(buildRoutes());
  74. // Get routes with customer domains on.
  75. spy.mockReturnValue(true);
  76. const domainRoutes = extractRoutes(buildRoutes());
  77. // All routes that exist under orgId path slugs should
  78. // have a sibling under customer-domains.
  79. const mismatch: Array<{domain: string; slug: string}> = [];
  80. for (const path in routeMap) {
  81. // Normalize the URLs so that we know the path we're looking for.
  82. const domainPath = normalizeUrl(path, {forceCustomerDomain: true});
  83. // Path is not different under customer domains.
  84. if (domainPath === path) {
  85. continue;
  86. }
  87. if (!domainRoutes[domainPath]) {
  88. mismatch.push({slug: path, domain: domainPath});
  89. }
  90. }
  91. if (mismatch.length > 0) {
  92. const routelist = mismatch
  93. .map(item => `- slug: ${item.slug}\n domain: ${item.domain}`)
  94. .join('\n');
  95. throw new Error(
  96. `Unable to find matching URLs for the following ${mismatch.length} routes:\n\n` +
  97. routelist +
  98. '\n\nEach route with the :orgId parameter is expected to have corresponding domain based route as well. ' +
  99. 'If you need help with this drop by #proj-hybrid-cloud.'
  100. );
  101. }
  102. });
  103. });