Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 34 additions & 26 deletions src/apps/backups/BackupService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { Environment } from '@internxt/inxt-js';
import { Mock } from 'vitest';
import { mockDeep } from 'vitest-mock-extended';
import { BackupService } from './BackupService';
import LocalTreeBuilder from '../../context/local/localTree/application/LocalTreeBuilder';
import { RemoteTreeBuilder } from '../../context/virtual-drive/remoteTree/application/RemoteTreeBuilder';
import { SimpleFolderCreator } from '../../context/virtual-drive/folders/application/create/SimpleFolderCreator';
import { BackupInfo } from './BackupInfo';
import { DriveDesktopError } from '../../context/shared/domain/errors/DriveDesktopError';
import { LocalTreeMother } from '../../context/local/localTree/domain/__test-helpers__/LocalTreeMother';
import { RemoteTreeMother } from '../../context/virtual-drive/remoteTree/domain/__test-helpers__/RemoteTreeMother';
import { left, right } from '../../context/shared/domain/Either';
import { left } from '../../context/shared/domain/Either';
import { RemoteTree } from '../../context/virtual-drive/remoteTree/domain/RemoteTree';
import { UsageModule } from '../../backend/features/usage/usage.module';
import { FolderMother } from '../../context/virtual-drive/folders/domain/__test-helpers__/FolderMother';
Expand All @@ -18,15 +17,19 @@ import * as executeAsyncQueueModule from '../../backend/common/async-queue/execu
import * as addFileToTrashModule from '../../infra/drive-server/services/files/services/add-file-to-trash';
import { partialSpyOn } from '../../../tests/vitest/utils.helper';
import { AbsolutePath } from '../../context/local/localFile/infrastructure/AbsolutePath';
import * as buildLocalTreeModule from '../../backend/features/backup/local-tree/';
import * as backupFeaturesModule from '../../backend/features/backup';

vi.mock(import('../../backend/features/usage/usage.module'));
vi.mock(import('../../backend/features/backup/local-tree/'));

