ChooseProvider.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. /*
  2. Copyright (C) 2023 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import React from "react";
  15. import { Endpoint } from "@src/@types/Endpoint";
  16. import { ThemePalette } from "@src/components/Theme";
  17. import notificationStore from "@src/stores/NotificationStore";
  18. import FileUtils from "@src/utils/FileUtils";
  19. import { fireEvent, render, waitFor } from "@testing-library/react";
  20. import TestUtils from "@tests/TestUtils";
  21. import ChooseProvider from "./ChooseProvider";
  22. const OPENSTACK_ENDPOINT: Endpoint = {
  23. name: "Openstack",
  24. type: "openstack",
  25. id: "1",
  26. description: "",
  27. created_at: new Date().toISOString(),
  28. mapped_regions: ["region_1"],
  29. connection_info: {},
  30. };
  31. const mockDataTransfer = (files: any[]) => ({
  32. dataTransfer: {
  33. dropEffect: "none",
  34. files,
  35. items: files.map(file => ({
  36. kind: "file",
  37. type: file.type,
  38. getAsFile: () => file,
  39. })),
  40. types: ["Files"],
  41. },
  42. });
  43. jest.mock("react-router", () => ({ Link: "a" }));
  44. jest.mock("@src/stores/NotificationStore", () => ({
  45. alert: jest.fn(),
  46. }));
  47. jest.mock("@src/utils/Config", () => ({
  48. config: {
  49. providerSortPriority: {},
  50. providerNames: {
  51. openstack: "OpenStack",
  52. vmware_vsphere: "VMware vSphere",
  53. },
  54. },
  55. }));
  56. describe("ChooseProvider", () => {
  57. let defaultProps: ChooseProvider["props"];
  58. let readContentFromFileListMock: jest.SpyInstance;
  59. beforeEach(() => {
  60. jest.useFakeTimers();
  61. readContentFromFileListMock = jest.spyOn(
  62. FileUtils,
  63. "readContentFromFileList",
  64. );
  65. readContentFromFileListMock.mockResolvedValue([
  66. {
  67. name: "openstack.endpoint",
  68. content: JSON.stringify(OPENSTACK_ENDPOINT),
  69. },
  70. ]);
  71. defaultProps = {
  72. providers: ["openstack", "vmware_vsphere"],
  73. regions: [
  74. {
  75. id: "region_1",
  76. name: "Region 1",
  77. description: "",
  78. enabled: true,
  79. mapped_endpoints: [],
  80. },
  81. ],
  82. onCancelClick: jest.fn(),
  83. onProviderClick: jest.fn(),
  84. onUploadEndpoint: jest.fn(),
  85. loading: false,
  86. onValidateMultipleEndpoints: jest.fn(),
  87. multiValidating: false,
  88. multiValidation: [],
  89. onRemoveEndpoint: jest.fn(),
  90. onResetValidation: jest.fn(),
  91. };
  92. });
  93. afterEach(() => {
  94. jest.clearAllTimers();
  95. });
  96. it("renders without crashing", () => {
  97. render(<ChooseProvider {...defaultProps} />);
  98. expect(TestUtils.select("Button__")?.textContent).toBe("Cancel");
  99. });
  100. it('calls "onProviderClick" when a provider logo is clicked', () => {
  101. render(<ChooseProvider {...defaultProps} />);
  102. const providerButton = TestUtils.select("EndpointLogos__Wrapper")!;
  103. fireEvent.click(providerButton);
  104. expect(defaultProps.onProviderClick).toHaveBeenCalledWith("openstack");
  105. });
  106. it("shows loading state when loading is true", () => {
  107. render(<ChooseProvider {...defaultProps} loading />);
  108. expect(TestUtils.select("ChooseProvider__LoadingWrapper")).toBeTruthy();
  109. });
  110. it("handles file uploads and parses single files", async () => {
  111. const onUploadEndpointMock = jest.fn();
  112. render(
  113. <ChooseProvider
  114. {...defaultProps}
  115. onUploadEndpoint={onUploadEndpointMock}
  116. />,
  117. );
  118. const fileInput = document.querySelector('input[type="file"]')!;
  119. const file = new File(
  120. [JSON.stringify(OPENSTACK_ENDPOINT)],
  121. "openstack.endpoint",
  122. {
  123. type: "application/json",
  124. },
  125. );
  126. expect(fileInput).toBeTruthy();
  127. expect(defaultProps.onUploadEndpoint).not.toHaveBeenCalled();
  128. fireEvent.change(fileInput, { target: { files: [file] } });
  129. await waitFor(() => expect(onUploadEndpointMock).toHaveBeenCalled());
  130. });
  131. it("highlights dropzone on drag enter", () => {
  132. render(<ChooseProvider {...defaultProps} />);
  133. jest.advanceTimersByTime(1000);
  134. const uploadArea = TestUtils.select("ChooseProvider__Upload")!;
  135. const style = () => window.getComputedStyle(uploadArea);
  136. expect(style().border).toBe(`1px dashed white`);
  137. fireEvent.dragEnter(window, mockDataTransfer([]));
  138. expect(style().border).toBe(
  139. `1px dashed ${ThemePalette.primary.toLowerCase()}`,
  140. );
  141. });
  142. it("removes highlight from dropzone on drag leave", () => {
  143. render(<ChooseProvider {...defaultProps} />);
  144. jest.advanceTimersByTime(1000);
  145. const uploadArea = TestUtils.select("ChooseProvider__Upload")!;
  146. const style = () => window.getComputedStyle(uploadArea);
  147. fireEvent.dragLeave(window, mockDataTransfer([]));
  148. expect(style().border).toBe(`1px dashed white`);
  149. });
  150. it("processes file on drop", async () => {
  151. render(<ChooseProvider {...defaultProps} />);
  152. jest.advanceTimersByTime(1000);
  153. const file = new File(["endpoint content"], "openstack.endpoint", {
  154. type: "application/json",
  155. });
  156. fireEvent.drop(window, mockDataTransfer([file]));
  157. await waitFor(() =>
  158. expect(FileUtils.readContentFromFileList).toHaveBeenCalled(),
  159. );
  160. });
  161. it("displays error notification for invalid file content", async () => {
  162. readContentFromFileListMock.mockResolvedValue([
  163. {
  164. name: "invalid.endpoint",
  165. content: "invalid content",
  166. },
  167. ]);
  168. render(<ChooseProvider {...defaultProps} />);
  169. jest.advanceTimersByTime(1000);
  170. const file = new File(["invalid content"], "invalid.endpoint", {
  171. type: "application/json",
  172. });
  173. fireEvent.drop(window, mockDataTransfer([file]));
  174. await waitFor(() =>
  175. expect(notificationStore.alert).toHaveBeenCalledWith(
  176. "Invalid .endpoint file",
  177. "error",
  178. ),
  179. );
  180. });
  181. it("displays error notification if endpoint has no name", async () => {
  182. readContentFromFileListMock.mockResolvedValue([
  183. {
  184. name: "invalid.endpoint",
  185. content: JSON.stringify({
  186. OPENSTACK_ENDPOINT,
  187. name: "",
  188. }),
  189. },
  190. ]);
  191. render(<ChooseProvider {...defaultProps} />);
  192. jest.advanceTimersByTime(1000);
  193. const file = new File(["invalid content"], "invalid.endpoint", {
  194. type: "application/json",
  195. });
  196. fireEvent.drop(window, mockDataTransfer([file]));
  197. await waitFor(() =>
  198. expect(notificationStore.alert).toHaveBeenCalledWith(
  199. "Invalid .endpoint file",
  200. "error",
  201. ),
  202. );
  203. });
  204. it("processes multiple files and handles unique names", async () => {
  205. const multipleFilesMeta = [
  206. {
  207. name: "file1.endpoint",
  208. content: JSON.stringify(OPENSTACK_ENDPOINT),
  209. },
  210. {
  211. name: "file2.endpoint",
  212. content: JSON.stringify(OPENSTACK_ENDPOINT),
  213. },
  214. ];
  215. const multipleFiles = multipleFilesMeta.map(
  216. ({ name, content }) =>
  217. new File([content], name, {
  218. type: "application/json",
  219. }),
  220. );
  221. readContentFromFileListMock.mockResolvedValue(multipleFilesMeta);
  222. let setStateObj: any = {};
  223. jest
  224. .spyOn(ChooseProvider.prototype, "setState")
  225. .mockImplementationOnce((obj: any) => {
  226. setStateObj = obj;
  227. });
  228. render(<ChooseProvider {...defaultProps} />);
  229. const fileInput = document.querySelector('input[type="file"]')!;
  230. fireEvent.change(fileInput, { target: { files: multipleFiles } });
  231. await waitFor(() => {
  232. expect(defaultProps.onResetValidation).toHaveBeenCalledTimes(1);
  233. expect(setStateObj.multipleUploadedEndpoints[0].name).toBe(
  234. OPENSTACK_ENDPOINT.name,
  235. );
  236. expect(setStateObj.multipleUploadedEndpoints[1].name).toBe(
  237. `${OPENSTACK_ENDPOINT.name} (1)`,
  238. );
  239. });
  240. });
  241. it("fires onresizeupdate when multipleUploadedEndpoints changes", async () => {
  242. const onResizeUpdateMock = jest.fn();
  243. const multipleFilesMeta = [
  244. {
  245. name: "file1.endpoint",
  246. content: JSON.stringify(OPENSTACK_ENDPOINT),
  247. },
  248. {
  249. name: "file2.endpoint",
  250. content: JSON.stringify(OPENSTACK_ENDPOINT),
  251. },
  252. ];
  253. const multipleFiles = multipleFilesMeta.map(
  254. ({ name, content }) =>
  255. new File([content], name, {
  256. type: "application/json",
  257. }),
  258. );
  259. readContentFromFileListMock.mockResolvedValue(multipleFilesMeta);
  260. render(
  261. <ChooseProvider {...defaultProps} onResizeUpdate={onResizeUpdateMock} />,
  262. );
  263. const fileInput = document.querySelector('input[type="file"]')!;
  264. fireEvent.change(fileInput, { target: { files: multipleFiles } });
  265. await waitFor(() => {
  266. expect(onResizeUpdateMock).toHaveBeenCalled();
  267. });
  268. });
  269. it("adds drop effect on drag over", async () => {
  270. render(<ChooseProvider {...defaultProps} />);
  271. jest.advanceTimersByTime(1000);
  272. const transfer = mockDataTransfer([]);
  273. fireEvent.dragOver(window, transfer);
  274. await waitFor(() => {
  275. expect(transfer.dataTransfer.dropEffect).toBe("copy");
  276. });
  277. });
  278. it("shows warning for unindetified regions", async () => {
  279. readContentFromFileListMock.mockResolvedValue([
  280. {
  281. name: "openstack.endpoint",
  282. content: JSON.stringify({
  283. ...OPENSTACK_ENDPOINT,
  284. mapped_regions: ["region_2"],
  285. }),
  286. },
  287. ]);
  288. render(<ChooseProvider {...defaultProps} />);
  289. jest.advanceTimersByTime(1000);
  290. const file = new File(["invalid content"], "openstack.endpoint", {
  291. type: "application/json",
  292. });
  293. fireEvent.drop(window, mockDataTransfer([file]));
  294. await waitFor(() =>
  295. expect(notificationStore.alert).toHaveBeenCalledWith(
  296. "1 Coriolis Region couldn't be mapped",
  297. "warning",
  298. ),
  299. );
  300. });
  301. it("processes remove endpoint", () => {
  302. const chooseProviderInstance = new ChooseProvider(defaultProps);
  303. jest
  304. .spyOn(chooseProviderInstance, "setState")
  305. .mockImplementation((callback: any) => {
  306. callback({ multipleUploadedEndpoints: [OPENSTACK_ENDPOINT] });
  307. });
  308. chooseProviderInstance.handleRemoveUploadedEndpoint(
  309. OPENSTACK_ENDPOINT,
  310. true,
  311. );
  312. expect(defaultProps.onRemoveEndpoint).toHaveBeenCalledWith(
  313. OPENSTACK_ENDPOINT,
  314. );
  315. });
  316. it("handles regions change", () => {
  317. const chooseProviderInstance = new ChooseProvider(defaultProps);
  318. let nextState: any = {};
  319. jest
  320. .spyOn(chooseProviderInstance, "setState")
  321. .mockImplementation((callback: any) => {
  322. nextState = callback({
  323. multipleUploadedEndpoints: [OPENSTACK_ENDPOINT],
  324. });
  325. });
  326. chooseProviderInstance.handleRegionsChange(OPENSTACK_ENDPOINT, [
  327. "region_2",
  328. ]);
  329. expect(nextState.multipleUploadedEndpoints[0].mapped_regions).toEqual([
  330. "region_2",
  331. ]);
  332. });
  333. });