organizationContext.spec.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import {Component} from 'react';
  2. import type {RouteContextInterface} from 'react-router';
  3. import * as Sentry from '@sentry/react';
  4. import {LocationFixture} from 'sentry-fixture/locationFixture';
  5. import {OrganizationFixture} from 'sentry-fixture/organization';
  6. import {ProjectFixture} from 'sentry-fixture/project';
  7. import {RouterFixture} from 'sentry-fixture/routerFixture';
  8. import {TeamFixture} from 'sentry-fixture/team';
  9. import {UserFixture} from 'sentry-fixture/user';
  10. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  11. import * as orgsActionCreators from 'sentry/actionCreators/organizations';
  12. import {openSudo} from 'sentry/actionCreators/sudoModal';
  13. import {SentryPropTypeValidators} from 'sentry/sentryPropTypeValidators';
  14. import ConfigStore from 'sentry/stores/configStore';
  15. import OrganizationStore from 'sentry/stores/organizationStore';
  16. import ProjectsStore from 'sentry/stores/projectsStore';
  17. import TeamStore from 'sentry/stores/teamStore';
  18. import type {Organization} from 'sentry/types';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import {OrganizationContextProvider, useEnsureOrganization} from './organizationContext';
  21. import {RouteContext} from './routeContext';
  22. jest.mock('sentry/actionCreators/sudoModal');
  23. describe('OrganizationContext', function () {
  24. let getOrgMock: jest.Mock;
  25. let getProjectsMock: jest.Mock;
  26. let getTeamsMock: jest.Mock;
  27. const organization = OrganizationFixture();
  28. const project = ProjectFixture();
  29. const team = TeamFixture();
  30. const router: RouteContextInterface = {
  31. router: RouterFixture(),
  32. location: LocationFixture(),
  33. routes: [],
  34. params: {orgId: organization.slug},
  35. };
  36. function setupOrgMocks(org: Organization) {
  37. const orgMock = MockApiClient.addMockResponse({
  38. url: `/organizations/${org.slug}/`,
  39. body: org,
  40. });
  41. const projectMock = MockApiClient.addMockResponse({
  42. url: `/organizations/${org.slug}/projects/`,
  43. body: [project],
  44. });
  45. const teamMock = MockApiClient.addMockResponse({
  46. url: `/organizations/${org.slug}/teams/`,
  47. body: [team],
  48. });
  49. return {orgMock, projectMock, teamMock};
  50. }
  51. beforeEach(function () {
  52. MockApiClient.clearMockResponses();
  53. const {orgMock, projectMock, teamMock} = setupOrgMocks(organization);
  54. getOrgMock = orgMock;
  55. getProjectsMock = projectMock;
  56. getTeamsMock = teamMock;
  57. jest.spyOn(TeamStore, 'loadInitialData');
  58. jest.spyOn(ProjectsStore, 'loadInitialData');
  59. ConfigStore.init();
  60. OrganizationStore.reset();
  61. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  62. });
  63. afterEach(function () {
  64. // eslint-disable-next-line no-console
  65. jest.mocked(console.error).mockRestore();
  66. });
  67. function OrganizationLoaderStub() {
  68. useEnsureOrganization();
  69. return null;
  70. }
  71. /**
  72. * Used to test that the organization context is propegated
  73. */
  74. function OrganizationName() {
  75. const org = useOrganization({allowNull: true});
  76. return <div>{org?.slug ?? 'no-org'}</div>;
  77. }
  78. /**
  79. * Used to test the legacy organization context behavior
  80. */
  81. class OrganizationNameLegacyConsumer extends Component {
  82. static contextTypes = {
  83. organization: SentryPropTypeValidators.isObject,
  84. };
  85. declare context: {organization: Organization | undefined};
  86. render() {
  87. return <div>{this.context.organization?.slug ?? 'no-org'}</div>;
  88. }
  89. }
  90. it('fetches org, projects, teams, and provides organization context', async function () {
  91. render(
  92. <OrganizationContextProvider>
  93. <OrganizationLoaderStub />
  94. <OrganizationName />
  95. </OrganizationContextProvider>
  96. );
  97. expect(await screen.findByText(organization.slug)).toBeInTheDocument();
  98. expect(getOrgMock).toHaveBeenCalled();
  99. expect(getProjectsMock).toHaveBeenCalled();
  100. expect(getTeamsMock).toHaveBeenCalled();
  101. });
  102. it('provides legacy organization context', async function () {
  103. render(
  104. <OrganizationContextProvider>
  105. <OrganizationLoaderStub />
  106. <OrganizationNameLegacyConsumer />
  107. </OrganizationContextProvider>
  108. );
  109. expect(await screen.findByText(organization.slug)).toBeInTheDocument();
  110. expect(Sentry.captureMessage).toHaveBeenCalledWith('Legacy organization accessed!');
  111. });
  112. it('does not fetch if organization is already set', async function () {
  113. OrganizationStore.onUpdate(organization);
  114. render(
  115. <OrganizationContextProvider>
  116. <OrganizationLoaderStub />
  117. <OrganizationName />
  118. </OrganizationContextProvider>
  119. );
  120. expect(await screen.findByText(organization.slug)).toBeInTheDocument();
  121. expect(getOrgMock).not.toHaveBeenCalled();
  122. });
  123. it('fetches new org when router params change', async function () {
  124. // First render with org-slug
  125. const {rerender} = render(
  126. <RouteContext.Provider value={router}>
  127. <OrganizationContextProvider>
  128. <OrganizationLoaderStub />
  129. <OrganizationName />
  130. </OrganizationContextProvider>
  131. </RouteContext.Provider>
  132. );
  133. expect(await screen.findByText(organization.slug)).toBeInTheDocument();
  134. const anotherOrg = OrganizationFixture({slug: 'another-org'});
  135. const {orgMock, projectMock, teamMock} = setupOrgMocks(anotherOrg);
  136. const switchOrganization = jest.spyOn(orgsActionCreators, 'switchOrganization');
  137. // re-render with another-org
  138. rerender(
  139. <RouteContext.Provider value={{...router, params: {orgId: 'another-org'}}}>
  140. <OrganizationContextProvider>
  141. <OrganizationLoaderStub />
  142. <OrganizationName />
  143. </OrganizationContextProvider>
  144. </RouteContext.Provider>
  145. );
  146. expect(await screen.findByText(anotherOrg.slug)).toBeInTheDocument();
  147. expect(orgMock).toHaveBeenCalled();
  148. expect(projectMock).toHaveBeenCalled();
  149. expect(teamMock).toHaveBeenCalled();
  150. expect(switchOrganization).toHaveBeenCalled();
  151. });
  152. it('opens sudo modal for superusers for nonmember org with active staff', async function () {
  153. ConfigStore.set('user', UserFixture({isSuperuser: true, isStaff: true}));
  154. organization.access = [];
  155. getOrgMock = MockApiClient.addMockResponse({
  156. url: `/organizations/${organization.slug}/`,
  157. body: organization,
  158. });
  159. render(
  160. <OrganizationContextProvider>
  161. <OrganizationLoaderStub />
  162. <OrganizationName />
  163. </OrganizationContextProvider>
  164. );
  165. await waitFor(() => !OrganizationStore.getState().loading);
  166. await waitFor(() => expect(openSudo).toHaveBeenCalled());
  167. });
  168. it('opens sudo modal for superusers on 403s', async function () {
  169. ConfigStore.set('user', UserFixture({isSuperuser: true}));
  170. getOrgMock = MockApiClient.addMockResponse({
  171. url: '/organizations/org-slug/',
  172. statusCode: 403,
  173. });
  174. render(
  175. <OrganizationContextProvider>
  176. <OrganizationLoaderStub />
  177. <OrganizationName />
  178. </OrganizationContextProvider>
  179. );
  180. await waitFor(() => !OrganizationStore.getState().loading);
  181. // eslint-disable-next-line no-console
  182. await waitFor(() => expect(console.error).toHaveBeenCalled());
  183. expect(openSudo).toHaveBeenCalled();
  184. });
  185. /**
  186. * This test will rarely happen since most configurations are now using customer domains
  187. */
  188. it('uses last organization slug from ConfigStore', async function () {
  189. const configStoreOrg = OrganizationFixture({slug: 'config-store-org'});
  190. ConfigStore.set('lastOrganization', configStoreOrg.slug);
  191. const {orgMock, projectMock, teamMock} = setupOrgMocks(configStoreOrg);
  192. // orgId is not present in the router.
  193. render(
  194. <RouteContext.Provider value={{...router, params: {}}}>
  195. <OrganizationContextProvider>
  196. <OrganizationLoaderStub />
  197. <OrganizationName />
  198. </OrganizationContextProvider>
  199. </RouteContext.Provider>
  200. );
  201. expect(await screen.findByText(configStoreOrg.slug)).toBeInTheDocument();
  202. expect(orgMock).toHaveBeenCalled();
  203. expect(projectMock).toHaveBeenCalled();
  204. expect(teamMock).toHaveBeenCalled();
  205. });
  206. });