describe('BackupService', () => {
const executeAsyncQueueMock = partialSpyOn(executeAsyncQueueModule, 'executeAsyncQueue');
const addFileToTrashMock = partialSpyOn(addFileToTrashModule, 'addFileToTrash');
const buildLocalTreeMock = vi.mocked(buildLocalTreeModule.buildLocalTree);
const backupErrorsTrackerAddMock = partialSpyOn(backupFeaturesModule.backupErrorsTracker, 'add');

let backupService: BackupService;
let localTreeBuilder: LocalTreeBuilder;
let remoteTreeBuilder: RemoteTreeBuilder;
let simpleFolderCreator: SimpleFolderCreator;
let environment: Environment;
Expand All @@ -44,7 +47,6 @@ describe('BackupService', () => {
};

beforeEach(() => {
localTreeBuilder = mockDeep<LocalTreeBuilder>();
remoteTreeBuilder = mockDeep<RemoteTreeBuilder>();
simpleFolderCreator = mockDeep<SimpleFolderCreator>();
environment = mockDeep<Environment>();
Expand All @@ -53,62 +55,73 @@ describe('BackupService', () => {
mockValidateSpace = vi.mocked(UsageModule.validateSpace);
abortController = new AbortController();

buildLocalTreeMock.mockResolvedValue({ data: { tree: LocalTreeMother.oneLevel(10), skippedItems: [] } });
vi.mocked(remoteTreeBuilder.run).mockResolvedValue(RemoteTreeMother.oneLevel(10));
mockValidateSpace.mockResolvedValue({ data: { hasSpace: true } });
vi.mocked(simpleFolderCreator.run).mockResolvedValue(FolderMother.any());
executeAsyncQueueMock.mockResolvedValue({ data: undefined });
addFileToTrashMock.mockResolvedValue({ data: true });

backupService = new BackupService(
localTreeBuilder,
remoteTreeBuilder,
simpleFolderCreator,
environment,
'backups-bucket',
);
backupService = new BackupService(remoteTreeBuilder, simpleFolderCreator, environment, 'backups-bucket');

mockValidateSpace.mockClear();
backupErrorsTrackerAddMock.mockClear();
});

it('should successfully run the backup process', async () => {
const localTree = LocalTreeMother.oneLevel(10);
const remoteTree = RemoteTreeMother.oneLevel(10);

vi.mocked(localTreeBuilder.run).mockResolvedValueOnce(right(localTree));
vi.mocked(remoteTreeBuilder.run).mockResolvedValueOnce(remoteTree);
mockValidateSpace.mockResolvedValueOnce({ data: { hasSpace: true } });

const result = await backupService.run(info, abortController.signal, tracker);

expect(result).toBeUndefined();
expect(localTreeBuilder.run).toHaveBeenCalledWith(info.pathname);
expect(remoteTreeBuilder.run).toHaveBeenCalledWith(info.folderId, info.folderUuid);
expect(buildLocalTreeMock).toHaveBeenCalledWith(info.pathname);
expect(remoteTreeBuilder.run).toHaveBeenCalledWith(info.folderId, info.folderUuid, true);
expect(tracker.incrementProcessed).toHaveBeenCalled();
});

it('should return an error if local tree generation fails', async () => {
const error = new DriveDesktopError('NOT_EXISTS', 'Failed to generate local tree');

vi.mocked(localTreeBuilder.run).mockResolvedValueOnce(left(error));
buildLocalTreeMock.mockResolvedValueOnce({ error });

const result = await backupService.run(info, abortController.signal, tracker);

expect(result).toBe(error);
expect(localTreeBuilder.run).toHaveBeenCalledWith(info.pathname);
expect(buildLocalTreeMock).toHaveBeenCalledWith(info.pathname);
});

it('should add skipped local tree items as individual backup issues', async () => {
const skippedError = new DriveDesktopError('ACTION_NOT_PERMITTED', 'Symbolic links are skipped');
buildLocalTreeMock.mockResolvedValueOnce({
data: {
tree: LocalTreeMother.oneLevel(10),
skippedItems: [{ path: '/path/to/backup/thunderbird-link' as AbsolutePath, error: skippedError }],
},
});

await backupService.run(info, abortController.signal, tracker);

expect(backupErrorsTrackerAddMock).toHaveBeenCalledWith(info.folderId, {
name: 'thunderbird-link',
error: 'ACTION_NOT_PERMITTED',
});
});

it('should return an error if remote tree generation fails', async () => {
const error = new DriveDesktopError('NOT_EXISTS', 'Failed to generate remote tree');

vi.mocked(localTreeBuilder.run).mockResolvedValueOnce(right(LocalTreeMother.oneLevel(10)));
vi.mocked(remoteTreeBuilder.run).mockResolvedValueOnce(left(error) as unknown as RemoteTree);

const result = await backupService.run(info, abortController.signal, tracker);

expect(result).toStrictEqual(new DriveDesktopError('UNKNOWN', 'An unknown error occurred'));
expect(remoteTreeBuilder.run).toHaveBeenCalledWith(info.folderId, info.folderUuid);
expect(remoteTreeBuilder.run).toHaveBeenCalledWith(info.folderId, info.folderUuid, true);
});

it('should return an error if there is not enough space', async () => {
vi.mocked(localTreeBuilder.run).mockResolvedValueOnce(right(LocalTreeMother.oneLevel(10)));
vi.mocked(remoteTreeBuilder.run).mockResolvedValueOnce(RemoteTreeMother.oneLevel(10));
mockValidateSpace.mockResolvedValueOnce({ data: { hasSpace: false } });

Expand All @@ -118,9 +131,7 @@ describe('BackupService', () => {
});

it('should return an unknown error for unexpected issues', async () => {
vi.mocked(localTreeBuilder.run).mockImplementationOnce(() => {
throw new Error('Unexpected error');
});
buildLocalTreeMock.mockRejectedValueOnce(new Error('Unexpected error'));

const result = await backupService.run(info, abortController.signal, tracker);

Expand All @@ -131,8 +142,6 @@ describe('BackupService', () => {
it('should propagate fatal error from uploadAndCreate', async () => {
const fatalError = new DriveDesktopError('NOT_ENOUGH_SPACE', 'No space left');

vi.mocked(localTreeBuilder.run).mockResolvedValueOnce(right(LocalTreeMother.oneLevel(10)));
vi.mocked(remoteTreeBuilder.run).mockResolvedValueOnce(RemoteTreeMother.oneLevel(10));
mockValidateSpace.mockResolvedValueOnce({ data: { hasSpace: true } });
executeAsyncQueueMock.mockResolvedValueOnce({ error: fatalError });

Expand All @@ -144,7 +153,6 @@ describe('BackupService', () => {
it('should propagate fatal error from uploadAndUpdate', async () => {
const fatalError = new DriveDesktopError('NOT_ENOUGH_SPACE', 'No space left');

vi.mocked(localTreeBuilder.run).mockResolvedValueOnce(right(LocalTreeMother.oneLevel(10)));
vi.mocked(remoteTreeBuilder.run).mockResolvedValueOnce(RemoteTreeMother.oneLevel(10));
mockValidateSpace.mockResolvedValueOnce({ data: { hasSpace: true } });
executeAsyncQueueMock.mockResolvedValueOnce({ data: undefined }).mockResolvedValueOnce({ error: fatalError });
Expand Down
23 changes: 15 additions & 8 deletions src/apps/backups/BackupService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import { DEFAULT_CONCURRENCY } from '../../backend/features/backup/upload/constants';
import { LocalFile } from '../../context/local/localFile/domain/LocalFile';
import { AbsolutePath } from '../../context/local/localFile/infrastructure/AbsolutePath';
import LocalTreeBuilder from '../../context/local/localTree/application/LocalTreeBuilder';
import { LocalTree } from '../../context/local/localTree/domain/LocalTree';
import { File } from '../../context/virtual-drive/files/domain/File';
import { SimpleFolderCreator } from '../../context/virtual-drive/folders/application/create/SimpleFolderCreator';
Expand All @@ -28,11 +27,12 @@
import { Either, left, right } from '../../context/shared/domain/Either';
import { addFileToTrash } from '../../infra/drive-server/services/files/services/add-file-to-trash';
import { createBackupUploadExecutor } from '../../backend/features/backup/upload/create-backup-upload-executor';
import { backupErrorsTracker } from '../../backend/features/backup';
import { buildLocalTree } from '../../backend/features/backup/local-tree';

@Service()
export class BackupService {
constructor(
private readonly localTreeBuilder: LocalTreeBuilder,
private readonly remoteTreeBuilder: RemoteTreeBuilder,
private readonly simpleFolderCreator: SimpleFolderCreator,
private readonly environment: Environment,
Expand All @@ -49,15 +49,22 @@

try {
logger.debug({ tag: 'BACKUPS', msg: 'Generating local tree' });
const localTreeEither = await this.localTreeBuilder.run(info.pathname as AbsolutePath);

if (localTreeEither.isLeft()) {
const error = localTreeEither.getLeft();
const { data, error } = await buildLocalTree(info.pathname as AbsolutePath);
if (error) {
logger.error({ tag: 'BACKUPS', msg: 'Error generating local tree:', error });
return error;
}

const local = localTreeEither.getRight();
if (data.skippedItems.length > 0) {
/** TODO PB-6391:

Check warning on line 58 in src/apps/backups/BackupService.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ4iGB_CGc6GMsbmSqCY&open=AZ4iGB_CGc6GMsbmSqCY&pullRequest=341
* Extend backup issues window to include
* skipped items details so the issues window can show each skipped path
*/
backupErrorsTracker.add(info.folderId, {
name: info.name,
error: 'ACTION_NOT_PERMITTED',
});
}
const local = data.tree;
logger.debug({ tag: 'BACKUPS', msg: 'Local tree generated successfully' });

logger.debug({ tag: 'BACKUPS', msg: 'Generating remote tree' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import { registerFilesServices } from './virtual-drive/registerFilesServices';
import { registerFolderServices } from './virtual-drive/registerFolderServices';
import { registerLocalFileServices } from './local/registerLocalFileServices';
import { BackupService } from '../BackupService';
import { registerLocalTreeServices } from './local/registerLocalTreeServices';
import { registerRemoteTreeServices } from './virtual-drive/registerRemoteTreeServices';
import { DependencyInjectionUserProvider } from '../../shared/dependency-injection/DependencyInjectionUserProvider';
import { DownloaderHandlerFactory } from '../../../context/storage/StorageFiles/domain/download/DownloaderHandlerFactory';
import { EnvironmentFileDownloaderHandlerFactory } from '../../../context/storage/StorageFiles/infrastructure/download/EnvironmentRemoteFileContentsManagersFactory';
import LocalTreeBuilder from '../../../context/local/localTree/application/LocalTreeBuilder';
import { RemoteTreeBuilder } from '../../../context/virtual-drive/remoteTree/application/RemoteTreeBuilder';
import { SimpleFolderCreator } from '../../../context/virtual-drive/folders/application/create/SimpleFolderCreator';

Expand All @@ -30,15 +28,13 @@ export class BackupsDependencyContainerFactory {
registerRemoteTreeServices(builder);

registerLocalFileServices(builder);
registerLocalTreeServices(builder);

builder
.register(DownloaderHandlerFactory)
.useFactory((c) => new EnvironmentFileDownloaderHandlerFactory(c.get(Environment), user.backupsBucket));

builder.register(BackupService).useFactory((c) => {
return new BackupService(
c.get(LocalTreeBuilder),
c.get(RemoteTreeBuilder),
c.get(SimpleFolderCreator),
c.get(Environment),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as precalculateBackupItemCountModule from './precalculate-backup-item-c
import { partialSpyOn } from 'tests/vitest/utils.helper';
import { loggerMock } from 'tests/vitest/mocks.helper';
import { AbsolutePath } from '../../../context/local/localFile/infrastructure/AbsolutePath';
import { RemoteTreeBuilder } from '../../../context/virtual-drive/remoteTree/application/RemoteTreeBuilder';

const makeBackup = (folderUuid: string): BackupInfo => ({
folderUuid,
Expand All @@ -17,11 +18,13 @@ const makeBackup = (folderUuid: string): BackupInfo => ({

describe('calculateBackupsItemsCount', () => {
let container: Container;
let remoteTreeBuilder: RemoteTreeBuilder;
let signal: AbortSignal;
const precalcuteBackupItemCountMock = partialSpyOn(precalculateBackupItemCountModule, 'precalculateBackupItemCount');

beforeEach(() => {
container = { get: vi.fn().mockReturnValue({}) } as unknown as Container;
remoteTreeBuilder = {} as RemoteTreeBuilder;
container = { get: vi.fn().mockReturnValue(remoteTreeBuilder) } as unknown as Container;
signal = new AbortController().signal;
});

Expand All @@ -43,13 +46,14 @@ describe('calculateBackupsItemsCount', () => {
expect(result.size).toBe(2);
});

it('calls precalculateBackupItemCount with the correct backup and container', async () => {
it('calls precalculateBackupItemCount with the correct backup and remote tree builder', async () => {
const backup = makeBackup('uuid-1');
precalcuteBackupItemCountMock.mockResolvedValueOnce({ data: 7 });

await calculateBackupsItemsCount({ backups: [backup], signal, container });

expect(precalcuteBackupItemCountMock).toBeCalledWith(backup, expect.anything(), expect.anything());
expect(container.get).toBeCalledWith(RemoteTreeBuilder);
expect(precalcuteBackupItemCountMock).toBeCalledWith(backup, remoteTreeBuilder);
});

it('stops processing when signal is already aborted', async () => {
Expand Down
5 changes: 2 additions & 3 deletions src/backend/features/backup/calculate-backups-items-count.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { precalculateBackupItemCount } from './precalculate-backup-item-count';
import { logger } from '@internxt/drive-desktop-core/build/backend';
import { BackupInfo } from '../../../apps/backups/BackupInfo';
import LocalTreeBuilder from '../../../context/local/localTree/application/LocalTreeBuilder';
import { RemoteTreeBuilder } from '../../../context/virtual-drive/remoteTree/application/RemoteTreeBuilder';
import { Container } from 'diod';

Expand All @@ -13,7 +12,6 @@ type Props = {

export async function calculateBackupsItemsCount({ backups, signal, container }: Props) {
const itemCounts = new Map<string, number>();
const localTreeBuilder = container.get(LocalTreeBuilder);
const remoteTreeBuilder = container.get(RemoteTreeBuilder);

for (const backup of backups) {
Expand All @@ -22,7 +20,8 @@ export async function calculateBackupsItemsCount({ backups, signal, container }:
break;
}

const result = await precalculateBackupItemCount(backup, localTreeBuilder, remoteTreeBuilder);
// eslint-disable-next-line no-await-in-loop
const result = await precalculateBackupItemCount(backup, remoteTreeBuilder);
if (result.error) {
logger.error({
tag: 'BACKUPS',
Expand Down
Loading
Loading