relocation.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import {browserHistory} from 'react-router';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {
  4. fireEvent,
  5. render,
  6. screen,
  7. userEvent,
  8. waitFor,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  11. import ConfigStore from 'sentry/stores/configStore';
  12. import Relocation from 'sentry/views/relocation/relocation';
  13. jest.mock('sentry/actionCreators/indicator');
  14. const fakePublicKey = `-----BEGIN PUBLIC KEY-----
  15. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5Or1zsGE1XJTL4q+1c4
  16. Ztu8+7SC/exrnEYlWH+LVLI8TVyuGwDTAXrgKHGwaMM5ZnjijP5i8+ph8lfLrybT
  17. l+2D81qPIqagEtNMDaHqUDm5Tq7I2qvxkJ5YuDLawRUPccKMwWlIDR2Gvfe3efce
  18. 870EicPsExz4uPOkNXGHJZ/FwCQrLo87MXFeqrqj+0Cf+qwCQSCW9qFWe5cj+zqt
  19. eeJa0qflcHHQzxK4/EKKpl/hkt4zi0aE/PuJgvJz2KB+X3+LzekTy90LzW3VhR4y
  20. IAxCAaGQJVsg9dhKOORjAf4XK9aXHvy/jUSyT43opj6AgNqXlKEQjb1NBA8qbJJS
  21. 8wIDAQAB
  22. -----END PUBLIC KEY-----`;
  23. const fakeRegionName = 'Narnia';
  24. const fakeRegionUrl = 'https://example.com';
  25. describe('Relocation', function () {
  26. let fetchExistingRelocation: jest.Mock;
  27. let fetchPublicKey: jest.Mock;
  28. beforeEach(function () {
  29. MockApiClient.asyncDelay = undefined;
  30. MockApiClient.clearMockResponses();
  31. fetchExistingRelocation = MockApiClient.addMockResponse({
  32. url: '/relocations/',
  33. body: [],
  34. });
  35. fetchPublicKey = MockApiClient.addMockResponse({
  36. url: '/publickeys/relocations/',
  37. body: {
  38. public_key: fakePublicKey,
  39. },
  40. });
  41. // The tests fail because we have a "component update was not wrapped in act" error. It should
  42. // be safe to ignore this error, but we should remove the mock once we move to react testing
  43. // library.
  44. //
  45. // eslint-disable-next-line no-console
  46. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  47. });
  48. afterEach(function () {
  49. MockApiClient.clearMockResponses();
  50. MockApiClient.asyncDelay = undefined;
  51. });
  52. function renderPage(step) {
  53. const routeParams = {
  54. step,
  55. };
  56. const {routerProps, routerContext, organization} = initializeOrg({
  57. router: {
  58. params: routeParams,
  59. },
  60. });
  61. return render(<Relocation {...routerProps} />, {
  62. context: routerContext,
  63. organization,
  64. });
  65. }
  66. async function waitForRenderSuccess(step) {
  67. renderPage(step);
  68. await waitFor(() => expect(screen.getByTestId(step)).toBeInTheDocument());
  69. }
  70. async function waitForRenderError(step) {
  71. renderPage(step);
  72. await waitFor(() => expect(screen.getByTestId('loading-error')).toBeInTheDocument());
  73. }
  74. describe('Get Started', function () {
  75. it('renders', async function () {
  76. await waitForRenderSuccess('get-started');
  77. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  78. expect(
  79. await screen.findByText('Basic information needed to get started')
  80. ).toBeInTheDocument();
  81. expect(
  82. await screen.findByText('Organization slugs being relocated')
  83. ).toBeInTheDocument();
  84. expect(await screen.findByText('Choose a datacenter region')).toBeInTheDocument();
  85. });
  86. it('redirects to `in-progress` page if user already has active relocation', async function () {
  87. MockApiClient.clearMockResponses();
  88. fetchExistingRelocation = MockApiClient.addMockResponse({
  89. url: '/relocations/',
  90. body: [
  91. {
  92. uuid: 'ccef828a-03d8-4dd0-918a-487ffecf8717',
  93. status: 'IN_PROGRESS',
  94. },
  95. ],
  96. });
  97. fetchPublicKey = MockApiClient.addMockResponse({
  98. url: '/publickeys/relocations/',
  99. body: {
  100. public_key: fakePublicKey,
  101. },
  102. });
  103. await waitForRenderSuccess('get-started');
  104. await waitFor(() => expect(fetchExistingRelocation).toHaveBeenCalled());
  105. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  106. expect(browserHistory.push).toHaveBeenCalledWith('/relocation/in-progress/');
  107. });
  108. it('should prevent user from going to the next step if no org slugs or region are entered', async function () {
  109. await waitForRenderSuccess('get-started');
  110. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  111. expect(await screen.getByRole('button', {name: 'Continue'})).toBeDisabled();
  112. });
  113. it('should be allowed to go to next step if org slug is entered and region is selected', async function () {
  114. await waitForRenderSuccess('get-started');
  115. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  116. ConfigStore.set('regions', [{name: fakeRegionName, url: fakeRegionUrl}]);
  117. ConfigStore.set('relocationConfig', {selectableRegions: [fakeRegionName]});
  118. const orgSlugsInput = await screen.getByLabelText('org-slugs');
  119. const continueButton = await screen.getByRole('button', {name: 'Continue'});
  120. await userEvent.type(orgSlugsInput, 'test-org');
  121. await userEvent.type(await screen.getByLabelText('region'), 'Narnia');
  122. await userEvent.click(await screen.getByRole('menuitemradio'));
  123. expect(continueButton).toBeEnabled();
  124. });
  125. it('should show loading indicator and error message if existing relocation retrieval failed', async function () {
  126. MockApiClient.clearMockResponses();
  127. fetchExistingRelocation = MockApiClient.addMockResponse({
  128. url: '/relocations/',
  129. statusCode: 400,
  130. });
  131. fetchPublicKey = MockApiClient.addMockResponse({
  132. url: '/publickeys/relocations/',
  133. body: {
  134. public_key: fakePublicKey,
  135. },
  136. });
  137. await waitForRenderError('get-started');
  138. await waitFor(() => expect(fetchExistingRelocation).toHaveBeenCalled());
  139. expect(fetchPublicKey).toHaveBeenCalledTimes(1);
  140. expect(
  141. await screen.queryByRole('button', {name: 'Continue'})
  142. ).not.toBeInTheDocument();
  143. expect(await screen.queryByLabelText('org-slugs')).not.toBeInTheDocument();
  144. expect(await screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
  145. MockApiClient.addMockResponse({
  146. url: '/relocations/',
  147. body: [],
  148. });
  149. await userEvent.click(screen.getByRole('button', {name: 'Retry'}));
  150. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  151. await waitFor(() => expect(screen.getByTestId('get-started')).toBeInTheDocument());
  152. expect(fetchExistingRelocation).toHaveBeenCalledTimes(1);
  153. expect(await screen.queryByLabelText('org-slugs')).toBeInTheDocument();
  154. expect(await screen.queryByRole('button', {name: 'Continue'})).toBeInTheDocument();
  155. });
  156. });
  157. describe('Public Key', function () {
  158. it('should show instructions if key retrieval was successful', async function () {
  159. await waitForRenderSuccess('public-key');
  160. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  161. expect(
  162. await screen.findByText("Save Sentry's public key to your machine")
  163. ).toBeInTheDocument();
  164. expect(await screen.getByText('key.pub')).toBeInTheDocument();
  165. expect(await screen.getByRole('button', {name: 'Continue'})).toBeInTheDocument();
  166. });
  167. it('should show loading indicator if key retrieval still in progress', function () {
  168. MockApiClient.asyncDelay = 1;
  169. renderPage('public-key');
  170. expect(screen.queryByRole('button', {name: 'Continue'})).not.toBeInTheDocument();
  171. expect(screen.queryByText('key.pub')).not.toBeInTheDocument();
  172. });
  173. it('should show loading indicator and error message if key retrieval failed', async function () {
  174. MockApiClient.clearMockResponses();
  175. fetchExistingRelocation = MockApiClient.addMockResponse({
  176. url: '/relocations/',
  177. body: [],
  178. });
  179. fetchPublicKey = MockApiClient.addMockResponse({
  180. url: '/publickeys/relocations/',
  181. statusCode: 400,
  182. });
  183. await waitForRenderError('public-key');
  184. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  185. expect(fetchExistingRelocation).toHaveBeenCalledTimes(1);
  186. expect(
  187. await screen.queryByRole('button', {name: 'Continue'})
  188. ).not.toBeInTheDocument();
  189. expect(await screen.queryByText('key.pub')).not.toBeInTheDocument();
  190. expect(await screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
  191. MockApiClient.addMockResponse({
  192. url: '/publickeys/relocations/',
  193. body: {
  194. public_key: fakePublicKey,
  195. },
  196. });
  197. await userEvent.click(screen.getByRole('button', {name: 'Retry'}));
  198. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  199. await waitFor(() => expect(screen.getByTestId('public-key')).toBeInTheDocument());
  200. expect(fetchExistingRelocation).toHaveBeenCalledTimes(1);
  201. expect(await screen.queryByText('key.pub')).toBeInTheDocument();
  202. expect(await screen.queryByRole('button', {name: 'Continue'})).toBeInTheDocument();
  203. });
  204. });
  205. describe('Encrypt Backup', function () {
  206. it('renders', async function () {
  207. await waitForRenderSuccess('encrypt-backup');
  208. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  209. expect(
  210. await screen.findByText(
  211. 'Create an encrypted backup of your current self-hosted instance'
  212. )
  213. ).toBeInTheDocument();
  214. });
  215. });
  216. describe('Upload Backup', function () {
  217. it('renders', async function () {
  218. await waitForRenderSuccess('upload-backup');
  219. expect(
  220. await screen.findByText('Upload Tarball to begin the relocation process')
  221. ).toBeInTheDocument();
  222. });
  223. it('accepts a file upload', async function () {
  224. await waitForRenderSuccess('upload-backup');
  225. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  226. const input = screen.getByLabelText('file-upload');
  227. await userEvent.upload(input, relocationFile);
  228. expect(await screen.findByText('hello.tar')).toBeInTheDocument();
  229. expect(await screen.findByText('Start Relocation')).toBeInTheDocument();
  230. });
  231. it('accepts a file upload through drag and drop', async function () {
  232. await waitForRenderSuccess('upload-backup');
  233. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  234. const dropzone = screen.getByLabelText('dropzone');
  235. fireEvent.drop(dropzone, {dataTransfer: {files: [relocationFile]}});
  236. expect(await screen.findByText('hello.tar')).toBeInTheDocument();
  237. expect(await screen.findByText('Start Relocation')).toBeInTheDocument();
  238. });
  239. it('correctly removes file and prompts for file upload', async function () {
  240. await waitForRenderSuccess('upload-backup');
  241. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  242. const input = screen.getByLabelText('file-upload');
  243. await userEvent.upload(input, relocationFile);
  244. await userEvent.click(screen.getByText('Remove file'));
  245. expect(screen.queryByText('hello.tar')).not.toBeInTheDocument();
  246. expect(
  247. await screen.findByText('Upload Tarball to begin the relocation process')
  248. ).toBeInTheDocument();
  249. });
  250. it('fails to starts relocation job if some form data is missing', async function () {
  251. const mockapi = MockApiClient.addMockResponse({
  252. url: `/relocations/`,
  253. method: 'POST',
  254. });
  255. await waitForRenderSuccess('upload-backup');
  256. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  257. const input = screen.getByLabelText('file-upload');
  258. await userEvent.upload(input, relocationFile);
  259. await userEvent.click(await screen.findByText('Start Relocation'));
  260. await waitFor(() => expect(mockapi).not.toHaveBeenCalled());
  261. expect(addErrorMessage).toHaveBeenCalledWith(
  262. 'An error has occurred while trying to start relocation job. Please contact support for further assistance.'
  263. );
  264. });
  265. it('starts relocation job if form data is available from previous steps', async function () {
  266. const mockapi = MockApiClient.addMockResponse({
  267. url: `/relocations/`,
  268. method: 'POST',
  269. responseJSON: [
  270. {
  271. uuid: 'ccef828a-03d8-4dd0-918a-487ffecf8717',
  272. status: 'IN_PROGRESS',
  273. },
  274. ],
  275. });
  276. await waitForRenderSuccess('get-started');
  277. ConfigStore.set('regions', [{name: fakeRegionName, url: fakeRegionUrl}]);
  278. ConfigStore.set('relocationConfig', {selectableRegions: [fakeRegionName]});
  279. const orgSlugsInput = await screen.getByLabelText('org-slugs');
  280. const continueButton = await screen.getByRole('button', {name: 'Continue'});
  281. await userEvent.type(orgSlugsInput, 'test-org');
  282. await userEvent.type(screen.getByLabelText('region'), 'Narnia');
  283. await userEvent.click(screen.getByRole('menuitemradio'));
  284. await userEvent.click(continueButton);
  285. await waitForRenderSuccess('upload-backup');
  286. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  287. const input = screen.getByLabelText('file-upload');
  288. await userEvent.upload(input, relocationFile);
  289. await userEvent.click(await screen.findByText('Start Relocation'));
  290. await waitFor(() =>
  291. expect(mockapi).toHaveBeenCalledWith(
  292. '/relocations/',
  293. expect.objectContaining({host: fakeRegionUrl, method: 'POST'})
  294. )
  295. );
  296. expect(addSuccessMessage).toHaveBeenCalledWith(
  297. "Your relocation has started - we'll email you with updates as soon as we have 'em!"
  298. );
  299. await waitForRenderSuccess('in-progress');
  300. });
  301. it('throws error if user already has an in-progress relocation job', async function () {
  302. const mockapi = MockApiClient.addMockResponse({
  303. url: `/relocations/`,
  304. method: 'POST',
  305. statusCode: 409,
  306. });
  307. await waitForRenderSuccess('get-started');
  308. ConfigStore.set('regions', [{name: fakeRegionName, url: fakeRegionUrl}]);
  309. ConfigStore.set('relocationConfig', {selectableRegions: [fakeRegionName]});
  310. const orgSlugsInput = screen.getByLabelText('org-slugs');
  311. const continueButton = screen.getByRole('button', {name: 'Continue'});
  312. await userEvent.type(orgSlugsInput, 'test-org');
  313. await userEvent.type(screen.getByLabelText('region'), 'Narnia');
  314. await userEvent.click(screen.getByRole('menuitemradio'));
  315. await userEvent.click(continueButton);
  316. await waitForRenderSuccess('upload-backup');
  317. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  318. const input = screen.getByLabelText('file-upload');
  319. await userEvent.upload(input, relocationFile);
  320. await userEvent.click(await screen.findByText('Start Relocation'));
  321. await waitFor(() => expect(mockapi).toHaveBeenCalled());
  322. expect(addErrorMessage).toHaveBeenCalledWith(
  323. 'You already have an in-progress relocation job.'
  324. );
  325. });
  326. it('throws error if daily limit of relocations has been reached', async function () {
  327. const mockapi = MockApiClient.addMockResponse({
  328. url: `/relocations/`,
  329. method: 'POST',
  330. statusCode: 429,
  331. });
  332. await waitForRenderSuccess('get-started');
  333. ConfigStore.set('regions', [{name: fakeRegionName, url: fakeRegionUrl}]);
  334. ConfigStore.set('relocationConfig', {selectableRegions: [fakeRegionName]});
  335. const orgSlugsInput = screen.getByLabelText('org-slugs');
  336. const continueButton = screen.getByRole('button', {name: 'Continue'});
  337. await userEvent.type(orgSlugsInput, 'test-org');
  338. await userEvent.type(screen.getByLabelText('region'), 'Narnia');
  339. await userEvent.click(screen.getByRole('menuitemradio'));
  340. await userEvent.click(continueButton);
  341. await waitForRenderSuccess('upload-backup');
  342. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  343. const input = screen.getByLabelText('file-upload');
  344. await userEvent.upload(input, relocationFile);
  345. await userEvent.click(await screen.findByText('Start Relocation'));
  346. await waitFor(() => expect(mockapi).toHaveBeenCalled());
  347. expect(addErrorMessage).toHaveBeenCalledWith(
  348. 'We have reached the daily limit of relocations - please try again tomorrow, or contact support.'
  349. );
  350. });
  351. it('throws error if user session has expired', async function () {
  352. const mockapi = MockApiClient.addMockResponse({
  353. url: `/relocations/`,
  354. method: 'POST',
  355. statusCode: 401,
  356. });
  357. ConfigStore.set('regions', [{name: fakeRegionName, url: fakeRegionUrl}]);
  358. ConfigStore.set('relocationConfig', {selectableRegions: [fakeRegionName]});
  359. await waitForRenderSuccess('get-started');
  360. const orgSlugsInput = screen.getByLabelText('org-slugs');
  361. const continueButton = screen.getByRole('button', {name: 'Continue'});
  362. await userEvent.type(orgSlugsInput, 'test-org');
  363. await userEvent.type(screen.getByLabelText('region'), 'Narnia');
  364. await userEvent.click(screen.getByRole('menuitemradio'));
  365. await userEvent.click(continueButton);
  366. await waitForRenderSuccess('upload-backup');
  367. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  368. const input = screen.getByLabelText('file-upload');
  369. await userEvent.upload(input, relocationFile);
  370. await userEvent.click(await screen.findByText('Start Relocation'));
  371. await waitFor(() => expect(mockapi).toHaveBeenCalled());
  372. expect(addErrorMessage).toHaveBeenCalledWith('Your session has expired.');
  373. });
  374. it('throws error for 500 error', async function () {
  375. const mockapi = MockApiClient.addMockResponse({
  376. url: `/relocations/`,
  377. method: 'POST',
  378. statusCode: 500,
  379. });
  380. ConfigStore.set('regions', [{name: fakeRegionName, url: fakeRegionUrl}]);
  381. ConfigStore.set('relocationConfig', {selectableRegions: [fakeRegionName]});
  382. await waitForRenderSuccess('get-started');
  383. const orgSlugsInput = screen.getByLabelText('org-slugs');
  384. const continueButton = screen.getByRole('button', {name: 'Continue'});
  385. await userEvent.type(orgSlugsInput, 'test-org');
  386. await userEvent.type(screen.getByLabelText('region'), 'Narnia');
  387. await userEvent.click(screen.getByRole('menuitemradio'));
  388. await userEvent.click(continueButton);
  389. await waitForRenderSuccess('upload-backup');
  390. const relocationFile = new File(['hello'], 'hello.tar', {type: 'file'});
  391. const input = screen.getByLabelText('file-upload');
  392. await userEvent.upload(input, relocationFile);
  393. await userEvent.click(await screen.findByText('Start Relocation'));
  394. await waitFor(() => expect(mockapi).toHaveBeenCalled());
  395. expect(addErrorMessage).toHaveBeenCalledWith(
  396. 'An error has occurred while trying to start relocation job. Please contact support for further assistance.'
  397. );
  398. });
  399. });
  400. describe('In Progress', function () {
  401. it('renders', async function () {
  402. MockApiClient.clearMockResponses();
  403. fetchExistingRelocation = MockApiClient.addMockResponse({
  404. url: '/relocations/',
  405. body: [
  406. {
  407. uuid: 'ccef828a-03d8-4dd0-918a-487ffecf8717',
  408. status: 'IN_PROGRESS',
  409. },
  410. ],
  411. });
  412. fetchPublicKey = MockApiClient.addMockResponse({
  413. url: '/publickeys/relocations/',
  414. body: {
  415. public_key: fakePublicKey,
  416. },
  417. });
  418. await waitForRenderSuccess('in-progress');
  419. expect(
  420. await screen.findByText('Your relocation is under way!')
  421. ).toBeInTheDocument();
  422. });
  423. it('redirects to `get-started` page if there is no existing relocation', async function () {
  424. await waitForRenderSuccess('in-progress');
  425. await waitFor(() => expect(fetchExistingRelocation).toHaveBeenCalled());
  426. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  427. expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/');
  428. });
  429. it('redirects to `get-started` page if there is no active relocation', async function () {
  430. MockApiClient.clearMockResponses();
  431. fetchExistingRelocation = MockApiClient.addMockResponse({
  432. url: '/relocations/',
  433. body: [
  434. {
  435. uuid: 'ccef828a-03d8-4dd0-918a-487ffecf8717',
  436. status: 'SUCCESS',
  437. },
  438. ],
  439. });
  440. fetchPublicKey = MockApiClient.addMockResponse({
  441. url: '/publickeys/relocations/',
  442. body: {
  443. public_key: fakePublicKey,
  444. },
  445. });
  446. await waitForRenderSuccess('in-progress');
  447. await waitFor(() => expect(fetchExistingRelocation).toHaveBeenCalled());
  448. await waitFor(() => expect(fetchPublicKey).toHaveBeenCalled());
  449. expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/');
  450. });
  451. });
  452. });