diff --git a/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts b/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts index 97057cefe7..b6999a656e 100644 --- a/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts +++ b/src/apps/main/background-processes/backups/BackupConfiguration/BackupConfiguration.ts @@ -33,7 +33,8 @@ export class BackupConfiguration { const { error, data } = await DeviceModule.getOrCreateDevice(); if (error) return []; - const enabledBackupEntries = await DeviceModule.getBackupsFromDevice(data, true); + const { error: backupsError, data: enabledBackupEntries } = await DeviceModule.getBackupsFromDevice(data, true); + if (backupsError || !enabledBackupEntries) return []; return this.map(enabledBackupEntries, data.bucket); } diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 6b62365cd0..357000b244 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -36,7 +36,7 @@ export interface IElectronAPI { getOrCreateDevice: () => Promise>; - getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise>; + getBackupsFromDevice: (device: Device, isCurrent?: boolean) => Promise, Error>>; addBackup: () => Promise>; @@ -62,7 +62,7 @@ export interface IElectronAPI { abortDownloadBackups: (deviceId: string) => void; - renameDevice: (deviceName: string) => Promise; + renameDevice: (deviceName: string) => Promise>; devices: { getDevices: () => Promise>; }; diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index aca484dcf5..64c796510d 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -100,9 +100,13 @@ declare interface Window { path: typeof import('path'); - getOrCreateDevice: typeof import('../../backend/features/device/device.module').DeviceModule.getOrCreateDevice; + getOrCreateDevice: () => Promise< + import('../../context/shared/domain/Result').Result + >; - renameDevice: typeof import('../../backend/features/device/device.module').DeviceModule.renameDevice; + renameDevice: ( + deviceName: string, + ) => Promise>; devices: { getDevices: () => Promise>; @@ -110,7 +114,12 @@ declare interface Window { onDeviceCreated(func: (value: Device) => void): () => void; - getBackupsFromDevice: typeof import('../../backend/features/device/device.module').DeviceModule.getBackupsFromDevice; + getBackupsFromDevice: ( + device: import('../main/device/service').Device, + isCurrent?: boolean, + ) => Promise< + import('../../context/shared/domain/Result').Result + >; addBackup: typeof import('../../backend/features/backup/add-backup').addBackup; diff --git a/src/apps/renderer/context/DeviceContext.tsx b/src/apps/renderer/context/DeviceContext.tsx index dc7a1e3aa9..7318504e76 100644 --- a/src/apps/renderer/context/DeviceContext.tsx +++ b/src/apps/renderer/context/DeviceContext.tsx @@ -58,18 +58,15 @@ export function DeviceProvider({ children }: { children: ReactNode }) { const deviceRename = async (deviceName: string) => { setDeviceState({ status: 'LOADING' }); - try { - const updatedDevice = await window.electron.renameDevice(deviceName); - setDeviceState({ status: 'SUCCESS', device: updatedDevice }); - setCurrent(updatedDevice); - setSelected(updatedDevice); - } catch (err) { - window.electron.logger.error({ - msg: '[RENDERER] Failed to rename device', - error: err, - }); + const { error, data: updatedDevice } = await window.electron.renameDevice(deviceName); + if (error || !updatedDevice) { setDeviceState({ status: 'ERROR' }); + return; } + + setDeviceState({ status: 'SUCCESS', device: updatedDevice }); + setCurrent(updatedDevice); + setSelected(updatedDevice); }; return ( diff --git a/src/apps/renderer/hooks/backups/useBackups.tsx b/src/apps/renderer/hooks/backups/useBackups.tsx index 9a169ec57c..d4cf97337c 100644 --- a/src/apps/renderer/hooks/backups/useBackups.tsx +++ b/src/apps/renderer/hooks/backups/useBackups.tsx @@ -23,10 +23,17 @@ export function useBackups(): BackupContextProps { const [backups, setBackups] = useState>([]); const [hasExistingBackups, setHasExistingBackups] = useState(false); - async function fetchBackups(): Promise { - if (!selected) return; - const backups = await window.electron.getBackupsFromDevice(selected, selected === current); - setBackups(backups); + async function fetchBackups(): Promise { + if (!selected) return true; + + const { error, data } = await window.electron.getBackupsFromDevice(selected, selected === current); + if (error || !data) { + setBackups([]); + return false; + } + + setBackups(data); + return true; } const validateIfBackupExists = async () => { @@ -38,13 +45,14 @@ export function useBackups(): BackupContextProps { setBackupsState('LOADING'); setBackups([]); - try { - await fetchBackups(); - setBackupsState('SUCCESS'); - } catch { + const isLoaded = await fetchBackups(); + if (!isLoaded) { setBackupsState('ERROR'); setBackups([]); + return; } + + setBackupsState('SUCCESS'); } useEffect(() => { diff --git a/src/backend/features/backup/change-backup-path.test.ts b/src/backend/features/backup/change-backup-path.test.ts index d0cccc8e97..625a4bc509 100644 --- a/src/backend/features/backup/change-backup-path.test.ts +++ b/src/backend/features/backup/change-backup-path.test.ts @@ -67,7 +67,7 @@ describe('change-backup-path', () => { mockedConfigStoreGet.mockReturnValue(backupList); mockedGetBackupFolderUuid.mockResolvedValue({ data: 'remote-folder-uuid' }); mockedRenameFolder.mockResolvedValue({ data: {} }); - mockedMigrateBackupEntryIfNeeded.mockResolvedValue(migratedBackup); + mockedMigrateBackupEntryIfNeeded.mockResolvedValue({ data: migratedBackup }); const result = await changeBackupPath({ currentPath, newPath }); diff --git a/src/backend/features/backup/change-backup-path.ts b/src/backend/features/backup/change-backup-path.ts index 9a2879c58d..f2db4e93a6 100644 --- a/src/backend/features/backup/change-backup-path.ts +++ b/src/backend/features/backup/change-backup-path.ts @@ -42,11 +42,13 @@ export async function changeBackupPath({ currentPath, newPath }: Props): Promise delete backupsList[currentPath]; - const migratedExistingBackup = await migrateBackupEntryIfNeeded({ + const { error, data } = await migrateBackupEntryIfNeeded({ pathname: newPath, backup: existingBackup, }); - backupsList[newPath] = migratedExistingBackup; + if (error) return { error }; + + backupsList[newPath] = data; configStore.set('backupList', backupsList); diff --git a/src/backend/features/backup/delete-device-backups.test.ts b/src/backend/features/backup/delete-device-backups.test.ts index f036bceebb..3b828700f4 100644 --- a/src/backend/features/backup/delete-device-backups.test.ts +++ b/src/backend/features/backup/delete-device-backups.test.ts @@ -36,8 +36,8 @@ describe('delete-device-backups', () => { }, ]; - getBackupsFromDeviceMock.mockResolvedValue(backups); - deleteBackupMock.mockResolvedValue(undefined); + getBackupsFromDeviceMock.mockResolvedValue({ data: backups }); + deleteBackupMock.mockResolvedValue({ data: undefined }); getBackupFolderTreeSnapshotMock.mockResolvedValue({ data: { tree: { @@ -68,8 +68,8 @@ describe('delete-device-backups', () => { }, ]; - getBackupsFromDeviceMock.mockResolvedValue(backups); - deleteBackupMock.mockResolvedValue(undefined); + getBackupsFromDeviceMock.mockResolvedValue({ data: backups }); + deleteBackupMock.mockResolvedValue({ data: undefined }); getBackupFolderTreeSnapshotMock.mockResolvedValue({ data: { tree: { children: [{ id: 10, uuid: 'folder-uuid-1' }] } }, } as never); diff --git a/src/backend/features/backup/delete-device-backups.ts b/src/backend/features/backup/delete-device-backups.ts index a821caf833..3d2031a1f4 100644 --- a/src/backend/features/backup/delete-device-backups.ts +++ b/src/backend/features/backup/delete-device-backups.ts @@ -11,7 +11,12 @@ type Props = { }; export async function deleteDeviceBackups({ device, isCurrent }: Props) { - const backups = await DeviceModule.getBackupsFromDevice(device, isCurrent); + const { error: getBackupsError, data: backups } = await DeviceModule.getBackupsFromDevice(device, isCurrent); + if (getBackupsError) { + logger.error({ tag: 'BACKUPS', msg: 'Error fetching backups from device', error: getBackupsError }); + return; + } + logger.debug({ tag: 'BACKUPS', msg: '[BACKUPS] Deleting backups from device', count: backups.length }); logger.debug({ tag: 'BACKUPS', msg: '[BACKUPS] Backups details', backups }); diff --git a/src/backend/features/backup/download-backup.test.ts b/src/backend/features/backup/download-backup.test.ts index 0c4f5d6efa..f4c9142239 100644 --- a/src/backend/features/backup/download-backup.test.ts +++ b/src/backend/features/backup/download-backup.test.ts @@ -53,6 +53,7 @@ describe('download-backup', () => { it('should download backup and broadcast progress when not aborted', async () => { downloadDeviceBackupZipMock.mockImplementation(async ({ updateProgress }) => { updateProgress(33); + return { data: true }; }); await downloadBackup({ device, pathname }); @@ -86,6 +87,7 @@ describe('download-backup', () => { const abortListener = ipcMainOnMock.mock.calls[0]?.[1]; abortListener?.({} as never, device.uuid); updateProgress(90); + return { data: true }; }); await downloadBackup({ device, pathname }); @@ -98,6 +100,7 @@ describe('download-backup', () => { const abortListener = ipcMainOnMock.mock.calls[0]?.[1]; abortListener?.({} as never, 'other-device-uuid'); updateProgress(12); + return { data: true }; }); await downloadBackup({ device, pathname }); diff --git a/src/backend/features/backup/enable-existing-backup.test.ts b/src/backend/features/backup/enable-existing-backup.test.ts index ef1942df3b..07f7426b92 100644 --- a/src/backend/features/backup/enable-existing-backup.test.ts +++ b/src/backend/features/backup/enable-existing-backup.test.ts @@ -6,7 +6,7 @@ import { migrateBackupEntryIfNeeded } from './migrate-backup-entry-if-needed'; import { PATHS } from '../../../core/electron/paths'; import { createAbsolutePath } from '../../../context/local/localFile/infrastructure/AbsolutePath'; import { DriveServerError } from 'src/infra/drive-server/drive-server.error'; -import { GetFolderContentDto } from 'src/infra/drive-server/out/dto'; +import { GetFolderContentDto } from '../../../infra/drive-server/out/dto'; vi.mock('../../../apps/main/config'); vi.mock('../../../infra/drive-server/services/folder/services/fetch-folder'); diff --git a/src/backend/features/device/createAndSetupNewDevice.test.ts b/src/backend/features/device/createAndSetupNewDevice.test.ts new file mode 100644 index 0000000000..8c7d9509a1 --- /dev/null +++ b/src/backend/features/device/createAndSetupNewDevice.test.ts @@ -0,0 +1,103 @@ +import { BrowserWindow } from 'electron'; +import { broadcastToWindows } from '../../../apps/main/windows'; +import { DependencyInjectionUserProvider } from '../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { createNewDevice } from './createNewDevice'; +import { createAndSetupNewDevice } from './createAndSetupNewDevice'; +import { getDeviceIdentifier } from './getDeviceIdentifier'; + +vi.mock('electron', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + app: { + ...actual.app, + getPath: vi.fn().mockReturnValue('/tmp/backups'), + }, + ipcMain: { + ...actual.ipcMain, + on: vi.fn(), + handle: vi.fn(), + removeHandler: vi.fn(), + }, + BrowserWindow: { + ...actual.BrowserWindow, + getAllWindows: vi.fn(), + }, + }; +}); +vi.mock('./getDeviceIdentifier'); +vi.mock('./createNewDevice'); +vi.mock('../../../apps/main/windows', () => ({ + broadcastToWindows: vi.fn(), +})); +vi.mock('../../../apps/shared/dependency-injection/DependencyInjectionUserProvider', () => ({ + DependencyInjectionUserProvider: { get: vi.fn(), updateUser: vi.fn() }, +})); + +describe('createAndSetupNewDevice', () => { + const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier); + const mockedCreateNewDevice = vi.mocked(createNewDevice); + const mockedBroadcastToWindows = vi.mocked(broadcastToWindows); + const mockedBrowserWindowGetAllWindows = vi.mocked(BrowserWindow.getAllWindows); + const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get); + const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser); + + beforeEach(() => { + vi.clearAllMocks(); + mockedUserProviderGet.mockReturnValue({ backupsBucket: '' } as never); + mockedBrowserWindowGetAllWindows.mockReturnValue([] as never); + }); + + it('should return only error when the device identifier is unavailable', async () => { + const error = new Error('Missing device identifier'); + mockedGetDeviceIdentifier.mockReturnValue({ error }); + + const result = await createAndSetupNewDevice(); + + expect(result).toStrictEqual({ error }); + expect(mockedCreateNewDevice).not.toHaveBeenCalled(); + expect(mockedBroadcastToWindows).not.toHaveBeenCalled(); + }); + + it('should return only error when the device creation fails', async () => { + const error = new Error('Create device failed'); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedCreateNewDevice.mockResolvedValue({ error }); + + const result = await createAndSetupNewDevice(); + + expect(result).toStrictEqual({ error }); + expect(mockedBroadcastToWindows).not.toHaveBeenCalled(); + expect(mockedUserProviderUpdate).not.toHaveBeenCalled(); + }); + + it('should update the user and notify windows when the device is created', async () => { + const user = { backupsBucket: '' }; + const send = vi.fn(); + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedUserProviderGet.mockReturnValue(user as never); + mockedBrowserWindowGetAllWindows.mockReturnValue([{ webContents: { send } }] as never); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedCreateNewDevice.mockResolvedValue({ data: device }); + + const result = await createAndSetupNewDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(user.backupsBucket).toBe('bucket-1'); + expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user); + expect(send).toHaveBeenCalledWith('reinitialize-backups'); + expect(mockedBroadcastToWindows).toHaveBeenCalledWith('device-created', device); + }); +}); diff --git a/src/backend/features/device/createAndSetupNewDevice.ts b/src/backend/features/device/createAndSetupNewDevice.ts index f8b451a27b..0210b11090 100644 --- a/src/backend/features/device/createAndSetupNewDevice.ts +++ b/src/backend/features/device/createAndSetupNewDevice.ts @@ -9,17 +9,16 @@ export async function createAndSetupNewDevice() { const { error, data: deviceIdentifier } = getDeviceIdentifier(); if (error) return { error }; - const createNewDeviceEither = await createNewDevice(deviceIdentifier); - if (createNewDeviceEither.isLeft()) { + const { error: createDeviceError, data: device } = await createNewDevice(deviceIdentifier); + if (createDeviceError) { logger.error({ tag: 'BACKUPS', msg: '[DEVICE] Error creating new device', - error: createNewDeviceEither.getLeft(), + error: createDeviceError, }); - return { error: createNewDeviceEither.getLeft() }; + return { error: createDeviceError }; } - const device = createNewDeviceEither.getRight(); const user = DependencyInjectionUserProvider.get(); user.backupsBucket = device.bucket; DependencyInjectionUserProvider.updateUser(user); diff --git a/src/backend/features/device/createNewDevice.test.ts b/src/backend/features/device/createNewDevice.test.ts new file mode 100644 index 0000000000..0ed690cf9a --- /dev/null +++ b/src/backend/features/device/createNewDevice.test.ts @@ -0,0 +1,42 @@ +import { createNewDevice } from './createNewDevice'; +import { createUniqueDevice } from './createUniqueDevice'; +import { saveDeviceToConfig } from './saveDeviceToConfig'; + +vi.mock('./createUniqueDevice'); +vi.mock('./saveDeviceToConfig'); + +describe('createNewDevice', () => { + const mockedCreateUniqueDevice = vi.mocked(createUniqueDevice); + const mockedSaveDeviceToConfig = vi.mocked(saveDeviceToConfig); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return only error when creating a unique device fails', async () => { + const error = new Error('Could not create device'); + mockedCreateUniqueDevice.mockResolvedValue({ error }); + + const result = await createNewDevice({ key: 'key', platform: 'linux', hostname: 'host' }); + + expect(result).toStrictEqual({ error }); + expect(mockedSaveDeviceToConfig).not.toHaveBeenCalled(); + }); + + it('should save the device to config when creating the device succeeds', async () => { + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedCreateUniqueDevice.mockResolvedValue({ data: device }); + + const result = await createNewDevice({ key: 'key', platform: 'linux', hostname: 'host' }); + + expect(result).toStrictEqual({ data: device }); + expect(mockedSaveDeviceToConfig).toHaveBeenCalledWith(device); + }); +}); diff --git a/src/backend/features/device/createNewDevice.ts b/src/backend/features/device/createNewDevice.ts index d094f5e987..c4c7e8b5b5 100644 --- a/src/backend/features/device/createNewDevice.ts +++ b/src/backend/features/device/createNewDevice.ts @@ -1,15 +1,13 @@ -import { Either, right } from './../../../context/shared/domain/Either'; import { Device } from '../backup/types/Device'; +import { Result } from '../../../context/shared/domain/Result'; import { createUniqueDevice } from './createUniqueDevice'; import { saveDeviceToConfig } from './saveDeviceToConfig'; import { DeviceIdentifierDTO } from './device.types'; -export async function createNewDevice(deviceIdentifier: DeviceIdentifierDTO): Promise> { - const createUniqueDeviceEither = await createUniqueDevice(deviceIdentifier); - if (createUniqueDeviceEither.isRight()) { - const device = createUniqueDeviceEither.getRight(); - saveDeviceToConfig(device); - return right(device); - } - return createUniqueDeviceEither; +export async function createNewDevice(deviceIdentifier: DeviceIdentifierDTO): Promise> { + const { data: device, error } = await createUniqueDevice(deviceIdentifier); + if (error) return { error }; + + saveDeviceToConfig(device); + return { data: device }; } diff --git a/src/backend/features/device/createUniqueDevice.ts b/src/backend/features/device/createUniqueDevice.ts index 5cc6304044..12c06e4447 100644 --- a/src/backend/features/device/createUniqueDevice.ts +++ b/src/backend/features/device/createUniqueDevice.ts @@ -2,18 +2,19 @@ import { Device } from '../backup/types/Device'; import { hostname } from 'node:os'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { tryCreateDevice } from './tryCreateDevice'; -import { Either, left, right } from '../../../context/shared/domain/Either'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; import { DeviceIdentifierDTO } from './device.types'; +import { Result } from '../../../context/shared/domain/Result'; +import { BackupError } from '../../../infra/drive-server/services/backup/backup.error'; /** * Creates a new device with a unique name - * @returns Either containing the created device or an error if device creation fails after multiple attempts + * @returns Result containing the created device or an error if device creation fails after multiple attempts * @param attempts The number of attempts to create a device with a unique name, defaults to 1000 */ export async function createUniqueDevice( deviceIdentifier: DeviceIdentifierDTO, attempts = 1000, -): Promise> { +): Promise> { const baseName = hostname(); const nameVariants = [baseName, ...Array.from({ length: attempts }, (_, i) => `${baseName} (${i + 1})`)]; @@ -22,21 +23,22 @@ export async function createUniqueDevice( tag: 'BACKUPS', msg: `Trying to create device with name "${name}"`, }); - const tryCreateDeviceEither = await tryCreateDevice(name, deviceIdentifier); + const { data, error } = await tryCreateDevice(name, deviceIdentifier); - if (tryCreateDeviceEither.isRight()) { - return right(tryCreateDeviceEither.getRight()); + if (data) { + return { data }; } - const error = tryCreateDeviceEither.getLeft(); - if (error.message == 'Error creating device') { - return left(tryCreateDeviceEither.getLeft()); + + if (!(error instanceof BackupError && error.code === 'ALREADY_EXISTS')) { + return { error }; } } + const finalError = logger.error({ tag: 'BACKUPS', msg: 'Could not create device trying different names', }); addUnknownDeviceIssue(finalError); - return left(finalError); + return { error: finalError }; } diff --git a/src/backend/features/device/getBackupsFromDevice.test.ts b/src/backend/features/device/getBackupsFromDevice.test.ts new file mode 100644 index 0000000000..e839062856 --- /dev/null +++ b/src/backend/features/device/getBackupsFromDevice.test.ts @@ -0,0 +1,133 @@ +import { app } from 'electron'; +import configStore from '../../../apps/main/config'; +import { findBackupPathnameFromId } from '../backup/find-backup-pathname-from-id'; +import { fetchFolder } from '../../../infra/drive-server/services/folder/services/fetch-folder'; +import { getBackupsFromDevice } from './getBackupsFromDevice'; + +vi.mock('electron', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + app: { + ...actual.app, + getPath: vi.fn().mockReturnValue('/tmp/backups'), + }, + ipcMain: { + ...actual.ipcMain, + on: vi.fn(), + handle: vi.fn(), + removeHandler: vi.fn(), + }, + }; +}); +vi.mock('../../../infra/drive-server/services/folder/services/fetch-folder'); +vi.mock('../../../apps/main/config', () => ({ + default: { get: vi.fn() }, +})); +vi.mock('../backup/find-backup-pathname-from-id', () => ({ + findBackupPathnameFromId: vi.fn(), +})); + +describe('getBackupsFromDevice', () => { + const mockedFetchFolder = vi.mocked(fetchFolder); + const mockedConfigStore = vi.mocked(configStore); + const mockedFindBackupPathnameFromId = vi.mocked(findBackupPathnameFromId); + const mockedAppGetPath = vi.mocked(app.getPath); + + beforeEach(() => { + vi.clearAllMocks(); + mockedAppGetPath.mockReturnValue('/tmp/backups'); + }); + + it('should return only error when fetching the folder fails', async () => { + const error = new Error('Folder fetch failed'); + mockedFetchFolder.mockResolvedValue({ error } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, false); + + expect(result.error).toBe(error); + expect(result.data).toBeUndefined(); + }); + + it('should return only data when the backups are retrieved for a non-current device', async () => { + mockedConfigStore.get.mockReturnValue({}); + mockedFetchFolder.mockResolvedValue({ + data: { + children: [{ id: 1, uuid: 'folder-uuid', plainName: 'Documents' }], + }, + } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, false); + + expect(result).toStrictEqual({ + data: [ + { + name: 'Documents', + pathname: '.', + folderId: 1, + folderUuid: 'folder-uuid', + tmpPath: '', + backupsBucket: 'bucket-1', + }, + ], + }); + }); + + it('should return only enabled current backups with their mapped pathname', async () => { + mockedConfigStore.get.mockReturnValue({ + '/home/docs': { enabled: true, folderId: 1, folderUuid: 'folder-uuid-1' }, + '/home/photos': { enabled: false, folderId: 2, folderUuid: 'folder-uuid-2' }, + }); + mockedFindBackupPathnameFromId.mockImplementation(({ id }: { id: number }) => { + if (id === 1) return '/home/docs'; + if (id === 2) return '/home/photos'; + return undefined; + }); + mockedFetchFolder.mockResolvedValue({ + data: { + children: [ + { id: 1, uuid: 'folder-uuid-1', plainName: 'Documents', bucket: 'bucket-docs' }, + { id: 2, uuid: 'folder-uuid-2', plainName: 'Photos', bucket: 'bucket-photos' }, + ], + }, + } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, true); + + expect(result).toStrictEqual({ + data: [ + { + name: 'Documents', + pathname: '/home/docs', + folderId: 1, + folderUuid: 'folder-uuid-1', + tmpPath: '/tmp/backups', + backupsBucket: 'bucket-docs', + }, + ], + }); + }); + + it('should return an empty list when current backups are missing a pathname or are disabled', async () => { + mockedConfigStore.get.mockReturnValue({ + '/home/photos': { enabled: false, folderId: 2, folderUuid: 'folder-uuid-2' }, + }); + mockedFindBackupPathnameFromId.mockImplementation(({ id }: { id: number }) => { + if (id === 2) return '/home/photos'; + return undefined; + }); + mockedFetchFolder.mockResolvedValue({ + data: { + children: [ + { id: 1, uuid: 'folder-uuid-1', plainName: 'Documents', bucket: 'bucket-docs' }, + { id: 2, uuid: 'folder-uuid-2', plainName: 'Photos', bucket: 'bucket-photos' }, + ], + }, + } as never); + + const result = await getBackupsFromDevice({ uuid: 'device-uuid', bucket: 'bucket-1' } as never, true); + + expect(result).toStrictEqual({ data: [] }); + }); +}); diff --git a/src/backend/features/device/getBackupsFromDevice.ts b/src/backend/features/device/getBackupsFromDevice.ts index 460a3c307f..69a2aaa66b 100644 --- a/src/backend/features/device/getBackupsFromDevice.ts +++ b/src/backend/features/device/getBackupsFromDevice.ts @@ -3,15 +3,19 @@ import { fetchFolder } from '../../../infra/drive-server/services/folder/service import configStore from '../../../apps/main/config'; import { BackupInfo } from './../../../apps/backups/BackupInfo'; import { Device } from '../backup/types/Device'; +import { Result } from '../../../context/shared/domain/Result'; +import { createAbsolutePath } from '../../../context/local/localFile/infrastructure/AbsolutePath'; import { FolderDto } from '../../../infra/drive-server/out/dto'; import { mapFolderDtoToBackupInfo } from './utils/mapFolderDtoToBackupInfo'; import { findBackupPathnameFromId } from '../backup/find-backup-pathname-from-id'; -export async function getBackupsFromDevice(device: Device, isCurrent?: boolean): Promise> { +export async function getBackupsFromDevice( + device: Device, + isCurrent?: boolean, +): Promise, Error>> { const { data: folder, error } = await fetchFolder(device.uuid); - if (error) { - throw error; - } + if (error) return { error }; + if (isCurrent) { const backupsList = configStore.get('backupList'); const result = folder.children @@ -23,16 +27,18 @@ export async function getBackupsFromDevice(device: Device, isCurrent?: boolean): return !!(backup.pathname && backupsList[backup.pathname]?.enabled); }) .map(mapFolderDtoToBackupInfo); - return result; - } else { - const result = folder.children.map((backup) => ({ - name: backup.plainName, - pathname: '', - folderId: backup.id, - folderUuid: backup.uuid, - tmpPath: '', - backupsBucket: device.bucket, - })); - return result; + + return { data: result }; } + + const result = folder.children.map((backup) => ({ + name: backup.plainName, + pathname: createAbsolutePath(''), + folderId: backup.id, + folderUuid: backup.uuid, + tmpPath: '', + backupsBucket: device.bucket, + })); + + return { data: result }; } diff --git a/src/backend/features/device/getOrCreateDevice.test.ts b/src/backend/features/device/getOrCreateDevice.test.ts index fc7747d68c..30dc544120 100644 --- a/src/backend/features/device/getOrCreateDevice.test.ts +++ b/src/backend/features/device/getOrCreateDevice.test.ts @@ -1,8 +1,11 @@ -import { getOrCreateDevice } from './getOrCreateDevice'; -import { getDeviceIdentifier } from './getDeviceIdentifier'; +import configStore from '../../../apps/main/config'; +import { DependencyInjectionUserProvider } from '../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; +import { createAndSetupNewDevice } from './createAndSetupNewDevice'; import { fetchDevice } from './fetchDevice'; -import configStore from '../../../apps/main/config'; +import { fetchDeviceLegacyAndMigrate } from './fetchDeviceLegacyAndMigrate'; +import { getDeviceIdentifier } from './getDeviceIdentifier'; +import { getOrCreateDevice } from './getOrCreateDevice'; vi.mock('./getDeviceIdentifier'); vi.mock('./addUnknownDeviceIssue'); @@ -20,10 +23,143 @@ describe('getOrCreateDevice', () => { const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier); const mockedAddUnknownDeviceIssue = vi.mocked(addUnknownDeviceIssue); const mockedFetchDevice = vi.mocked(fetchDevice); + const mockedFetchDeviceLegacyAndMigrate = vi.mocked(fetchDeviceLegacyAndMigrate); + const mockedCreateAndSetupNewDevice = vi.mocked(createAndSetupNewDevice); const mockedConfigStore = vi.mocked(configStore); + const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get); + const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser); beforeEach(() => { vi.clearAllMocks(); + mockedUserProviderGet.mockReturnValue({ backupsBucket: '' } as never); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return ''; + return undefined; + }); + }); + + it('should return the identifier error when the device identifier is unavailable', async () => { + const error = new Error('Unsupported platform'); + mockedGetDeviceIdentifier.mockReturnValue({ error }); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ error }); + expect(mockedFetchDevice).not.toHaveBeenCalled(); + expect(mockedFetchDeviceLegacyAndMigrate).not.toHaveBeenCalled(); + }); + + it('should return the existing device and update the user bucket when no saved identifiers exist', async () => { + const user = { backupsBucket: '' }; + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedUserProviderGet.mockReturnValue(user as never); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedFetchDevice.mockResolvedValue({ data: device } as never); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDevice).toHaveBeenCalledWith({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + expect(user.backupsBucket).toBe('bucket-1'); + expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user); + }); + + it('should create a new device when the current identifier lookup does not find one', async () => { + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedFetchDevice.mockResolvedValue({ error: new Error('Not found') } as never); + mockedCreateAndSetupNewDevice.mockResolvedValue({ data: device }); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedCreateAndSetupNewDevice).toHaveBeenCalled(); + }); + + it('should report the setup error when creating a new device fails', async () => { + const setupError = new Error('Create device failed'); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedFetchDevice.mockResolvedValue({ error: new Error('Not found') } as never); + mockedCreateAndSetupNewDevice.mockResolvedValue({ error: setupError }); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ error: setupError }); + expect(mockedAddUnknownDeviceIssue).toHaveBeenCalledWith(setupError); + }); + + it('should use the saved uuid when it exists in config', async () => { + const device = { + id: 1, + uuid: 'saved-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return 'saved-uuid'; + return undefined; + }); + mockedFetchDeviceLegacyAndMigrate.mockResolvedValue({ data: device } as never); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDeviceLegacyAndMigrate).toHaveBeenCalledWith({ uuid: 'saved-uuid' }); + expect(mockedFetchDevice).not.toHaveBeenCalled(); + }); + + it('should use the legacy id when there is no saved uuid', async () => { + const device = { + id: 1, + uuid: 'legacy-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return 42; + if (key === 'deviceUUID') return ''; + return undefined; + }); + mockedFetchDeviceLegacyAndMigrate.mockResolvedValue({ data: device } as never); + + const result = await getOrCreateDevice(); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDeviceLegacyAndMigrate).toHaveBeenCalledWith({ legacyId: '42' }); }); describe('when an unexpected error is thrown', () => { @@ -55,11 +191,6 @@ describe('getOrCreateDevice', () => { mockedGetDeviceIdentifier.mockReturnValue({ data: { key: 'key', platform: 'linux', hostname: 'host' }, }); - mockedConfigStore.get.mockImplementation((key: string) => { - if (key === 'deviceId') return -1; - if (key === 'deviceUUID') return ''; - return undefined; - }); const fetchError = new Error('Network error'); mockedFetchDevice.mockRejectedValue(fetchError); diff --git a/src/backend/features/device/getOrCreateDevice.ts b/src/backend/features/device/getOrCreateDevice.ts index 170292fce4..70bbfec58a 100644 --- a/src/backend/features/device/getOrCreateDevice.ts +++ b/src/backend/features/device/getOrCreateDevice.ts @@ -1,13 +1,10 @@ import { Device } from '../backup/types/Device'; -import configStore from '../../../apps/main/config'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { addUnknownDeviceIssue } from './addUnknownDeviceIssue'; -import { fetchDeviceLegacyAndMigrate } from './fetchDeviceLegacyAndMigrate'; -import { fetchDevice } from './fetchDevice'; import { createAndSetupNewDevice } from './createAndSetupNewDevice'; import { getDeviceIdentifier } from './getDeviceIdentifier'; -import { Result } from './../../../context/shared/domain/Result'; -import { DependencyInjectionUserProvider } from './../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { Result } from '../../../context/shared/domain/Result'; +import { fetchSavedOrCurrentDevice, syncUserBackupsBucket } from './utils/getOrCreateDeviceHelpers'; async function handleFetchDeviceResult(deviceResult: Result) { if (deviceResult.error) { @@ -22,39 +19,16 @@ async function handleFetchDeviceResult(deviceResult: Result) { return { data }; } - const user = DependencyInjectionUserProvider.get(); - user.backupsBucket = deviceResult.data.bucket; - DependencyInjectionUserProvider.updateUser(user); - + syncUserBackupsBucket({ device: deviceResult.data }); return { data: deviceResult.data }; } -export async function getOrCreateDevice(): Promise> { +export async function getOrCreateDevice() { try { - const { error, data } = getDeviceIdentifier(); + const { error, data: deviceIdentifier } = getDeviceIdentifier(); if (error) return { error }; - const legacyId = configStore.get('deviceId'); - const savedUUID = configStore.get('deviceUUID'); - logger.debug({ - tag: 'BACKUPS', - msg: '[DEVICE] Checking saved device identifiers', - legacyId, - savedUUID, - }); - - const hasLegacyId = legacyId !== -1; - const hasUuid = savedUUID !== ''; - if (!hasLegacyId && !hasUuid) { - const result = await fetchDevice({ deviceIdentifier: data }); - return await handleFetchDeviceResult(result); - } - - /* eventually, this whole if section is going to be replaced - when all the users naturaly migrated to the new identification mechanism */ - const prop = hasUuid ? { uuid: savedUUID } : { legacyId: legacyId.toString() }; - - const deviceResult = await fetchDeviceLegacyAndMigrate(prop); + const deviceResult = await fetchSavedOrCurrentDevice({ deviceIdentifier }); return await handleFetchDeviceResult(deviceResult); } catch (error) { const unknownError = error instanceof Error ? error : new Error('Unexpected error in getOrCreateDevice'); diff --git a/src/backend/features/device/renameDevice.test.ts b/src/backend/features/device/renameDevice.test.ts new file mode 100644 index 0000000000..c80e0cfffd --- /dev/null +++ b/src/backend/features/device/renameDevice.test.ts @@ -0,0 +1,67 @@ +import { driveServerModule } from '../../../infra/drive-server/drive-server.module'; +import { getDeviceIdentifier } from './getDeviceIdentifier'; +import { renameDevice } from './renameDevice'; + +vi.mock('./getDeviceIdentifier'); +vi.mock('../../../infra/drive-server/drive-server.module', () => ({ + driveServerModule: { + backup: { + updateDeviceByIdentifier: vi.fn(), + }, + }, +})); + +describe('renameDevice', () => { + const mockedGetDeviceIdentifier = vi.mocked(getDeviceIdentifier); + const mockedUpdateDeviceByIdentifier = vi.mocked(driveServerModule.backup.updateDeviceByIdentifier); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return only error when the device identifier is unavailable', async () => { + const error = new Error('No device identifier'); + mockedGetDeviceIdentifier.mockReturnValue({ error }); + + const result = await renameDevice('new-name'); + + expect(result.error).toBe(error); + expect(result.data).toBeUndefined(); + expect(mockedUpdateDeviceByIdentifier).not.toHaveBeenCalled(); + }); + + it('should return only data when the rename succeeds', async () => { + const device = { uuid: 'uuid-1', name: 'new-name', bucket: 'bucket-1' }; + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'device-key', platform: 'linux', hostname: 'host' }, + }); + mockedUpdateDeviceByIdentifier.mockResolvedValue({ + isRight: () => true, + isLeft: () => false, + getRight: () => device, + getLeft: () => undefined, + } as never); + + const result = await renameDevice('new-name'); + + expect(result).toStrictEqual({ data: device }); + }); + + it('should return only error when the rename request fails', async () => { + const error = new Error('Rename failed'); + mockedGetDeviceIdentifier.mockReturnValue({ + data: { key: 'device-key', platform: 'linux', hostname: 'host' }, + }); + mockedUpdateDeviceByIdentifier.mockResolvedValue({ + isRight: () => false, + isLeft: () => true, + getRight: () => undefined, + getLeft: () => error, + } as never); + + const result = await renameDevice('new-name'); + + expect(result.error).toBe(error); + expect(result.data).toBeUndefined(); + }); +}); diff --git a/src/backend/features/device/renameDevice.ts b/src/backend/features/device/renameDevice.ts index e2e86f3b80..6fabcb19dd 100644 --- a/src/backend/features/device/renameDevice.ts +++ b/src/backend/features/device/renameDevice.ts @@ -1,17 +1,33 @@ +import { logger } from '@internxt/drive-desktop-core/build/backend'; import { Device } from '../backup/types/Device'; +import { Result } from '../../../context/shared/domain/Result'; import { driveServerModule } from '../../../infra/drive-server/drive-server.module'; import { getDeviceIdentifier } from './getDeviceIdentifier'; -export async function renameDevice(deviceName: string): Promise { - const deviceIdentifier = getDeviceIdentifier(); - if (deviceIdentifier.error) { - throw new Error('Error in the request to rename a device'); - } +export async function renameDevice(deviceName: string): Promise> { + try { + const { error, data: deviceIdentifier } = getDeviceIdentifier(); + if (error) return { error }; + + const response = await driveServerModule.backup.updateDeviceByIdentifier(deviceIdentifier.key, deviceName); + if (response.isRight()) { + return { data: response.getRight() }; + } - const response = await driveServerModule.backup.updateDeviceByIdentifier(deviceIdentifier.data.key, deviceName); - if (response.isRight()) { - return response.getRight(); - } else { - throw new Error('Error in the request to rename a device'); + const requestError = response.getLeft(); + logger.error({ + tag: 'BACKUPS', + msg: 'Error in the request to rename a device', + error: requestError, + }); + return { error: requestError }; + } catch (error) { + const unexpectedError = error instanceof Error ? error : new Error('Unexpected error renaming device'); + logger.error({ + tag: 'BACKUPS', + msg: 'Unexpected error renaming device', + error: unexpectedError, + }); + return { error: unexpectedError }; } } diff --git a/src/backend/features/device/tryCreateDevice.ts b/src/backend/features/device/tryCreateDevice.ts index bf15078ce5..7c283781cf 100644 --- a/src/backend/features/device/tryCreateDevice.ts +++ b/src/backend/features/device/tryCreateDevice.ts @@ -1,15 +1,14 @@ import { Device } from '../backup/types/Device'; -import { left, right } from './../../../context/shared/domain/Either'; +import { Result } from '../../../context/shared/domain/Result'; import { driveServerModule } from './../../../infra/drive-server/drive-server.module'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { BackupError } from '../../../infra/drive-server/services/backup/backup.error'; -import { Either } from './../../../context/shared/domain/Either'; import { DeviceIdentifierDTO } from './device.types'; export async function tryCreateDevice( deviceName: string, deviceIdentifier: DeviceIdentifierDTO, -): Promise> { +): Promise> { const createDeviceEither = await driveServerModule.backup.createDeviceWithIdentifier({ name: deviceName, key: deviceIdentifier.key, @@ -17,21 +16,24 @@ export async function tryCreateDevice( platform: deviceIdentifier.platform, }); - if (createDeviceEither.isRight()) return right(createDeviceEither.getRight()); + if (createDeviceEither.isRight()) { + return { data: createDeviceEither.getRight() }; + } const createDeviceError = createDeviceEither.getLeft(); - if (createDeviceError instanceof BackupError && createDeviceError?.code === 'ALREADY_EXISTS') { + if (createDeviceError instanceof BackupError && createDeviceError.code === 'ALREADY_EXISTS') { logger.debug({ tag: 'BACKUPS', msg: 'Device name already exists', deviceName, }); - return left(createDeviceEither.getLeft()); + return { error: createDeviceError }; } - const error = logger.error({ + logger.error({ tag: 'BACKUPS', msg: 'Error creating device', + error: createDeviceError, }); - return left(error); + return { error: createDeviceError }; } diff --git a/src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts b/src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts new file mode 100644 index 0000000000..90e93dd42a --- /dev/null +++ b/src/backend/features/device/utils/getOrCreateDeviceHelpers.test.ts @@ -0,0 +1,143 @@ +import configStore from '../../../../apps/main/config'; +import { Device } from '../../../../apps/main/device/service'; +import { DependencyInjectionUserProvider } from '../../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { fetchDevice } from '../fetchDevice'; +import { fetchDeviceLegacyAndMigrate } from '../fetchDeviceLegacyAndMigrate'; +import { + fetchSavedOrCurrentDevice, + getSavedDeviceIdentifiers, + resolveFetchProps, + syncUserBackupsBucket, +} from './getOrCreateDeviceHelpers'; + +vi.mock('../fetchDevice'); +vi.mock('../fetchDeviceLegacyAndMigrate'); +vi.mock('../../../../apps/main/config', () => ({ + default: { get: vi.fn() }, +})); +vi.mock('../../../../apps/shared/dependency-injection/DependencyInjectionUserProvider', () => ({ + DependencyInjectionUserProvider: { get: vi.fn(), updateUser: vi.fn() }, +})); + +describe('getOrCreateDeviceHelpers', () => { + const mockedConfigStore = vi.mocked(configStore); + const mockedFetchDevice = vi.mocked(fetchDevice); + const mockedFetchDeviceLegacyAndMigrate = vi.mocked(fetchDeviceLegacyAndMigrate); + const mockedUserProviderGet = vi.mocked(DependencyInjectionUserProvider.get); + const mockedUserProviderUpdate = vi.mocked(DependencyInjectionUserProvider.updateUser); + + beforeEach(() => { + vi.clearAllMocks(); + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return ''; + return undefined; + }); + }); + + it('should read the saved device identifiers from config', () => { + const result = getSavedDeviceIdentifiers(); + + expect(result).toStrictEqual({ + legacyId: -1, + savedUUID: '', + hasLegacyId: false, + hasUuid: false, + }); + }); + + it('should resolve current identifier props when there are no saved identifiers', () => { + const result = resolveFetchProps({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + savedDeviceIdentifiers: { + legacyId: -1, + savedUUID: '', + hasLegacyId: false, + hasUuid: false, + }, + }); + + expect(result).toStrictEqual({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + }); + + it('should resolve saved uuid props when they exist', () => { + const result = resolveFetchProps({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + savedDeviceIdentifiers: { + legacyId: -1, + savedUUID: 'saved-uuid', + hasLegacyId: false, + hasUuid: true, + }, + }); + + expect(result).toStrictEqual({ uuid: 'saved-uuid' }); + }); + + it('should fetch the current device when no saved identifiers exist', async () => { + const device = { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + } satisfies Device; + mockedFetchDevice.mockResolvedValue({ data: device }); + + const result = await fetchSavedOrCurrentDevice({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDevice).toHaveBeenCalledWith({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + expect(mockedFetchDeviceLegacyAndMigrate).not.toHaveBeenCalled(); + }); + + it('should fetch the saved device when a uuid is stored', async () => { + const device = { + id: 1, + uuid: 'saved-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + } satisfies Device; + mockedConfigStore.get.mockImplementation((key: string) => { + if (key === 'deviceId') return -1; + if (key === 'deviceUUID') return 'saved-uuid'; + return undefined; + }); + mockedFetchDeviceLegacyAndMigrate.mockResolvedValue({ data: device }); + + const result = await fetchSavedOrCurrentDevice({ + deviceIdentifier: { key: 'key', platform: 'linux', hostname: 'host' }, + }); + + expect(result).toStrictEqual({ data: device }); + expect(mockedFetchDeviceLegacyAndMigrate).toHaveBeenCalledWith({ uuid: 'saved-uuid' }); + }); + + it('should sync the user backup bucket from the device', () => { + const user = { backupsBucket: '' }; + mockedUserProviderGet.mockReturnValue(user as never); + + syncUserBackupsBucket({ + device: { + id: 1, + uuid: 'device-uuid', + name: 'Laptop', + bucket: 'bucket-1', + removed: false, + hasBackups: true, + }, + }); + + expect(user.backupsBucket).toBe('bucket-1'); + expect(mockedUserProviderUpdate).toHaveBeenCalledWith(user); + }); +}); diff --git a/src/backend/features/device/utils/getOrCreateDeviceHelpers.ts b/src/backend/features/device/utils/getOrCreateDeviceHelpers.ts new file mode 100644 index 0000000000..acae7d8119 --- /dev/null +++ b/src/backend/features/device/utils/getOrCreateDeviceHelpers.ts @@ -0,0 +1,73 @@ +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import configStore from '../../../../apps/main/config'; +import { Device } from '../../../../apps/main/device/service'; +import { DependencyInjectionUserProvider } from '../../../../apps/shared/dependency-injection/DependencyInjectionUserProvider'; +import { Result } from '../../../../context/shared/domain/Result'; +import { DeviceIdentifierDTO } from '../device.types'; +import { fetchDevice } from '../fetchDevice'; +import { fetchDeviceLegacyAndMigrate } from '../fetchDeviceLegacyAndMigrate'; + +export type SavedDeviceIdentifiers = { + legacyId: number; + savedUUID: string; + hasLegacyId: boolean; + hasUuid: boolean; +}; + +export function syncUserBackupsBucket({ device }: { device: Device }) { + const user = DependencyInjectionUserProvider.get(); + user.backupsBucket = device.bucket; + DependencyInjectionUserProvider.updateUser(user); +} + +export function getSavedDeviceIdentifiers() { + const legacyId = configStore.get('deviceId'); + const savedUUID = configStore.get('deviceUUID'); + + logger.debug({ + tag: 'BACKUPS', + msg: '[DEVICE] Checking saved device identifiers', + legacyId, + savedUUID, + }); + + return { + legacyId, + savedUUID, + hasLegacyId: legacyId !== -1, + hasUuid: savedUUID !== '', + } satisfies SavedDeviceIdentifiers; +} + +export function resolveFetchProps({ + deviceIdentifier, + savedDeviceIdentifiers, +}: { + deviceIdentifier: DeviceIdentifierDTO; + savedDeviceIdentifiers: SavedDeviceIdentifiers; +}) { + if (!savedDeviceIdentifiers.hasLegacyId && !savedDeviceIdentifiers.hasUuid) { + return { deviceIdentifier }; + } + + /* eventually, this whole if section is going to be replaced + when all the users naturaly migrated to the new identification mechanism */ + return savedDeviceIdentifiers.hasUuid + ? { uuid: savedDeviceIdentifiers.savedUUID } + : { legacyId: savedDeviceIdentifiers.legacyId.toString() }; +} + +export async function fetchSavedOrCurrentDevice({ + deviceIdentifier, +}: { + deviceIdentifier: DeviceIdentifierDTO; +}): Promise> { + const savedDeviceIdentifiers = getSavedDeviceIdentifiers(); + const props = resolveFetchProps({ deviceIdentifier, savedDeviceIdentifiers }); + + if ('deviceIdentifier' in props) { + return await fetchDevice(props); + } + + return await fetchDeviceLegacyAndMigrate(props); +}