Skip to content

Commit e5c8eab

Browse files
committed
feat: Refactor device management functions to use Result type for error handling
1 parent e00ae96 commit e5c8eab

19 files changed

Lines changed: 830 additions & 124 deletions

src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export class BackupConfiguration {
3333
const { error, data } = await DeviceModule.getOrCreateDevice();
3434
if (error) return [];
3535

36-
const enabledBackupEntries = await DeviceModule.getBackupsFromDevice(data, true);
36+
const { error: backupsError, data: enabledBackupEntries } = await DeviceModule.getBackupsFromDevice(data, true);
37+
if (backupsError || !enabledBackupEntries) return [];
3738

3839
return this.map(enabledBackupEntries, data.bucket);
3940
}

src/apps/main/interface.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface IElectronAPI {
3636

3737
getOrCreateDevice: () => Promise<Result<Device, Error>>;
3838

39-
getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise<Array<BackupInfo>>;
39+
getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise<Result<Array<BackupInfo>, Error>>;
4040

4141
addBackup: () => Promise<Result<BackupInfo, Error>>;
4242

@@ -62,7 +62,7 @@ export interface IElectronAPI {
6262

6363
abortDownloadBackups: (deviceId: string) => void;
6464

65-
renameDevice: (deviceName: string) => Promise<Device>;
65+
renameDevice: (deviceName: string) => Promise<Result<Device, Error>>;
6666
devices: {
6767
getDevices: () => Promise<Array<Device>>;
6868
};

src/apps/main/preload.d.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,26 @@ declare interface Window {
100100

101101
path: typeof import('path');
102102

103-
getOrCreateDevice: typeof import('../../backend/features/device/device.module').DeviceModule.getOrCreateDevice;
103+
getOrCreateDevice: () => Promise<
104+
import('../../context/shared/domain/Result').Result<import('../main/device/service').Device, Error>
105+
>;
104106

105-
renameDevice: typeof import('../../backend/features/device/device.module').DeviceModule.renameDevice;
107+
renameDevice: (
108+
deviceName: string,
109+
) => Promise<import('../../context/shared/domain/Result').Result<import('../main/device/service').Device, Error>>;
106110

107111
devices: {
108112
getDevices: () => Promise<Array<Device>>;
109113
};
110114

111115
onDeviceCreated(func: (value: Device) => void): () => void;
112116

113-
getBackupsFromDevice: typeof import('../../backend/features/device/device.module').DeviceModule.getBackupsFromDevice;
117+
getBackupsFromDevice: (
118+
device: import('../main/device/service').Device,
119+
isCurrent?: boolean,
120+
) => Promise<
121+
import('../../context/shared/domain/Result').Result<import('../backups/BackupInfo').BackupInfo[], Error>
122+
>;
114123

115124
addBackup: typeof import('../../backend/features/backup/add-backup').addBackup;
116125

src/apps/renderer/context/DeviceContext.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,15 @@ export function DeviceProvider({ children }: { children: ReactNode }) {
5858
const deviceRename = async (deviceName: string) => {
5959
setDeviceState({ status: 'LOADING' });
6060

61-
try {
62-
const updatedDevice = await window.electron.renameDevice(deviceName);
63-
setDeviceState({ status: 'SUCCESS', device: updatedDevice });
64-
setCurrent(updatedDevice);
65-
setSelected(updatedDevice);
66-
} catch (err) {
67-
window.electron.logger.error({
68-
msg: '[RENDERER] Failed to rename device',
69-
error: err,
70-
});
61+
const { error, data: updatedDevice } = await window.electron.renameDevice(deviceName);
62+
if (error || !updatedDevice) {
7163
setDeviceState({ status: 'ERROR' });
64+
return;
7265
}
66+
67+
setDeviceState({ status: 'SUCCESS', device: updatedDevice });
68+
setCurrent(updatedDevice);
69+
setSelected(updatedDevice);
7370
};
7471

7572
return (

src/apps/renderer/hooks/backups/useBackups.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@ export function useBackups(): BackupContextProps {
2323
const [backups, setBackups] = useState<Array<BackupInfo>>([]);
2424
const [hasExistingBackups, setHasExistingBackups] = useState(false);
2525

26-
async function fetchBackups(): Promise<void> {
27-
if (!selected) return;
28-
const backups = await window.electron.getBackupsFromDevice(selected, selected === current);
29-
setBackups(backups);
26+
async function fetchBackups(): Promise<boolean> {
27+
if (!selected) return true;
28+
29+
const { error, data } = await window.electron.getBackupsFromDevice(selected, selected === current);
30+
if (error || !data) {
31+
setBackups([]);
32+
return false;
33+
}
34+
35+
setBackups(data);
36+
return true;
3037
}
3138

3239
const validateIfBackupExists = async () => {
@@ -38,13 +45,14 @@ export function useBackups(): BackupContextProps {
3845
setBackupsState('LOADING');
3946
setBackups([]);
4047

41-
try {
42-
await fetchBackups();
43-
setBackupsState('SUCCESS');
44-
} catch {
48+
const isLoaded = await fetchBackups();
49+
if (!isLoaded) {
4550
setBackupsState('ERROR');
4651
setBackups([]);
52+
return;
4753
}
54+
55+
setBackupsState('SUCCESS');
4856
}
4957

5058
useEffect(() => {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { BrowserWindow } from 'electron';
2+
import { broadcastToWindows } from '../../../apps/main/windows';
3+
import { DependencyInjectionUserProvider } from '../../../apps/shared/dependency-injection/DependencyInjectionUserProvider';
4+
import { createNewDevice } from './createNewDevice';
5+
import { createAndSetupNewDevice } from './createAndSetupNewDevice';
6+
import { getDeviceIdentifier } from './getDeviceIdentifier';
7+
8+
vi.mock('electron', async (importOriginal) => {
9+
const actual = await importOriginal<typeof import('electron')>();
10+
11+
return {
12+
...actual,
13+
app: {
14+
...actual.app,
15+
getPath: vi.fn().mockReturnValue('/tmp/backups'),
16+
},
17+
ipcMain: {
18+
...actual.ipcMain,
19+
on: vi.fn(),
20+
handle: vi.fn(),
21+
removeHandler: vi.fn(),
22+
},
23+
BrowserWindow: {
24+
...actual.BrowserWindow,
25+
getAllWindows: vi.fn(),
26+
},
27+
};
28+
});
29+
vi.mock('./getDeviceIdentifier');
30+
vi.mock('./createNewDevice');
31+
vi.mock('../../../apps/main/windows', () => ({
32+
broadcastToWindows: vi.fn(),
33+
}));
34+
vi.mock('../../../apps/shared/dependency-injection/DependencyInjectionUserProvider', () => ({
35+
DependencyInjectionUserProvider: { get: vi.fn(), updateUser: vi.fn() },
36+
}));
37+
38+
describe('createAndSetupNewDevice', () => {
39+
const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier);
40+
const mockedCreateNewDevice = vi.mocked(createNewDevice);
41+
const mockedBroadcastToWindows = vi.mocked(broadcastToWindows);
42+
const mockedBrowserWindowGetAllWindows = vi.mocked(BrowserWindow.getAllWindows);
43+
const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get);
44+
const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser);
45+
46+
beforeEach(() => {
47+
vi.clearAllMocks();
48+
mockedUserProviderGet.mockReturnValue({ backupsBucket: '' } as never);
49+
mockedBrowserWindowGetAllWindows.mockReturnValue([] as never);
50+
});
51+
52+
it('should return only error when the device identifier is unavailable', async () => {
53+
const error = new Error('Missing device identifier');
54+
mockedGetDeviceIdentifier.mockReturnValue({ error });
55+
56+
const result = await createAndSetupNewDevice();
57+
58+
expect(result).toStrictEqual({ error });
59+
expect(mockedCreateNewDevice).not.toHaveBeenCalled();
60+
expect(mockedBroadcastToWindows).not.toHaveBeenCalled();
61+
});
62+
63+
it('should return only error when the device creation fails', async () => {
64+
const error = new Error('Create device failed');
65+
mockedGetDeviceIdentifier.mockReturnValue({
66+
data: { key: 'key', platform: 'linux', hostname: 'host' },
67+
});
68+
mockedCreateNewDevice.mockResolvedValue({ error });
69+
70+
const result = await createAndSetupNewDevice();
71+
72+
expect(result).toStrictEqual({ error });
73+
expect(mockedBroadcastToWindows).not.toHaveBeenCalled();
74+
expect(mockedUserProviderUpdate).not.toHaveBeenCalled();
75+
});
76+
77+
it('should update the user and notify windows when the device is created', async () => {
78+
const user = { backupsBucket: '' };
79+
const send = vi.fn();
80+
const device = {
81+
id: 1,
82+
uuid: 'device-uuid',
83+
name: 'Laptop',
84+
bucket: 'bucket-1',
85+
removed: false,
86+
hasBackups: true,
87+
};
88+
mockedUserProviderGet.mockReturnValue(user as never);
89+
mockedBrowserWindowGetAllWindows.mockReturnValue([{ webContents: { send } }] as never);
90+
mockedGetDeviceIdentifier.mockReturnValue({
91+
data: { key: 'key', platform: 'linux', hostname: 'host' },
92+
});
93+
mockedCreateNewDevice.mockResolvedValue({ data: device });
94+
95+
const result = await createAndSetupNewDevice();
96+
97+
expect(result).toStrictEqual({ data: device });
98+
expect(user.backupsBucket).toBe('bucket-1');
99+
expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user);
100+
expect(send).toHaveBeenCalledWith('reinitialize-backups');
101+
expect(mockedBroadcastToWindows).toHaveBeenCalledWith('device-created', device);
102+
});
103+
});

src/backend/features/device/createAndSetupNewDevice.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@ export async function createAndSetupNewDevice() {
99
const { error, data: deviceIdentifier } = getDeviceIdentifier();
1010
if (error) return { error };
1111

12-
const createNewDeviceEither = await createNewDevice(deviceIdentifier);
13-
if (createNewDeviceEither.isLeft()) {
12+
const { error: createDeviceError, data: device } = await createNewDevice(deviceIdentifier);
13+
if (createDeviceError) {
1414
logger.error({
1515
tag: 'BACKUPS',
1616
msg: '[DEVICE] Error creating new device',
17-
error: createNewDeviceEither.getLeft(),
17+
error: createDeviceError,
1818
});
19-
return { error: createNewDeviceEither.getLeft() };
19+
return { error: createDeviceError };
2020
}
2121

22-
const device = createNewDeviceEither.getRight();
2322
const user = DependencyInjectionUserProvider.get();
2423
user.backupsBucket = device.bucket;
2524
DependencyInjectionUserProvider.updateUser(user);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createNewDevice } from './createNewDevice';
2+
import { createUniqueDevice } from './createUniqueDevice';
3+
import { saveDeviceToConfig } from './saveDeviceToConfig';
4+
5+
vi.mock('./createUniqueDevice');
6+
vi.mock('./saveDeviceToConfig');
7+
8+
describe('createNewDevice', () => {
9+
const mockedCreateUniqueDevice = vi.mocked(createUniqueDevice);
10+
const mockedSaveDeviceToConfig = vi.mocked(saveDeviceToConfig);
11+
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
});
15+
16+
it('should return only error when creating a unique device fails', async () => {
17+
const error = new Error('Could not create device');
18+
mockedCreateUniqueDevice.mockResolvedValue({ error });
19+
20+
const result = await createNewDevice({ key: 'key', platform: 'linux', hostname: 'host' });
21+
22+
expect(result).toStrictEqual({ error });
23+
expect(mockedSaveDeviceToConfig).not.toHaveBeenCalled();
24+
});
25+
26+
it('should save the device to config when creating the device succeeds', async () => {
27+
const device = {
28+
id: 1,
29+
uuid: 'device-uuid',
30+
name: 'Laptop',
31+
bucket: 'bucket-1',
32+
removed: false,
33+
hasBackups: true,
34+
};
35+
mockedCreateUniqueDevice.mockResolvedValue({ data: device });
36+
37+
const result = await createNewDevice({ key: 'key', platform: 'linux', hostname: 'host' });
38+
39+
expect(result).toStrictEqual({ data: device });
40+
expect(mockedSaveDeviceToConfig).toHaveBeenCalledWith(device);
41+
});
42+
});
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { Either, right } from './../../../context/shared/domain/Either';
2-
import { Device } from '../backup/types/Device';
1+
import { Device } from '../../../apps/main/device/service';
2+
import { Result } from '../../../context/shared/domain/Result';
33
import { createUniqueDevice } from './createUniqueDevice';
44
import { saveDeviceToConfig } from './saveDeviceToConfig';
55
import { DeviceIdentifierDTO } from './device.types';
66

7-
export async function createNewDevice(deviceIdentifier: DeviceIdentifierDTO): Promise<Either<Error, Device>> {
8-
const createUniqueDeviceEither = await createUniqueDevice(deviceIdentifier);
9-
if (createUniqueDeviceEither.isRight()) {
10-
const device = createUniqueDeviceEither.getRight();
11-
saveDeviceToConfig(device);
12-
return right(device);
13-
}
14-
return createUniqueDeviceEither;
7+
export async function createNewDevice(deviceIdentifier: DeviceIdentifierDTO): Promise<Result<Device, Error>> {
8+
const { data: device, error } = await createUniqueDevice(deviceIdentifier);
9+
if (error) return { error };
10+
11+
saveDeviceToConfig(device);
12+
return { data: device };
1513
}

src/backend/features/device/createUniqueDevice.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@ import { Device } from '../backup/types/Device';
22
import { hostname } from 'node:os';
33
import { logger } from '@internxt/drive-desktop-core/build/backend';
44
import { tryCreateDevice } from './tryCreateDevice';
5-
import { Either, left, right } from '../../../context/shared/domain/Either';
65
import { addUnknownDeviceIssue } from './addUnknownDeviceIssue';
76
import { DeviceIdentifierDTO } from './device.types';
7+
import { Result } from '../../../context/shared/domain/Result';
8+
import { BackupError } from '../../../infra/drive-server/services/backup/backup.error';
89
/**
910
* Creates a new device with a unique name
10-
* @returns Either containing the created device or an error if device creation fails after multiple attempts
11+
* @returns Result containing the created device or an error if device creation fails after multiple attempts
1112
* @param attempts The number of attempts to create a device with a unique name, defaults to 1000
1213
*/
1314
export async function createUniqueDevice(
1415
deviceIdentifier: DeviceIdentifierDTO,
1516
attempts = 1000,
16-
): Promise<Either<Error, Device>> {
17+
): Promise<Result<Device, Error>> {
1718
const baseName = hostname();
1819
const nameVariants = [baseName, ...Array.from({ length: attempts }, (_, i) => `${baseName} (${i + 1})`)];
1920

@@ -22,21 +23,22 @@ export async function createUniqueDevice(
2223
tag: 'BACKUPS',
2324
msg: `Trying to create device with name "${name}"`,
2425
});
25-
const tryCreateDeviceEither = await tryCreateDevice(name, deviceIdentifier);
26+
const { data, error } = await tryCreateDevice(name, deviceIdentifier);
2627

27-
if (tryCreateDeviceEither.isRight()) {
28-
return right(tryCreateDeviceEither.getRight());
28+
if (data) {
29+
return { data };
2930
}
30-
const error = tryCreateDeviceEither.getLeft();
31-
if (error.message == 'Error creating device') {
32-
return left(tryCreateDeviceEither.getLeft());
31+
32+
if (!(error instanceof BackupError && error.code === 'ALREADY_EXISTS')) {
33+
return { error };
3334
}
3435
}
36+
3537
const finalError = logger.error({
3638
tag: 'BACKUPS',
3739
msg: 'Could not create device trying different names',
3840
});
3941

4042
addUnknownDeviceIssue(finalError);
41-
return left(finalError);
43+
return { error: finalError };
4244
}

0 commit comments

Comments
 (0)