diff --git a/client/src/components/asset/AssetBoard.tsx b/client/src/components/asset/AssetBoard.tsx index 741b45319..30d9cb44c 100644 --- a/client/src/components/asset/AssetBoard.tsx +++ b/client/src/components/asset/AssetBoard.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Grid } from '@mui/material'; import { useDispatch, useSelector } from 'react-redux'; import { GitlabInstance } from 'util/gitlab'; diff --git a/client/src/components/asset/AssetCard.tsx b/client/src/components/asset/AssetCard.tsx index 23a8fe359..d939b39ab 100644 --- a/client/src/components/asset/AssetCard.tsx +++ b/client/src/components/asset/AssetCard.tsx @@ -233,4 +233,4 @@ function AssetCardExecute({ asset }: AssetCardProps) { ); } -export { AssetCardManage, AssetCardExecute }; +export { AssetCardManage, AssetCardExecute, CardButtonsContainerManage, CardButtonsContainerExecute}; diff --git a/client/src/route/digitaltwins/DeleteDialog.tsx b/client/src/route/digitaltwins/DeleteDialog.tsx index a8500f320..757a94d5c 100644 --- a/client/src/route/digitaltwins/DeleteDialog.tsx +++ b/client/src/route/digitaltwins/DeleteDialog.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; +import { Dispatch, SetStateAction } from 'react'; import { Dialog, DialogContent, @@ -5,7 +7,6 @@ import { Button, Typography, } from '@mui/material'; -import React, { Dispatch, SetStateAction } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { selectDigitalTwinByName } from 'store/digitalTwin.slice'; import DigitalTwin, { formatName } from 'util/gitlabDigitalTwin'; diff --git a/client/src/route/digitaltwins/DigitalTwinsPreview.tsx b/client/src/route/digitaltwins/DigitalTwinsPreview.tsx index 31f93f1a9..9ccb37267 100644 --- a/client/src/route/digitaltwins/DigitalTwinsPreview.tsx +++ b/client/src/route/digitaltwins/DigitalTwinsPreview.tsx @@ -41,7 +41,7 @@ export const fetchSubfolders = async ( const subfolders = await gitlabInstance.getDTSubfolders( gitlabInstance.projectId, ); - dispatch(setAssets(subfolders)); // Dispatcha gli asset nel Redux store + dispatch(setAssets(subfolders)); } else { dispatch(setAssets([])); } diff --git a/client/test/unit/components/asset/AssetBoard.test.tsx b/client/test/unit/components/asset/AssetBoard.test.tsx index 78dc8b9bb..17209423d 100644 --- a/client/test/unit/components/asset/AssetBoard.test.tsx +++ b/client/test/unit/components/asset/AssetBoard.test.tsx @@ -1,11 +1,15 @@ import * as React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import AssetBoard from 'components/asset/AssetBoard'; -import { Asset } from 'components/asset/Asset'; import { GitlabInstance } from 'util/gitlab'; import '@testing-library/jest-dom'; -import store from 'store/store'; import { Provider } from 'react-redux'; +import { createStore, combineReducers } from 'redux'; +import assetsReducer from 'store/assets.slice'; +import { Asset } from 'components/asset/Asset'; +import { RootState } from 'store/store'; +import { deleteAsset } from 'store/assets.slice'; +import * as ReactRedux from 'react-redux'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -37,16 +41,31 @@ jest.mock('components/asset/AssetCard', () => ({ AssetCardExecute: jest.fn(({ asset }) => (
{`Execute ${asset.name}`}
)), - AssetCardManage: jest.fn(({ asset }) =>
{`Manage ${asset.name}`}
), + AssetCardManage: jest.fn(({ asset, onDelete }) => ( +
+ {`Manage ${asset.name}`} + +
+ )), })); +const mockStore = createStore( + combineReducers({ + assets: assetsReducer, + }), + { + assets: { + items: assetsMock, + }, + } as RootState, +); + describe('AssetBoard', () => { it('renders AssetCardExecute components when tab is "Execute"', () => { render( - + @@ -59,10 +78,9 @@ describe('AssetBoard', () => { it('renders AssetCardManage components when tab is not "Execute"', () => { render( - + @@ -76,10 +94,9 @@ describe('AssetBoard', () => { it('displays an error message when error prop is provided', () => { const errorMessage = 'Something went wrong!'; render( - + @@ -90,11 +107,21 @@ describe('AssetBoard', () => { }); it('renders correctly with no assets', () => { + const emptyStore = createStore( + combineReducers({ + assets: assetsReducer, + }), + { + assets: { + items: [], + }, + } as unknown as RootState, + ); + render( - + @@ -104,4 +131,24 @@ describe('AssetBoard', () => { const manageCards = screen.queryAllByText(/Manage/); expect(manageCards).toHaveLength(0); }); + + it('dispatches deleteAsset action when delete button is clicked', () => { + const mockDispatch = jest.fn(); + jest.spyOn(ReactRedux, 'useDispatch').mockReturnValue(mockDispatch); + + render( + + + , + ); + + const deleteButtons = screen.getAllByText('Delete'); + fireEvent.click(deleteButtons[0]); + + expect(mockDispatch).toHaveBeenCalledWith(deleteAsset('path1')); + }); }); diff --git a/client/test/unit/components/asset/AssetCard.test.tsx b/client/test/unit/components/asset/AssetCard.test.tsx index 998d7a677..ceab3a7f4 100644 --- a/client/test/unit/components/asset/AssetCard.test.tsx +++ b/client/test/unit/components/asset/AssetCard.test.tsx @@ -1,65 +1,181 @@ import * as React from 'react'; -import { render, screen } from '@testing-library/react'; -import { AssetCardManage, AssetCardExecute } from 'components/asset/AssetCard'; -import { Provider } from 'react-redux'; -import store from 'store/store'; -import { enableFetchMocks } from 'jest-fetch-mock'; +import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { Provider } from 'react-redux'; +import { createStore, combineReducers } from 'redux'; +import { AssetCardManage, AssetCardExecute, CardButtonsContainerManage, CardButtonsContainerExecute } from 'components/asset/AssetCard'; +import assetsReducer from 'store/assets.slice'; +import { Asset } from 'components/asset/Asset'; +import { RootState } from 'store/store'; +import { setDigitalTwin } from 'store/digitalTwin.slice'; +import DigitalTwin from 'util/gitlabDigitalTwin'; + +jest.mock('route/digitaltwins/Snackbar', () => () =>
Snackbar
); +jest.mock('route/digitaltwins/DetailsDialog', () => ({ showLog, setShowLog, name }: { showLog: boolean; setShowLog: (show: boolean) => void; name: string }) => ( +
+ DetailsDialog for {name} + {showLog && } +
+)); +jest.mock('route/digitaltwins/DeleteDialog', () => ({ showLog, setShowLog, name, onDelete }: { showLog: boolean; setShowLog: (show: boolean) => void; name: string; onDelete: () => void }) => ( +
+ DeleteDialog for {name} + {showLog && } +
+)); +jest.mock('route/digitaltwins/LogDialog', () => ({ showLog, setShowLog, name }: { showLog: boolean; setShowLog: (show: boolean) => void; name: string }) => ( +
+ LogDialog for {name} + {showLog && } +
+)); -enableFetchMocks(); +const assetsMock: Asset[] = [ + { name: 'Asset1', path: 'path1', description: 'Description1' }, + { name: 'Asset2', path: 'path2', description: 'Description2' }, +]; + +interface DigitalTwinState { + [key: string]: DigitalTwin; +} -if (!AbortSignal.timeout) { - AbortSignal.timeout = (ms) => { - const controller = new AbortController(); - setTimeout(() => controller.abort(new DOMException('TimeoutError')), ms); - return controller.signal; +interface SetDigitalTwinAction { + type: typeof setDigitalTwin.type; + payload: { + assetName: string; + digitalTwin: DigitalTwin; }; } -jest.deepUnmock('components/asset/AssetCard'); +type DigitalTwinActions = SetDigitalTwinAction; -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), -})); +const mockStore = createStore( + combineReducers({ + assets: assetsReducer, + digitalTwin: (state: DigitalTwinState = {}, action: DigitalTwinActions) => { + switch (action.type) { + case setDigitalTwin.type: + return { ...state, [action.payload.assetName]: action.payload.digitalTwin }; + default: + return state; + } + }, + }), + { + assets: { items: assetsMock }, + } as RootState, +); -jest.mock('react-oidc-context', () => ({ - ...jest.requireActual('react-oidc-context'), - useAuth: jest.fn(), -})); +describe('AssetCard Components', () => { + it('renders AssetCard with asset and buttons', () => { + render( + + {}} + /> + , + ); -jest.mock('util/envUtil', () => ({ - ...jest.requireActual('util/envUtil'), - getAuthority: jest.fn(() => 'https://example.com'), -})); + expect(screen.getByText(/Description1/)).toBeInTheDocument(); + }); -jest.mock(''); + it('renders CardButtonsContainerManage with buttons', () => { + const setShowDetailsLog = jest.fn(); + const setShowDeleteLog = jest.fn(); -describe('AssetCard', () => { - const assetMock = { - name: 'TestName', - path: 'testPath', - description: 'testDescription', - }; + render( + + ); + + expect(screen.getByText('DetailsButton')).toBeInTheDocument(); + expect(screen.getByText('ReconfigureButton')).toBeInTheDocument(); + expect(screen.getByText('DeleteButton')).toBeInTheDocument(); + }); + + it('handles button clicks in CardButtonsContainerManage', () => { + const setShowDetailsLog = jest.fn(); + const setShowDeleteLog = jest.fn(); - test('renders Asset Card Manage correctly', () => { render( - - - ); - , + ); - expect(screen.getByText(assetMock.name)).toBeInTheDocument(); + fireEvent.click(screen.getByText('DetailsButton')); + expect(setShowDetailsLog).toHaveBeenCalled(); + + fireEvent.click(screen.getByText('DeleteButton')); + expect(setShowDeleteLog).toHaveBeenCalled(); }); - test('renders Asset Card Execute correctly', () => { + it('renders CardButtonsContainerExecute with buttons', () => { + const setShowLog = jest.fn(); + render( - - - ); - , + + ); + + expect(screen.getByText('StartStopButton')).toBeInTheDocument(); + expect(screen.getByText('LogButton')).toBeInTheDocument(); + }); + + it('handles button clicks in CardButtonsContainerExecute', () => { + const setShowLog = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('StartStopButton')); + + fireEvent.click(screen.getByText('LogButton')); + expect(setShowLog).toHaveBeenCalled(); + }); + + it('renders AssetCardManage correctly and handles dialogs', () => { + const handleDelete = jest.fn(); + + render( + + + + ); + + expect(screen.getByText('Snackbar')).toBeInTheDocument(); + fireEvent.click(screen.getByText('DetailsDialog for Asset1')); + fireEvent.click(screen.getByText('Close')); + + fireEvent.click(screen.getByText('DeleteDialog for Asset1')); + fireEvent.click(screen.getByText('Delete')); + expect(handleDelete).toHaveBeenCalled(); + }); + + it('renders AssetCardExecute correctly and handles dialogs', () => { + render( + + + ); - expect(screen.getByText(assetMock.name)).toBeInTheDocument(); + expect(screen.getByText('Snackbar')).toBeInTheDocument(); + fireEvent.click(screen.getByText('LogDialog for Asset1')); + fireEvent.click(screen.getByText('Close')); }); }); diff --git a/client/test/unit/components/asset/DetailsButton.test.tsx b/client/test/unit/components/asset/DetailsButton.test.tsx index 7e75f1668..40b6e1748 100644 --- a/client/test/unit/components/asset/DetailsButton.test.tsx +++ b/client/test/unit/components/asset/DetailsButton.test.tsx @@ -20,7 +20,6 @@ const digitalTwinMock = { }; describe('DetailsButton', () => { - const setFullDescriptionMock = jest.fn(); const setShowLogMock = jest.fn(); beforeEach(() => { @@ -31,7 +30,7 @@ describe('DetailsButton', () => { jest.clearAllMocks(); }); - test('renders the Details button correctly', () => { + it('renders the Details button correctly', () => { render( @@ -42,7 +41,7 @@ describe('DetailsButton', () => { expect(button).toBeInTheDocument(); }); - test('calls handleToggleLog when button is clicked', async () => { + it('calls setShowLog with true after clicking the button and getting the description', async () => { render( @@ -50,14 +49,11 @@ describe('DetailsButton', () => { ); const button = screen.getByRole('button', { name: /Details/i }); + fireEvent.click(button); - await waitFor(() => { - expect(digitalTwinMock.getFullDescription).toHaveBeenCalled(); - expect(setFullDescriptionMock).toHaveBeenCalledWith( - 'Full Description Mock', - ); - expect(setShowLogMock).toHaveBeenCalledWith(true); - }); + await waitFor(() => expect(digitalTwinMock.getFullDescription).toHaveBeenCalled()); + + expect(setShowLogMock).toHaveBeenCalledWith(true); }); }); diff --git a/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx b/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx index 559abfacd..0019ea972 100644 --- a/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx +++ b/client/test/unit/routes/digitaltwins/DeleteDialog.test.tsx @@ -1,32 +1,30 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; import DetailsDialog from 'route/digitaltwins/DetailsDialog'; import DigitalTwin from 'util/gitlabDigitalTwin'; import { selectDigitalTwinByName } from 'store/digitalTwin.slice'; +import { GitlabInstance } from 'util/gitlab'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), +})); jest.mock('store/digitalTwin.slice', () => ({ selectDigitalTwinByName: jest.fn(), })); -const middlewares = [thunk]; -const mockStore = configureStore(middlewares); - describe('DetailsDialog', () => { let store: any; let setShowLogMock: jest.Mock; beforeEach(() => { - store = mockStore({ - digitalTwin: {}, - }); setShowLogMock = jest.fn(); }); it('should render the dialog and display the digital twin name', () => { - const digitalTwin = new DigitalTwin('testDT', {}); + const gitlabInstance = new GitlabInstance('user1', 'authority', 'token1'); + const digitalTwin = new DigitalTwin('testDT', gitlabInstance); (selectDigitalTwinByName as jest.Mock).mockReturnValue(digitalTwin); render( @@ -78,4 +76,4 @@ describe('DetailsDialog', () => { expect(deleteMock).toHaveBeenCalled(); expect(setShowLogMock).toHaveBeenCalledWith(false); }); -}); +}); \ No newline at end of file diff --git a/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx b/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx index 57f9ebcb0..8a133f74e 100644 --- a/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx +++ b/client/test/unit/routes/digitaltwins/DigitalTwinsPreview.test.tsx @@ -13,13 +13,8 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import '@testing-library/jest-dom'; import { GitlabInstance } from 'util/gitlab'; -import { renderHook, act } from '@testing-library/react'; -import { Asset } from 'components/asset/Asset'; - -jest.mock('react-oidc-context', () => ({ - ...jest.requireActual('react-oidc-context'), - useAuth: jest.fn(), -})); +import { setAssets } from 'store/assets.slice'; +import { useDispatch } from 'react-redux'; jest.mock('util/gitlab', () => ({ GitlabInstance: jest.fn().mockImplementation(() => ({ @@ -33,6 +28,11 @@ jest.mock('util/envUtil', () => ({ getAuthority: jest.fn(() => 'https://example.com'), })); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: jest.fn(), +})); + describe('Digital Twins Preview', () => { const tabLabels: string[] = []; tabs.forEach((tab) => tabLabels.push(tab.label)); @@ -49,26 +49,26 @@ describe('Digital Twins Preview', () => { itHasCorrectExecuteTabNameInDTIframe(tabLabels); - it('should call getDTSubfolders and update subfolders state', async () => { + it('should call getDTSubfolders and dispatch setAssets', async () => { const mockGitlabInstance = new GitlabInstance( 'username', 'https://example.com', 'access_token', ); - const { result } = renderHook(() => React.useState([])); - const setSubfolders: React.Dispatch> = - result.current[1]; + const mockDispatch = jest.fn(); + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); + + jest.spyOn(mockGitlabInstance, 'init').mockResolvedValue(undefined); + jest.spyOn(mockGitlabInstance, 'getDTSubfolders').mockResolvedValue([]); - await act(async () => { - await fetchSubfolders(mockGitlabInstance, setSubfolders, jest.fn()); - }); + await fetchSubfolders(mockGitlabInstance, mockDispatch, jest.fn()); expect(mockGitlabInstance.init).toHaveBeenCalled(); expect(mockGitlabInstance.getDTSubfolders).toHaveBeenCalledWith( - 'mockedProjectId', + 'mockedProjectId' ); - expect(result.current[0]).toEqual([]); + expect(mockDispatch).toHaveBeenCalledWith(setAssets([])); }); it('should handle empty projectId correctly', async () => { @@ -77,23 +77,20 @@ describe('Digital Twins Preview', () => { 'https://example.com', 'access_token', ); + jest.spyOn(mockGitlabInstance, 'init').mockResolvedValue(undefined); jest.spyOn(mockGitlabInstance, 'getDTSubfolders').mockResolvedValue([]); mockGitlabInstance.projectId = null; - const { result } = renderHook(() => React.useState([])); - - const setSubfolders: React.Dispatch> = - result.current[1]; + const mockDispatch = jest.fn(); + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); - await act(async () => { - await fetchSubfolders(mockGitlabInstance, setSubfolders, jest.fn()); - }); + await fetchSubfolders(mockGitlabInstance, mockDispatch, jest.fn()); expect(mockGitlabInstance.init).toHaveBeenCalled(); expect(mockGitlabInstance.getDTSubfolders).not.toHaveBeenCalled(); - expect(result.current[0]).toEqual([]); + expect(mockDispatch).toHaveBeenCalledWith(setAssets([])); }); it('should handle errors correctly', async () => { @@ -102,29 +99,19 @@ describe('Digital Twins Preview', () => { 'https://example.com', 'access_token', ); - jest - .spyOn(mockGitlabInstance, 'init') - .mockRejectedValue(new Error('Initialization failed')); - const { result: subfoldersResult } = renderHook(() => - React.useState([]), - ); - const { result: errorResult } = renderHook(() => - React.useState(null), - ); + jest.spyOn(mockGitlabInstance, 'init').mockRejectedValue(new Error('Initialization failed')); + + const mockDispatch = jest.fn(); + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); - const setSubfolders: React.Dispatch> = - subfoldersResult.current[1]; - const setError: React.Dispatch> = - errorResult.current[1]; + const setError = jest.fn(); - await act(async () => { - await fetchSubfolders(mockGitlabInstance, setSubfolders, setError); - }); + await fetchSubfolders(mockGitlabInstance, mockDispatch, setError); expect(mockGitlabInstance.init).toHaveBeenCalled(); expect(mockGitlabInstance.getDTSubfolders).not.toHaveBeenCalled(); - expect(subfoldersResult.current[0]).toEqual([]); - expect(errorResult.current[0]).toBe('An error occurred'); + expect(mockDispatch).not.toHaveBeenCalled(); + expect(setError).toHaveBeenCalledWith('An error occurred'); }); }); diff --git a/client/test/unit/routes/digitaltwins/Snackbar.test.tsx b/client/test/unit/routes/digitaltwins/Snackbar.test.tsx index 632fdba7f..e775d5ce7 100644 --- a/client/test/unit/routes/digitaltwins/Snackbar.test.tsx +++ b/client/test/unit/routes/digitaltwins/Snackbar.test.tsx @@ -1,74 +1,103 @@ import * as React from 'react'; import { render, screen, fireEvent, act } from '@testing-library/react'; import '@testing-library/jest-dom'; -import CustomSnackbar from 'route/digitaltwins/Snackbar'; // Adjust the import path accordingly +import CustomSnackbar from 'route/digitaltwins/Snackbar'; +import { Provider } from 'react-redux'; +import { hideSnackbar } from 'store/snackbar.slice'; + +const mockStore = (initialState: any) => { + return { + getState: () => initialState, + dispatch: jest.fn(), + subscribe: jest.fn(), + }; +}; describe('CustomSnackbar', () => { it('renders the snackbar with the correct message and severity', () => { - act(() => { - render( - , - ); + const store = mockStore({ + snackbar: { + open: true, + message: 'Test Message', + severity: 'success', + }, }); + render( + + + + ); + expect(screen.getByText('Test Message')).toBeInTheDocument(); expect(screen.getByRole('alert')).toHaveClass('MuiAlert-standardSuccess'); }); - it('does not render the snackbar when snackbarOpen is false', () => { - const { container } = render( - , + it('does not render the snackbar when open is false', () => { + const store = mockStore({ + snackbar: { + open: false, + message: 'Test Message', + severity: 'success', + }, + }); + + render( + + + ); - expect(container.querySelector('div')).toBeNull(); + expect(screen.queryByText('Test Message')).toBeNull(); }); - it('calls handleCloseSnackbar when the snackbar is closed through the alert button', () => { - const setSnackbarOpen = jest.fn(); - act(() => { - render( - , - ); + it('dispatches hideSnackbar action when the snackbar is closed through the alert button', () => { + const mockDispatch = jest.fn(); + const store = mockStore({ + snackbar: { + open: true, + message: 'Test Message', + severity: 'error', + }, }); + (store.dispatch as jest.Mock) = mockDispatch; + + render( + + + + ); + fireEvent.click(screen.getByRole('button')); - expect(setSnackbarOpen).toHaveBeenCalledWith(false); + expect(mockDispatch).toHaveBeenCalledWith(hideSnackbar()); }); - it('calls handleCloseSnackbar when the snackbar is closed via auto-hide duration', () => { - jest.useFakeTimers(); // Mock timers - const setSnackbarOpen = jest.fn(); - act(() => { - render( - , - ); + it('dispatches hideSnackbar action when the snackbar is closed via auto-hide duration', () => { + jest.useFakeTimers(); + + const mockDispatch = jest.fn(); + const store = mockStore({ + snackbar: { + open: true, + message: 'Test Message', + severity: 'warning', + }, }); + (store.dispatch as jest.Mock) = mockDispatch; + + render( + + + + ); + act(() => { jest.runAllTimers(); }); - expect(setSnackbarOpen).toHaveBeenCalledWith(false); + expect(mockDispatch).toHaveBeenCalledWith(hideSnackbar()); jest.useRealTimers(); }); }); diff --git a/client/test/unitTests/Util/gitlabDigitalTwin.test.ts b/client/test/unitTests/Util/gitlabDigitalTwin.test.ts index 19a028699..0c17c25ed 100644 --- a/client/test/unitTests/Util/gitlabDigitalTwin.test.ts +++ b/client/test/unitTests/Util/gitlabDigitalTwin.test.ts @@ -1,25 +1,14 @@ -import { ProjectSchema, PipelineTriggerTokenSchema } from '@gitbeaker/rest'; -import DigitalTwin from 'util/gitlabDigitalTwin'; import { GitlabInstance } from 'util/gitlab'; - -type LogEntry = { status: string; DTName: string; runnerTag: string }; +import DigitalTwin, { formatName } from 'util/gitlabDigitalTwin'; const mockApi = { - Groups: { + RepositoryFiles: { show: jest.fn(), - allProjects: jest.fn(), + remove: jest.fn(), }, PipelineTriggerTokens: { - all: jest.fn(), trigger: jest.fn(), }, - Repositories: { - allRepositoryTrees: jest.fn(), - }, - RepositoryFiles: { - show: jest.fn(), - remove: jest.fn(), - }, Pipelines: { cancel: jest.fn(), }, @@ -27,168 +16,153 @@ const mockApi = { const mockGitlabInstance = { api: mockApi as unknown as GitlabInstance['api'], - executionLogs: jest.fn() as jest.Mock, + projectId: 1, + triggerToken: 'test-token', + logs: [] as { jobName: string; log: string }[], getProjectId: jest.fn(), getTriggerToken: jest.fn(), - getDTSubfolders: jest.fn(), - logs: [], } as unknown as GitlabInstance; describe('DigitalTwin', () => { let dt: DigitalTwin; beforeEach(() => { + mockGitlabInstance.projectId = 1; dt = new DigitalTwin('test-DTName', mockGitlabInstance); }); - it('should handle null project ID during pipeline execution', async () => { - mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' }); - mockApi.Groups.allProjects.mockResolvedValue([]); - (mockGitlabInstance.getProjectId as jest.Mock).mockResolvedValue(null); + it('should return full description if projectId exists', async () => { + const mockContent = btoa('Test README content'); + (mockApi.RepositoryFiles.show as jest.Mock).mockResolvedValue({ + content: mockContent, + }); - const success = await dt.execute(); + await dt.getFullDescription(); - expect(success).toBe(false); - expect(dt.lastExecutionStatus).toBe('error'); - expect(mockApi.PipelineTriggerTokens.trigger).not.toHaveBeenCalled(); + expect(dt.fullDescription).toBe('Test README content'); + expect(mockApi.RepositoryFiles.show).toHaveBeenCalledWith( + 1, + 'digital_twins/test-DTName/README.md', + 'main', + ); }); - it('should handle null trigger token during pipeline execution', async () => { - mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' }); - mockApi.Groups.allProjects.mockResolvedValue([ - { id: 1, name: 'user1' } as ProjectSchema, - ]); - mockApi.PipelineTriggerTokens.all.mockResolvedValue([]); - (mockGitlabInstance.getTriggerToken as jest.Mock).mockResolvedValue(null); + it('should return error message if no README.md file exists', async () => { + (mockApi.RepositoryFiles.show as jest.Mock).mockRejectedValue( + new Error('File not found'), + ); - const success = await dt.execute(); + await dt.getFullDescription(); - expect(success).toBe(false); - expect(dt.lastExecutionStatus).toBe('error'); - expect(mockApi.PipelineTriggerTokens.trigger).not.toHaveBeenCalled(); + expect(dt.fullDescription).toBe( + 'There is no README.md file in the test-DTName GitLab folder', + ); + }); + + it('should return error message when projectId is missing', async () => { + dt.gitlabInstance.projectId = null; + await dt.getFullDescription(); + expect(dt.fullDescription).toBe('Error fetching description, retry.'); }); - it('should execute pipeline successfully', async () => { - mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' }); - mockApi.Groups.allProjects.mockResolvedValue([ - { id: 1, name: 'user1' } as ProjectSchema, - ]); - mockApi.PipelineTriggerTokens.all.mockResolvedValue([ - { token: 'test-token' } as PipelineTriggerTokenSchema, - ]); + it('should execute pipeline and return the pipeline ID', async () => { + const mockResponse = { id: 123 }; + (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockResolvedValue( + mockResponse, + ); (mockGitlabInstance.getProjectId as jest.Mock).mockResolvedValue(1); (mockGitlabInstance.getTriggerToken as jest.Mock).mockResolvedValue( 'test-token', ); - (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockResolvedValue( - undefined, - ); - const success = await dt.execute(); + const pipelineId = await dt.execute(); - expect(success).toBe(true); + expect(pipelineId).toBe(123); expect(dt.lastExecutionStatus).toBe('success'); expect(mockApi.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( 1, 'main', 'test-token', - { variables: { DTName: 'test-DTName', RunnerTag: 'test-runnerTag' } }, + { variables: { DTName: 'test-DTName', RunnerTag: 'linux' } }, ); }); - it('should handle non-Error thrown during pipeline execution', async () => { - mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' }); - mockApi.Groups.allProjects.mockResolvedValue([ - { id: 1, name: 'user1' } as ProjectSchema, - ]); - mockApi.PipelineTriggerTokens.all.mockResolvedValue([ - { token: 'test-token' } as PipelineTriggerTokenSchema, - ]); - (mockGitlabInstance.getProjectId as jest.Mock).mockResolvedValue(1); - (mockGitlabInstance.getTriggerToken as jest.Mock).mockResolvedValue( - 'test-token', - ); + it('should log error and return null when projectId or triggerToken is missing', async () => { + dt.gitlabInstance.projectId = null; + dt.gitlabInstance.triggerToken = null; + + jest.spyOn(dt, 'isValidInstance').mockReturnValue(false); + + (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockReset(); + + const pipelineId = await dt.execute(); + + expect(pipelineId).toBeNull(); + expect(dt.lastExecutionStatus).toBe('error'); + expect(mockApi.PipelineTriggerTokens.trigger).not.toHaveBeenCalled(); + }); + + it('should log success and update status', () => { + dt.logSuccess(); + + expect(dt.gitlabInstance.logs).toContainEqual({ + status: 'success', + DTName: 'test-DTName', + runnerTag: 'linux', + }); + expect(dt.lastExecutionStatus).toBe('success'); + }); + + it('should log error when triggering pipeline fails', async () => { + jest.spyOn(dt, 'isValidInstance').mockReturnValue(true); + const errorMessage = 'Trigger failed'; (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockRejectedValue( - 'String error message', + errorMessage, ); - const success = await dt.execute(); + const pipelineId = await dt.execute(); - expect(success).toBe(false); + expect(pipelineId).toBeNull(); expect(dt.lastExecutionStatus).toBe('error'); - expect(mockApi.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( - 1, - 'main', - 'test-token', - { variables: { DTName: 'test-DTName', RunnerTag: 'test-runnerTag' } }, - ); }); - it('should handle Error thrown during pipeline execution', async () => { - mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' }); - mockApi.Groups.allProjects.mockResolvedValue([ - { id: 1, name: 'user1' } as ProjectSchema, - ]); - mockApi.PipelineTriggerTokens.all.mockResolvedValue([ - { token: 'test-token' } as PipelineTriggerTokenSchema, - ]); - - mockApi.PipelineTriggerTokens.trigger.mockRejectedValue( - new Error('Error instance message'), + it('should handle non-Error thrown during pipeline execution', async () => { + (mockApi.PipelineTriggerTokens.trigger as jest.Mock).mockRejectedValue( + 'String error message', ); - const success = await dt.execute(); - - expect(success).toBe(false); + const pipelineId = await dt.execute(); + expect(pipelineId).toBeNull(); expect(dt.lastExecutionStatus).toBe('error'); }); - it('should return execution logs', async () => { - mockApi.Groups.show.mockResolvedValue({ id: 1, name: 'DTaaS' }); - mockApi.Groups.allProjects.mockResolvedValue([ - { id: 1, name: 'user1' } as ProjectSchema, - ]); - mockApi.PipelineTriggerTokens.all.mockResolvedValue([ - { token: 'test-token' } as PipelineTriggerTokenSchema, - ]); - mockApi.PipelineTriggerTokens.trigger.mockResolvedValue(undefined); - - await dt.execute(); - - (mockGitlabInstance.executionLogs as jest.Mock).mockReturnValue([ - { status: 'success', DTName: 'test-DTName', runnerTag: 'test-runnerTag' }, - ]); - - const logs = dt.gitlabInstance.executionLogs(); - expect(logs).toHaveLength(1); - expect(logs[0].status).toBe('success'); - expect(logs[0].DTName).toBe('test-DTName'); - expect(logs[0].runnerTag).toBe('test-runnerTag'); - }); + it('should stop the pipeline and update status', async () => { + const pipelineId = 456; + (mockApi.Pipelines.cancel as jest.Mock).mockResolvedValue({}); - it('should return an error message if README.md does not exist', async () => { - mockGitlabInstance.projectId = 1; - const errorMessage = `There is no README.md file in the test-DTName GitLab folder`; - mockApi.RepositoryFiles.show.mockRejectedValue(new Error('File not found')); + await dt.stop(1, pipelineId); - const description = await dt.getFullDescription(); + expect(mockApi.Pipelines.cancel).toHaveBeenCalledWith(1, pipelineId); + expect(dt.lastExecutionStatus).toBe('canceled'); + }); - expect(description).toBe(errorMessage); - expect(mockApi.RepositoryFiles.show).toHaveBeenCalledWith( - 1, - 'digital_twins/test-DTName/README.md', - 'main', + it('should handle stop error', async () => { + (mockApi.Pipelines.cancel as jest.Mock).mockRejectedValue( + new Error('Stop failed'), ); + + await dt.stop(1, 456); + + expect(dt.lastExecutionStatus).toBe('error'); }); - it('should delete the Digital Twin successfully', async () => { - mockGitlabInstance.projectId = 1; - mockApi.RepositoryFiles.remove.mockResolvedValue(undefined); + it('should delete the digital twin and return success message', async () => { + (mockApi.RepositoryFiles.remove as jest.Mock).mockResolvedValue({}); - const message = await dt.delete(); + const result = await dt.delete(); - expect(message).toBe('test-DTName deleted successfully'); + expect(result).toBe('test-DTName deleted successfully'); expect(mockApi.RepositoryFiles.remove).toHaveBeenCalledWith( 1, 'digital_twins/test-DTName', @@ -197,60 +171,31 @@ describe('DigitalTwin', () => { ); }); - it('should handle error during Digital Twin deletion', async () => { - mockGitlabInstance.projectId = 1; - mockApi.RepositoryFiles.remove.mockRejectedValue( - new Error('Deletion error'), + it('should return error message when deletion fails', async () => { + (mockApi.RepositoryFiles.remove as jest.Mock).mockRejectedValue( + new Error('Delete failed'), ); - const message = await dt.delete(); + const result = await dt.delete(); - expect(message).toBe('Error deleting test-DTName digital twin'); - expect(mockApi.RepositoryFiles.remove).toHaveBeenCalledWith( - 1, - 'digital_twins/test-DTName', - 'main', - 'Removing test-DTName digital twin', - ); + expect(result).toBe('Error deleting test-DTName digital twin'); }); - it('should return an error if no project id is provided during deletion', async () => { - mockGitlabInstance.projectId = null; + it('should return error message when projectId is missing during deletion', async () => { + dt.gitlabInstance.projectId = null; - const message = await dt.delete(); + const result = await dt.delete(); - expect(message).toBe( + expect(result).toBe( 'Error deleting test-DTName digital twin: no project id', ); - expect(mockApi.RepositoryFiles.remove).not.toHaveBeenCalled(); - }); - - it('should stop a pipeline successfully', async () => { - mockApi.Pipelines.cancel.mockResolvedValue(undefined); - - await dt.stop(1, 123); - - expect(mockApi.Pipelines.cancel).toHaveBeenCalledWith(1, 123); - expect(mockGitlabInstance.logs).toContainEqual({ - status: 'canceled', - DTName: 'test-DTName', - runnerTag: 'linux', - }); - expect(dt.lastExecutionStatus).toBe('canceled'); }); - it('should handle error during pipeline stop', async () => { - mockApi.Pipelines.cancel.mockRejectedValue(new Error('Stop error')); - - await dt.stop(1, 123); + it('should format the name correctly', () => { + const testCases = [{ input: 'digital-twin', expected: 'Digital twin' }]; - expect(mockApi.Pipelines.cancel).toHaveBeenCalledWith(1, 123); - expect(mockGitlabInstance.logs).toContainEqual({ - status: 'error', - error: new Error('Stop error'), - DTName: 'test-DTName', - runnerTag: 'linux', + testCases.forEach(({ input, expected }) => { + expect(formatName(input)).toBe(expected); }); - expect(dt.lastExecutionStatus).toBe('error'); }); -}); +}); \ No newline at end of file