diff --git a/package.json b/package.json index c67f23b62d..78e208f3a5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir .", "reinstall:nautilus-extension": "NODE_ENV=development ts-node src/apps/nautilus-extension/reload.ts", - "lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=210", + "lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=60", "lint:fix": "npm run lint --fix", "format": "prettier src --check", "format:fix": "prettier src --write", diff --git a/src/apps/backups/BackupService.test.ts b/src/apps/backups/BackupService.test.ts index 3c526bb824..2a21f7d8dd 100644 --- a/src/apps/backups/BackupService.test.ts +++ b/src/apps/backups/BackupService.test.ts @@ -1,3 +1,4 @@ +import { Environment } from '@internxt/inxt-js'; import { Mock } from 'vitest'; import { mockDeep } from 'vitest-mock-extended'; import { BackupService } from './BackupService'; @@ -16,6 +17,7 @@ import { BackupProgressTracker } from '../../backend/features/backup/backup-prog import * as executeAsyncQueueModule from '../../backend/common/async-queue/execute-async-queue'; 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'; vi.mock(import('../../backend/features/usage/usage.module')); @@ -27,12 +29,13 @@ describe('BackupService', () => { let localTreeBuilder: LocalTreeBuilder; let remoteTreeBuilder: RemoteTreeBuilder; let simpleFolderCreator: SimpleFolderCreator; + let environment: Environment; let mockValidateSpace: Mock; let abortController: AbortController; let tracker: BackupProgressTracker; const info: BackupInfo = { - pathname: '/path/to/backup', + pathname: '/path/to/backup' as AbsolutePath, folderId: 123, folderUuid: 'uuid', tmpPath: '/tmp/path', @@ -44,6 +47,7 @@ describe('BackupService', () => { localTreeBuilder = mockDeep(); remoteTreeBuilder = mockDeep(); simpleFolderCreator = mockDeep(); + environment = mockDeep(); tracker = mockDeep(); mockValidateSpace = vi.mocked(UsageModule.validateSpace); @@ -57,7 +61,7 @@ describe('BackupService', () => { localTreeBuilder, remoteTreeBuilder, simpleFolderCreator, - {} as any, + environment, 'backups-bucket', ); @@ -77,7 +81,6 @@ describe('BackupService', () => { expect(result).toBeUndefined(); expect(localTreeBuilder.run).toHaveBeenCalledWith(info.pathname); expect(remoteTreeBuilder.run).toHaveBeenCalledWith(info.folderId, info.folderUuid); - expect(tracker.addToTotal).toHaveBeenCalled(); expect(tracker.incrementProcessed).toHaveBeenCalled(); }); diff --git a/src/apps/drive/__mocks__/ContainerMock.ts b/src/apps/drive/__mocks__/ContainerMock.ts index caa40d61d9..088670289d 100644 --- a/src/apps/drive/__mocks__/ContainerMock.ts +++ b/src/apps/drive/__mocks__/ContainerMock.ts @@ -5,11 +5,10 @@ export class ContainerMock implements Partial { get = vi.fn((service) => this.services.get(service)); - set(service: any, implementation: T): void { + set(service: Identifier, implementation: T): void { this.services.set(service, implementation); } - // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars findTaggedServiceIdentifiers(tag: string): Array> { return [] as Array>; diff --git a/src/apps/drive/fuse/FuseApp.test.ts b/src/apps/drive/fuse/FuseApp.test.ts index 01a42f2cbb..3b94c7a0cc 100644 --- a/src/apps/drive/fuse/FuseApp.test.ts +++ b/src/apps/drive/fuse/FuseApp.test.ts @@ -99,10 +99,7 @@ describe('FuseApp', () => { mountPromiseMock.mockRejectedValue(new Error('mount failed')); const startPromise = fuseApp.start(); - // eslint-disable-next-line no-await-in-loop - for (let i = 0; i < 5; i++) { - await vi.advanceTimersByTimeAsync(3000); - } + await vi.runAllTimersAsync(); await startPromise; expect(fuseApp.getStatus()).toBe('ERROR'); @@ -118,10 +115,7 @@ describe('FuseApp', () => { fuseApp.on('mount-error', mountErrorHandler); const startPromise = fuseApp.start(); - // eslint-disable-next-line no-await-in-loop - for (let i = 0; i < 5; i++) { - await vi.advanceTimersByTimeAsync(3000); - } + await vi.runAllTimersAsync(); await startPromise; expect(mountErrorHandler).toHaveBeenCalled(); diff --git a/src/apps/drive/fuse/callbacks/FuseCallback.ts b/src/apps/drive/fuse/callbacks/FuseCallback.ts index 82b44b9db7..9c762411df 100644 --- a/src/apps/drive/fuse/callbacks/FuseCallback.ts +++ b/src/apps/drive/fuse/callbacks/FuseCallback.ts @@ -31,7 +31,7 @@ export abstract class FuseCallback { }, ) {} - protected async executeAndCatch(params: any[]): Promise> { + protected async executeAndCatch(params: unknown[]): Promise> { // Ensure that an Either is always returned const stopwatch = new Stopwatch(); @@ -103,10 +103,10 @@ export abstract class FuseCallback { logger.debug({ msg: `${this.name}: `, message }); } - async handle(...params: any[]): Promise { + async handle(...params: unknown[]): Promise { const callback = params.pop() as CallbackWithData; - if (PathsToIgnore.some((regex) => regex.test(params[0]))) { + if (typeof params[0] === 'string' && PathsToIgnore.some((regex) => regex.test(params[0] as string))) { return callback(FuseCodes.EINVAL); } @@ -126,7 +126,7 @@ export abstract class FuseCallback { callback(FuseCallback.OK, data); } - abstract execute(...params: any[]): Promise>; + abstract execute(...params: unknown[]): Promise>; } export abstract class NotifyFuseCallback extends FuseCallback { @@ -134,7 +134,7 @@ export abstract class NotifyFuseCallback extends FuseCallback { return right(undefined); } - async handle(...params: any[]): Promise { + async handle(...params: unknown[]): Promise { const callback = params.pop() as Callback; if (this.debug.input) { diff --git a/src/apps/drive/fuse/callbacks/ReadCallback.test.ts b/src/apps/drive/fuse/callbacks/ReadCallback.test.ts index 0c1e6a4f61..d87be2c1a6 100644 --- a/src/apps/drive/fuse/callbacks/ReadCallback.test.ts +++ b/src/apps/drive/fuse/callbacks/ReadCallback.test.ts @@ -4,6 +4,7 @@ import * as handleReadModule from '../../../../backend/features/fuse/on-read/han import { partialSpyOn } from '../../../../../tests/vitest/utils.helper'; import { left, right } from '../../../../context/shared/domain/Either'; import { FuseNoSuchFileOrDirectoryError } from './FuseErrors'; +import { type Container } from 'diod'; const handleReadCallbackMock = partialSpyOn(handleReadModule, 'handleReadCallback'); @@ -18,7 +19,7 @@ function createMockContainer() { downloadFinished: vi.fn(), elapsedTime: vi.fn(), }), - } as any; + } as Partial as Container; } describe('ReadCallback', () => { diff --git a/src/apps/drive/fuse/callbacks/ReadCallback.ts b/src/apps/drive/fuse/callbacks/ReadCallback.ts index ff131b32ca..3b3557a3a0 100644 --- a/src/apps/drive/fuse/callbacks/ReadCallback.ts +++ b/src/apps/drive/fuse/callbacks/ReadCallback.ts @@ -19,14 +19,7 @@ import Fuse from '@gcas/fuse'; export class ReadCallback { constructor(private readonly container: Container) {} - async execute( - path: string, - _fd: any, - buf: Buffer, - len: number, - pos: number, - cb: (code: number, params?: any) => void, - ) { + async execute(path: string, _fd: number, buf: Buffer, len: number, pos: number, cb: (bytesRead?: number) => void) { try { const repo = this.container.get(StorageFilesRepository); const downloader = this.container.get(StorageFileDownloader); diff --git a/src/apps/drive/fuse/callbacks/ReleaseCallback.test.ts b/src/apps/drive/fuse/callbacks/ReleaseCallback.test.ts index 32cbe5545c..c28d0bf1a2 100644 --- a/src/apps/drive/fuse/callbacks/ReleaseCallback.test.ts +++ b/src/apps/drive/fuse/callbacks/ReleaseCallback.test.ts @@ -5,6 +5,7 @@ import { right } from '../../../../context/shared/domain/Either'; import * as openFlagsTracker from './../../../../backend/features/fuse/on-open/open-flags-tracker'; import * as handleReleaseModule from '../../../../backend/features/fuse/on-release/handle-release-callback'; import { partialSpyOn } from '../../../../../tests/vitest/utils.helper'; +import { Container } from 'diod'; vi.mock(import('@internxt/drive-desktop-core/build/backend')); @@ -12,7 +13,7 @@ describe('ReleaseCallback', () => { const onReleaseSpy = partialSpyOn(openFlagsTracker, 'onRelease'); const handleReleaseSpy = partialSpyOn(handleReleaseModule, 'handleReleaseCallback'); - const container = { get: vi.fn() } as any; + const container = { get: vi.fn() } as unknown as Container; const releaseCallback = new ReleaseCallback(container); it('should call onRelease to clean up open flags tracker', async () => { handleReleaseSpy.mockResolvedValue(right(undefined)); diff --git a/src/apps/drive/hydration-api/HydrationApi.test.ts b/src/apps/drive/hydration-api/HydrationApi.test.ts index 96f55e42af..067c4e254a 100644 --- a/src/apps/drive/hydration-api/HydrationApi.test.ts +++ b/src/apps/drive/hydration-api/HydrationApi.test.ts @@ -55,7 +55,7 @@ describe('HydrationApi', () => { await hydrationApi.start({ debug: true, timeElapsed: false }); - const response = await fetch('http://localhost:4567/hydration/test'); + await fetch('http://localhost:4567/hydration/test'); // The request itself may 404, but the debug middleware should have logged expect(loggerMock.debug).toBeCalledWith( expect.objectContaining({ diff --git a/src/apps/drive/hydration-api/controllers/contents.ts b/src/apps/drive/hydration-api/controllers/contents.ts index 3480e33f71..4468d73d82 100644 --- a/src/apps/drive/hydration-api/controllers/contents.ts +++ b/src/apps/drive/hydration-api/controllers/contents.ts @@ -1,5 +1,4 @@ import { Container } from 'diod'; -import { logger } from '@internxt/drive-desktop-core/build/backend'; import { NextFunction, Request, Response } from 'express'; import { extname } from 'path'; import { StorageFileDeleter } from '../../../../context/storage/StorageFiles/application/delete/StorageFileDeleter'; diff --git a/src/apps/drive/hydration-api/controllers/files.ts b/src/apps/drive/hydration-api/controllers/files.ts index 2e002fa1ba..02745291bf 100644 --- a/src/apps/drive/hydration-api/controllers/files.ts +++ b/src/apps/drive/hydration-api/controllers/files.ts @@ -14,17 +14,9 @@ export function buildFilesControllers(container: Container) { }; const filter = async (req: Request, res: Response) => { - const filter = Object.entries(req.query) - - .map(([key, param]) => { - return { key, value: param }; - }) - .reduce((partial: Partial, { key, value }: any) => { - return { - ...partial, - [key]: value.toString(), - }; - }, {}); + const filter = Object.fromEntries( + Object.entries(req.query).filter((entry): entry is [string, string] => typeof entry[1] === 'string'), + ) as Partial; const files = await container.get(FilesSearcherByPartialMatch).run(filter); diff --git a/src/apps/drive/index.ts b/src/apps/drive/index.ts index c48f372b68..3cd6da89e4 100644 --- a/src/apps/drive/index.ts +++ b/src/apps/drive/index.ts @@ -32,20 +32,18 @@ export async function startVirtualDrive() { await fuseApp.start(); } -export async function stopAndClearFuseApp() { - await stopHydrationApi(); - await stopFuseApp(); -} - -export async function updateFuseApp() { - await fuseApp.update(); -} +export async function stopHydrationApi() { + if (!hydrationApi) { + logger.debug({ msg: 'HydrationApi not initialized, skipping stop.' }); + return; + } -export function getFuseDriveState() { - if (!fuseApp) { - return 'UNMOUNTED'; + try { + logger.debug({ msg: 'Stopping HydrationApi...' }); + await hydrationApi.stop(); + } catch (error) { + logger.error({ msg: 'Error stopping HydrationApi:', error }); } - return fuseApp.getStatus(); } async function stopFuseApp() { @@ -64,16 +62,18 @@ async function stopFuseApp() { } } -export async function stopHydrationApi() { - if (!hydrationApi) { - logger.debug({ msg: 'HydrationApi not initialized, skipping stop.' }); - return; - } +export async function stopAndClearFuseApp() { + await stopHydrationApi(); + await stopFuseApp(); +} - try { - logger.debug({ msg: 'Stopping HydrationApi...' }); - await hydrationApi.stop(); - } catch (error) { - logger.error({ msg: 'Error stopping HydrationApi:', error }); +export async function updateFuseApp() { + await fuseApp.update(); +} + +export function getFuseDriveState() { + if (!fuseApp) { + return 'UNMOUNTED'; } + return fuseApp.getStatus(); } diff --git a/src/apps/main/antivirus/ClamAVDaemon.ts b/src/apps/main/antivirus/ClamAVDaemon.ts index c1d10e4f78..cdf11b1796 100644 --- a/src/apps/main/antivirus/ClamAVDaemon.ts +++ b/src/apps/main/antivirus/ClamAVDaemon.ts @@ -27,6 +27,126 @@ const MAX_SERVER_START_ATTEMPTS = 3; let lastRestartTime = 0; const MIN_RESTART_INTERVAL = 30000; // 30 seconds minimum between restarts +export const ensureDirectories = () => { + const dirs = [configDir, logDir, dbDir]; + for (const dir of dirs) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true, mode: DIRECTORY_MODE }); + logger.debug({ + tag: 'ANTIVIRUS', + msg: `[CLAM_AVD] Created directory: ${dir}`, + }); + } + } + + if (!fs.existsSync(logFilePath)) { + fs.writeFileSync(logFilePath, '', { mode: FILE_MODE }); + logger.debug({ + tag: 'ANTIVIRUS', + msg: `[CLAM_AVD] Created log file: ${logFilePath}`, + }); + } + + const resourceDbDir = path.join(RESOURCES_PATH, 'db'); + if (fs.existsSync(resourceDbDir)) { + const files = fs.readdirSync(resourceDbDir); + for (const file of files) { + const srcPath = path.join(resourceDbDir, file); + const destPath = path.join(dbDir, file); + if (!fs.existsSync(destPath)) { + fs.copyFileSync(srcPath, destPath); + logger.debug({ + tag: 'ANTIVIRUS', + msg: `[CLAM_AVD] Copied database file: ${file}`, + }); + } + } + } +}; + +/** + * Prepares the configuration files by replacing placeholder variables with actual paths + * This allows config files to be portable and work in any user environment + */ +export const prepareConfigFiles = (): { + clamdConfigPath: string; + freshclamConfigPath: string; +} => { + // Create temporary modified configs in the user's config directory + const tempClamdConfigPath = path.join(configDir, 'clamd.conf'); + const tempFreshclamConfigPath = path.join(configDir, 'freshclam.conf'); + + // Read the original config files from resources + const originalClamdConfig = fs.readFileSync(path.join(RESOURCES_PATH, '/etc/clamd.conf'), 'utf8'); + const originalFreshclamConfig = fs.readFileSync(path.join(RESOURCES_PATH, '/etc/freshclam.conf'), 'utf8'); + + const modifiedClamdConfig = originalClamdConfig + .replace('LOGFILE_PATH', logFilePath) + .replace('DATABASE_DIRECTORY', dbDir); + + const modifiedFreshclamConfig = originalFreshclamConfig + .replace('DATABASE_DIRECTORY', dbDir) + .replace('FRESHCLAM_LOG_PATH', freshclamLogPath); + + fs.writeFileSync(tempClamdConfigPath, modifiedClamdConfig); + fs.writeFileSync(tempFreshclamConfigPath, modifiedFreshclamConfig); + + logger.debug({ + tag: 'ANTIVIRUS', + msg: `[CLAM_AVD] Created modified config files in ${configDir}`, + }); + + return { + clamdConfigPath: tempClamdConfigPath, + freshclamConfigPath: tempFreshclamConfigPath, + }; +}; + +export const getEnvWithLibraryPath = () => { + const env = { ...process.env }; + const libPath = path.join(RESOURCES_PATH, 'lib'); + + env.LD_LIBRARY_PATH = `${libPath}:${env.LD_LIBRARY_PATH || ''}`; + + logger.debug({ + tag: 'ANTIVIRUS', + msg: `[CLAM_AVD] Setting library path to: ${libPath}`, + }); + return env; +}; + +export const checkClamdAvailability = (host = SERVER_HOST, port = SERVER_PORT): Promise => { + return new Promise((resolve) => { + const client = new net.Socket(); + + client.connect(port, host, () => { + client.end(); + resolve(true); + }); + + client.on('error', () => { + client.destroy(); + resolve(false); + }); + }); +}; + +const stopClamdServer = (): void => { + if (clamdProcess) { + logger.debug({ + tag: 'ANTIVIRUS', + msg: '[CLAM_AVD] Stopping clamd server...', + }); + clamdProcess.kill(); + clamdProcess = null; + } + + if (timer) { + clearTimeout(timer); + timer = null; + } +}; + const startClamdServer = async (): Promise => { logger.debug({ tag: 'ANTIVIRUS', @@ -145,97 +265,6 @@ const startClamdServer = async (): Promise => { } }; -const stopClamdServer = (): void => { - if (clamdProcess) { - logger.debug({ - tag: 'ANTIVIRUS', - msg: '[CLAM_AVD] Stopping clamd server...', - }); - clamdProcess.kill(); - clamdProcess = null; - } - - if (timer) { - clearTimeout(timer); - timer = null; - } -}; - -export const ensureDirectories = () => { - const dirs = [configDir, logDir, dbDir]; - for (const dir of dirs) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true, mode: DIRECTORY_MODE }); - logger.debug({ - tag: 'ANTIVIRUS', - msg: `[CLAM_AVD] Created directory: ${dir}`, - }); - } - } - - if (!fs.existsSync(logFilePath)) { - fs.writeFileSync(logFilePath, '', { mode: FILE_MODE }); - logger.debug({ - tag: 'ANTIVIRUS', - msg: `[CLAM_AVD] Created log file: ${logFilePath}`, - }); - } - - const resourceDbDir = path.join(RESOURCES_PATH, 'db'); - if (fs.existsSync(resourceDbDir)) { - const files = fs.readdirSync(resourceDbDir); - for (const file of files) { - const srcPath = path.join(resourceDbDir, file); - const destPath = path.join(dbDir, file); - if (!fs.existsSync(destPath)) { - fs.copyFileSync(srcPath, destPath); - logger.debug({ - tag: 'ANTIVIRUS', - msg: `[CLAM_AVD] Copied database file: ${file}`, - }); - } - } - } -}; - -/** - * Prepares the configuration files by replacing placeholder variables with actual paths - * This allows config files to be portable and work in any user environment - */ -export const prepareConfigFiles = (): { - clamdConfigPath: string; - freshclamConfigPath: string; -} => { - // Create temporary modified configs in the user's config directory - const tempClamdConfigPath = path.join(configDir, 'clamd.conf'); - const tempFreshclamConfigPath = path.join(configDir, 'freshclam.conf'); - - // Read the original config files from resources - const originalClamdConfig = fs.readFileSync(path.join(RESOURCES_PATH, '/etc/clamd.conf'), 'utf8'); - const originalFreshclamConfig = fs.readFileSync(path.join(RESOURCES_PATH, '/etc/freshclam.conf'), 'utf8'); - - const modifiedClamdConfig = originalClamdConfig - .replace('LOGFILE_PATH', logFilePath) - .replace('DATABASE_DIRECTORY', dbDir); - - const modifiedFreshclamConfig = originalFreshclamConfig - .replace('DATABASE_DIRECTORY', dbDir) - .replace('FRESHCLAM_LOG_PATH', freshclamLogPath); - - fs.writeFileSync(tempClamdConfigPath, modifiedClamdConfig); - fs.writeFileSync(tempFreshclamConfigPath, modifiedFreshclamConfig); - - logger.debug({ - tag: 'ANTIVIRUS', - msg: `[CLAM_AVD] Created modified config files in ${configDir}`, - }); - - return { - clamdConfigPath: tempClamdConfigPath, - freshclamConfigPath: tempFreshclamConfigPath, - }; -}; - const restartClamdServerIfNeeded = async (): Promise => { const now = Date.now(); @@ -283,35 +312,6 @@ const restartClamdServerIfNeeded = async (): Promise => { } }; -export const checkClamdAvailability = (host = SERVER_HOST, port = SERVER_PORT): Promise => { - return new Promise((resolve) => { - const client = new net.Socket(); - - client.connect(port, host, () => { - client.end(); - resolve(true); - }); - - client.on('error', () => { - client.destroy(); - resolve(false); - }); - }); -}; - -export const getEnvWithLibraryPath = () => { - const env = { ...process.env }; - const libPath = path.join(RESOURCES_PATH, 'lib'); - - env.LD_LIBRARY_PATH = `${libPath}:${env.LD_LIBRARY_PATH || ''}`; - - logger.debug({ - tag: 'ANTIVIRUS', - msg: `[CLAM_AVD] Setting library path to: ${libPath}`, - }); - return env; -}; - const waitForClamd = async ( timeout = DEFAULT_CLAMD_WAIT_TIMEOUT, interval = DEFAULT_CLAMD_CHECK_INTERVAL, diff --git a/src/apps/main/antivirus/utils/errorUtils.ts b/src/apps/main/antivirus/utils/errorUtils.ts index 3b5056b85a..3b6a5ade98 100644 --- a/src/apps/main/antivirus/utils/errorUtils.ts +++ b/src/apps/main/antivirus/utils/errorUtils.ts @@ -16,14 +16,14 @@ export function getErrorMessage(error: unknown): string { if (error && typeof error === 'object') { if ('data' in error) { - const data = (error as any).data; - if (data && data.err) { + const data = (error as Record).data; + if (data && typeof data === 'object' && 'err' in data) { return `ClamAV Error: ${getErrorMessage(data.err)}`; } } - if ('message' in error && typeof (error as any).message === 'string') { - return (error as any).message; + if ('message' in error && typeof (error as Record).message === 'string') { + return (error as Record).message as string; } try { diff --git a/src/apps/main/antivirus/utils/isPermissionError.test.ts b/src/apps/main/antivirus/utils/isPermissionError.test.ts index fb3fba30bb..b0ba2d22cb 100644 --- a/src/apps/main/antivirus/utils/isPermissionError.test.ts +++ b/src/apps/main/antivirus/utils/isPermissionError.test.ts @@ -49,14 +49,14 @@ describe('isPermissionError', () => { nestedError.code = 'EACCES'; const clamError = new NodeClamError('Clam error'); - (clamError as any).data = { err: nestedError }; + clamError.data = { err: nestedError }; expect(isPermissionError(clamError)).toBe(true); }); it('should handle NodeClamError without nested error', () => { const clamError = new NodeClamError('Clam error'); - (clamError as any).data = {}; + clamError.data = {}; expect(isPermissionError(clamError)).toBe(false); }); diff --git a/src/apps/main/antivirus/utils/isPermissionError.ts b/src/apps/main/antivirus/utils/isPermissionError.ts index 9bbc212a04..8d0c397464 100644 --- a/src/apps/main/antivirus/utils/isPermissionError.ts +++ b/src/apps/main/antivirus/utils/isPermissionError.ts @@ -5,15 +5,16 @@ const PERMISSION_ERROR_CODES = ['EACCES', 'EPERM', 'EBUSY', 'ENOENT', 'ENOFILE', const PERMISSION_ERROR_MESSAGES = ['operation not permitted', 'access denied', 'access is denied']; export const isPermissionError = (err: unknown) => { - let error = err as any; if (!err || typeof err !== 'object') return false; - if (err instanceof NodeClamError && (err as any).data?.err instanceof Error) { - error = (err as any).data.err; + let error: { message?: string; code?: string } = err as { message?: string; code?: string }; + + if (err instanceof NodeClamError && err.data?.err instanceof Error) { + error = err.data.err; } - const msg = error.message?.toLowerCase() || ''; - const hasPermissionErrorCode = PERMISSION_ERROR_CODES.includes(error.code); + const msg = error.message?.toLowerCase() ?? ''; + const hasPermissionErrorCode = PERMISSION_ERROR_CODES.includes(error.code ?? ''); const hasPermissionErrorMessage = PERMISSION_ERROR_MESSAGES.some((m) => msg.includes(m)); return hasPermissionErrorCode || hasPermissionErrorMessage; diff --git a/src/apps/main/auth/deeplink/setup-appimage-deeplink.ts b/src/apps/main/auth/deeplink/setup-appimage-deeplink.ts index f36907ef73..f0c5caf925 100644 --- a/src/apps/main/auth/deeplink/setup-appimage-deeplink.ts +++ b/src/apps/main/auth/deeplink/setup-appimage-deeplink.ts @@ -9,22 +9,6 @@ const execAsync = promisify(exec); const DESKTOP_FILE = join(homedir(), '.local/share/applications/internxt-appimage.desktop'); -export async function setupAppImageDeeplink() { - const appImagePath = process.env.APPIMAGE; - if (!appImagePath) return; - - await ensureDotDesktopUpdated(appImagePath); -} - -async function ensureDotDesktopUpdated(currentPath: string) { - const previousPath = await extractExecPath(); - - if (previousPath !== currentPath) { - await installDesktopFile(currentPath); - await registerProtocol(); - } -} - async function extractExecPath() { try { await access(DESKTOP_FILE); @@ -59,3 +43,19 @@ async function registerProtocol() { logger.error({ tag: 'AUTH', msg: 'Failed to register protocol:', err }); } } + +async function ensureDotDesktopUpdated(currentPath: string) { + const previousPath = await extractExecPath(); + + if (previousPath !== currentPath) { + await installDesktopFile(currentPath); + await registerProtocol(); + } +} + +export async function setupAppImageDeeplink() { + const appImagePath = process.env.APPIMAGE; + if (!appImagePath) return; + + await ensureDotDesktopUpdated(appImagePath); +} diff --git a/src/apps/main/auth/handlers.ts b/src/apps/main/auth/handlers.ts index c8d1c55721..47896241b3 100644 --- a/src/apps/main/auth/handlers.ts +++ b/src/apps/main/auth/handlers.ts @@ -8,6 +8,12 @@ import { getCredentials } from './get-credentials'; let isLoggedIn = false; +export function setIsLoggedIn(value: boolean) { + isLoggedIn = value; + + getWidget()?.webContents.send('user-logged-in-changed', value); +} + function initializeLoginState() { const { newToken } = getCredentials(); if (getUser() && newToken) { @@ -15,12 +21,6 @@ function initializeLoginState() { } } -export function setIsLoggedIn(value: boolean) { - isLoggedIn = value; - - getWidget()?.webContents.send('user-logged-in-changed', value); -} - export function getIsLoggedIn() { return isLoggedIn; } diff --git a/src/apps/main/backups/create-backup.test.ts b/src/apps/main/backups/create-backup.test.ts index da6482d826..d6fa81326e 100644 --- a/src/apps/main/backups/create-backup.test.ts +++ b/src/apps/main/backups/create-backup.test.ts @@ -3,6 +3,7 @@ import { createBackupFolder } from './create-backup-folder'; import configStore from '../config'; import { app } from 'electron'; import path from 'node:path'; +import { DriveServerError } from 'src/infra/drive-server/drive-server.error'; vi.mock('./create-backup-folder'); vi.mock('../config'); @@ -77,7 +78,7 @@ describe('createBackup', () => { it('should return undefined when createBackupFolder fails', async () => { mockPostBackup.mockResolvedValue({ - error: new Error('Failed to create backup folder') as any, + error: new DriveServerError('NOT_FOUND'), }); const result = await createBackup({ diff --git a/src/apps/main/backups/create-backup.ts b/src/apps/main/backups/create-backup.ts index df43b8240e..888f1d1e96 100644 --- a/src/apps/main/backups/create-backup.ts +++ b/src/apps/main/backups/create-backup.ts @@ -27,7 +27,7 @@ export async function createBackup({ pathname, device }: Props) { const createdBackup: BackupInfo = { folderUuid: newBackup.uuid, folderId: newBackup.id, - pathname: pathname, + pathname, name: base, tmpPath: app.getPath('temp'), backupsBucket: device.bucket, diff --git a/src/apps/main/backups/enable-existing-backup.test.ts b/src/apps/main/backups/enable-existing-backup.test.ts index f2c3152018..fc9f466a11 100644 --- a/src/apps/main/backups/enable-existing-backup.test.ts +++ b/src/apps/main/backups/enable-existing-backup.test.ts @@ -4,6 +4,8 @@ import { fetchFolder } from '../../../infra/drive-server/services/folder/service import { createBackup } from './create-backup'; import { migrateBackupEntryIfNeeded } from '../device/migrate-backup-entry-if-needed'; import { app } from 'electron'; +import { DriveServerError } from '../../../infra/drive-server/drive-server.error'; +import { GetFolderContentDto } from '../../../infra/drive-server/out/dto'; vi.mock('../config'); vi.mock('../../../infra/drive-server/services/folder/services/fetch-folder'); @@ -49,7 +51,7 @@ describe('enableExistingBackup', () => { mockedConfigStore.get.mockReturnValue({ [pathname]: existingBackupData }); mockedMigrateBackupEntryIfNeeded.mockResolvedValue(existingBackupData); - mockedFetchFolder.mockResolvedValue({ error: new Error('Folder not found') } as any); + mockedFetchFolder.mockResolvedValue({ error: new DriveServerError('NOT_FOUND') }); mockedCreateBackup.mockResolvedValue(mockNewBackupInfo); const result = await enableExistingBackup(pathname, mockDevice); @@ -76,7 +78,7 @@ describe('enableExistingBackup', () => { .mockReturnValueOnce(updatedBackupList); mockedMigrateBackupEntryIfNeeded.mockResolvedValue(migratedBackup); - mockedFetchFolder.mockResolvedValue({ data: { id: migratedBackup.folderId } } as any); + mockedFetchFolder.mockResolvedValue({ data: { id: migratedBackup.folderId } as GetFolderContentDto }); mockedApp.getPath.mockReturnValue('/tmp'); const result = await enableExistingBackup(pathname, mockDevice); diff --git a/src/apps/main/database/collections/DriveFileCollection.ts b/src/apps/main/database/collections/DriveFileCollection.ts index a1fd5085f7..d988bb770a 100644 --- a/src/apps/main/database/collections/DriveFileCollection.ts +++ b/src/apps/main/database/collections/DriveFileCollection.ts @@ -26,7 +26,7 @@ export class DriveFilesCollection implements DatabaseCollectionAdapter void; + abortController?: AbortController; + }, +): Promise { + if (!device.id) { + throw new Error('This backup has not been uploaded yet'); + } + + const user = getUser(); + if (!user) { + throw new Error('No saved user'); + } + + const { data: folder, error } = await fetchFolder(device.uuid); + if (error) { + throw new Error('Unsuccesful request to fetch folder'); + } + if (!folder || !folder.uuid || folder.uuid.length === 0) { + throw new Error('No backup data found'); + } + + const networkApiUrl = process.env.BRIDGE_URL; + const bridgeUser = user.bridgeUser; + const bridgePass = user.userId; + const { mnemonic } = getCredentials(); + + await downloadFolderAsZip( + device.name, + networkApiUrl!, + folder.uuid, + path, + { + bridgeUser, + bridgePass, + encryptionKey: mnemonic, + }, + { + abortController, + updateProgress, + }, + ); +} + export async function downloadBackup(device: Device): Promise { const chosenItem = await getPathFromDialog(); if (!chosenItem || !chosenItem.path) { @@ -149,56 +199,6 @@ export async function downloadBackup(device: Device): Promise { removeListenerIpc.removeListener(listenerName, abortListener); } -async function downloadDeviceBackupZip( - device: Device, - path: PathLike, - { - updateProgress, - abortController, - }: { - updateProgress: (progress: number) => void; - abortController?: AbortController; - }, -): Promise { - if (!device.id) { - throw new Error('This backup has not been uploaded yet'); - } - - const user = getUser(); - if (!user) { - throw new Error('No saved user'); - } - - const { data: folder, error } = await fetchFolder(device.uuid); - if (error) { - throw new Error('Unsuccesful request to fetch folder'); - } - if (!folder || !folder.uuid || folder.uuid.length === 0) { - throw new Error('No backup data found'); - } - - const networkApiUrl = process.env.BRIDGE_URL; - const bridgeUser = user.bridgeUser; - const bridgePass = user.userId; - const { mnemonic } = getCredentials(); - - await downloadFolderAsZip( - device.name, - networkApiUrl!, - folder.uuid, - path, - { - bridgeUser, - bridgePass, - encryptionKey: mnemonic, - }, - { - abortController, - updateProgress, - }, - ); -} - export async function deleteBackup(backup: BackupInfo, isCurrent?: boolean): Promise { const { error } = await addFolderToTrash(backup.folderUuid); if (error) { @@ -221,14 +221,19 @@ export async function deleteBackupsFromDevice(device: Device, isCurrent?: boolea logger.debug({ tag: 'BACKUPS', msg: '[BACKUPS] Deleting backups from device', count: backups.length }); logger.debug({ tag: 'BACKUPS', msg: '[BACKUPS] Backups details', backups }); - let deletionPromises: Promise[] = backups.map((backup) => deleteBackup(backup, isCurrent)); - await Promise.all(deletionPromises); + await Promise.all(backups.map((backup) => deleteBackup(backup, isCurrent))); // delete backups that are not in the backup list const { tree } = await fetchFolderTree(device.uuid); const foldersToDelete = tree.children.filter((folder) => !backups.some((backup) => backup.folderId === folder.id)); - deletionPromises = foldersToDelete.map((folder) => addFolderToTrash(folder.uuid)); - await Promise.all(deletionPromises); + await Promise.all(foldersToDelete.map((folder) => addFolderToTrash(folder.uuid))); +} + +export function findBackupPathnameFromId(id: number): string | undefined { + const backupsList = configStore.get('backupList'); + const entryfound = Object.entries(backupsList).find(([, b]) => b.folderId === id); + + return entryfound?.[0]; } export async function disableBackup(backup: BackupInfo): Promise { @@ -295,13 +300,6 @@ export async function changeBackupPath(currentPath: string): Promise { return false; } -export function findBackupPathnameFromId(id: number): string | undefined { - const backupsList = configStore.get('backupList'); - const entryfound = Object.entries(backupsList).find(([, b]) => b.folderId === id); - - return entryfound?.[0]; -} - export async function createBackupsFromLocalPaths(folderPaths: string[]) { configStore.set('backupsEnabled', true); diff --git a/src/apps/main/event-bus.ts b/src/apps/main/event-bus.ts index d2d0fa05b4..8f5bced9c6 100644 --- a/src/apps/main/event-bus.ts +++ b/src/apps/main/event-bus.ts @@ -2,8 +2,6 @@ import { EventEmitter } from 'events'; import { ProgressData } from './antivirus/types'; import { UserAvailableProducts } from '@internxt/drive-desktop-core/build/backend'; -class EventBus extends EventEmitter {} - interface Events { APP_IS_READY: () => void; @@ -37,10 +35,14 @@ interface Events { USER_AVAILABLE_PRODUCTS_UPDATED: (products: UserAvailableProducts) => void; } -declare interface EventBus { - on(event: U, listener: Events[U]): this; +class EventBus extends EventEmitter { + on(event: U, listener: Events[U]): this { + return super.on(event, listener); + } - emit(event: U, ...args: Parameters): boolean; + emit(event: U, ...args: Parameters): boolean { + return super.emit(event, ...args); + } } const eventBus = new EventBus(); diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index 9b59155dea..f8625feefc 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -129,6 +129,8 @@ export interface IElectronAPI { }; chooseSyncRootWithDialog(): Promise; getBackupErrorByFolder(folderId: number): Promise; + changeBackupPath: typeof import('./device/service').changeBackupPath; + startBackupsProcess(): void; getLastBackupHadIssues(): Promise; onBackupFatalErrorsChanged(fn: (backupErrors: Array) => void): () => void; getBackupFatalErrors(): Promise>; @@ -138,6 +140,10 @@ export interface IElectronAPI { onUpdateAvailable(callback: (info: { version: string }) => void): () => void; getRemoteSyncStatus(): Promise; onRemoteSyncStatusChange(callback: (status: import('./remote-sync/helpers').RemoteSyncStatus) => void): () => void; + getVirtualDriveStatus(): Promise; + onVirtualDriveStatusChange( + callback: (event: { status: import('../drive/fuse/FuseDriveStatus').FuseDriveStatus }) => void, + ): () => void; pathChanged(path: string): void; isUserLoggedIn(): Promise; diff --git a/src/apps/main/nautilus-extension/service.ts b/src/apps/main/nautilus-extension/service.ts index 483ccf3d32..335f8e6d7a 100644 --- a/src/apps/main/nautilus-extension/service.ts +++ b/src/apps/main/nautilus-extension/service.ts @@ -15,13 +15,7 @@ function extensionFile() { if (process.env.NODE_ENV === 'development') { return path.join(__dirname, '../../../../assets/python-nautilus', name); } else { - return path.join( - //@ts-ignore - process.resourcesPath, - 'assets', - 'python-nautilus', - name, - ); + return path.join(process.resourcesPath, 'assets', 'python-nautilus', name); } } diff --git a/src/apps/main/network/NetworkFacade.ts b/src/apps/main/network/NetworkFacade.ts index 9923790202..3f96d71a7a 100644 --- a/src/apps/main/network/NetworkFacade.ts +++ b/src/apps/main/network/NetworkFacade.ts @@ -1,5 +1,6 @@ import { Environment } from '@internxt/inxt-js'; import { Network as NetworkModule } from '@internxt/sdk'; +import { BinaryData } from '@internxt/sdk/dist/network/types'; import { createDecipheriv, randomBytes } from 'crypto'; import { validateMnemonic } from 'bip39'; import { downloadFile } from '@internxt/sdk/dist/network/download'; @@ -15,6 +16,27 @@ interface DownloadOptions { downloadingCallback?: DownloadProgressCallback; } +export function convertToReadableStream(readStream: Readable): ReadableStream { + return new ReadableStream({ + start(controller) { + readStream.on('data', (chunk) => { + controller.enqueue(new Uint8Array(chunk)); + }); + + readStream.on('end', () => { + controller.close(); + }); + + readStream.on('error', (err) => { + controller.error(err); + }); + }, + cancel() { + readStream.destroy(); + }, + }); +} + /** * The entry point for interacting with the network */ @@ -67,9 +89,12 @@ export class NetworkFacade { } }, async (_, key, iv, fileSize) => { + const toUint8Array = (data: BinaryData | Buffer): Uint8Array => + Uint8Array.from(Buffer.isBuffer(data) ? data : Buffer.from(data.toString('hex'), 'hex')); + const cipherKey = options?.key ?? key; const decryptedStream = getDecryptedStream( encryptedContentStreams, - createDecipheriv('aes-256-ctr', options?.key || (key as Buffer), iv as Buffer), + createDecipheriv('aes-256-ctr', toUint8Array(cipherKey), toUint8Array(iv)), ); fileStream = buildProgressStream(decryptedStream, (readBytes) => { @@ -83,28 +108,3 @@ export class NetworkFacade { return fileStream!; } } - -export function convertToReadableStream(readStream: Readable): ReadableStream { - return new ReadableStream({ - start(controller) { - readStream.on('data', (chunk) => { - // Convertir el chunk a Uint8Array y pasarlo al controller - controller.enqueue(new Uint8Array(chunk)); - }); - - readStream.on('end', () => { - // Señalar que la transmisión ha finalizado - controller.close(); - }); - - readStream.on('error', (err) => { - // Señalar un error al controller - controller.error(err); - }); - }, - cancel() { - // Abortar la lectura del ReadStream de fs - readStream.destroy(); - }, - }); -} diff --git a/src/apps/main/network/download.ts b/src/apps/main/network/download.ts index 77939fc094..c1fc67f468 100644 --- a/src/apps/main/network/download.ts +++ b/src/apps/main/network/download.ts @@ -20,85 +20,25 @@ import { Readable } from 'node:stream'; import fetch from 'electron-fetch'; import { convertToReadableStream } from './NetworkFacade'; -export async function downloadFolderAsZip( - deviceName: string, - networkApiUrl: string, - folderUuid: string, - fullPath: PathLike, - environment: { - bridgeUser: string; - bridgePass: string; - encryptionKey: string; - }, - opts: { - abortController?: AbortController; - updateProgress?: (progress: number) => void; - }, -) { - const writeStream = fs.createWriteStream(fullPath); - const destination = convertToWritableStream(writeStream); - - const { abortController, updateProgress } = opts; - const { bridgeUser, bridgePass, encryptionKey } = environment; - const { tree, folderDecryptedNames, fileDecryptedNames, size } = await fetchFolderTree(folderUuid); - tree.plainName = deviceName; - folderDecryptedNames[tree.id] = deviceName; - const pendingFolders: { path: string; data: FolderTree }[] = [{ path: '', data: tree }]; - - const zip = new FlatFolderZip(destination, { - abortController: opts.abortController, - // possible zip corruption caused by progress ?? - progress: (loadedBytes) => updateProgress?.(loadedBytes / size), - }); - - while (pendingFolders.length > 0 && !abortController?.signal.aborted) { - const currentFolder = pendingFolders.shift() as { - path: string; - data: FolderTree; - }; - const folderPath = - currentFolder.path + (currentFolder.path === '' ? '' : '/') + folderDecryptedNames[currentFolder.data.id]; - - zip.addFolder(folderPath); - - const { files, children: folders } = currentFolder.data; - - for (const file of files) { - if (abortController?.signal.aborted) { - throw new Error('Download cancelled'); - } - - const displayFilename = items.getItemDisplayName({ - name: fileDecryptedNames[file.id], - type: file.type, - }); - - const fileStreamPromise = downloadFile({ - networkApiUrl, - bucketId: file.bucket, - fileId: file.fileId, - creds: { - pass: bridgePass, - user: bridgeUser, - }, - mnemonic: encryptionKey, - options: { - notifyProgress: () => null, - abortController: opts.abortController, - }, - }); - - zip.addFile(folderPath + '/' + displayFilename, await fileStreamPromise); - } - - pendingFolders.push(...folders.map((tree) => ({ path: folderPath, data: tree }))); - } +interface MetadataRequiredForDownload { + mirrors: Mirror[]; + fileMeta: FileInfo; +} - if (abortController?.signal.aborted) { - throw new Error('Download cancelled'); - } +export type DownloadProgressCallback = (totalBytes: number, downloadedBytes: number) => void; - return zip.close(); +export interface IDownloadParams { + networkApiUrl: string; + bucketId: string; + fileId: string; + creds?: NetworkCredentials; + mnemonic?: string; + encryptionKey?: Buffer; + token?: string; + options?: { + notifyProgress: DownloadProgressCallback; + abortController?: AbortController; + }; } function convertToWritableStream(writeStream: fs.WriteStream): WritableStream { @@ -132,99 +72,74 @@ function convertToWritableStream(writeStream: fs.WriteStream): WritableStream void; -export interface IDownloadParams { - networkApiUrl: string; - bucketId: string; - fileId: string; - creds?: NetworkCredentials; - mnemonic?: string; - encryptionKey?: Buffer; - token?: string; - options?: { - notifyProgress: DownloadProgressCallback; - abortController?: AbortController; - }; -} +function joinReadableBinaryStreams(streams: ReadableStream[]): ReadableStream { + const streamsCopy = streams.map((s) => s); + let keepReading = true; -interface MetadataRequiredForDownload { - mirrors: Mirror[]; - fileMeta: FileInfo; -} + const flush = () => streamsCopy.forEach((s) => s.cancel()); -async function getRequiredFileMetadataWithToken( - networkApiUrl: string, - bucketId: string, - fileId: string, - token: string, -): Promise { - const fileMeta: FileInfo = await getFileInfoWithToken(networkApiUrl, bucketId, fileId, token); - const mirrors: Mirror[] = await getMirrors(networkApiUrl, bucketId, fileId, null, token); + const stream = new ReadableStream({ + async pull(controller) { + if (!keepReading) return flush(); - return { fileMeta, mirrors }; -} + const downStream = streamsCopy.shift(); -async function getRequiredFileMetadataWithAuth( - networkApiUrl: string, - bucketId: string, - fileId: string, - creds: NetworkCredentials, -): Promise { - const fileMeta: FileInfo = await getFileInfoWithAuth(networkApiUrl, bucketId, fileId, creds); - const mirrors: Mirror[] = await getMirrors(networkApiUrl, bucketId, fileId, creds); + if (!downStream) { + return controller.close(); + } - return { fileMeta, mirrors }; -} + const reader = downStream.getReader(); + let done = false; -async function downloadFile(params: IDownloadParams): Promise> { - const downloadFileV2Promise = downloadFileV2(params as any); + while (!done && keepReading) { + const status = await reader.read(); - return downloadFileV2Promise.catch((err: Error) => { - if (err instanceof FileVersionOneError) { - return _downloadFile(params); - } + if (!status.done) { + controller.enqueue(status.value); + } - throw err; - }); -} + done = status.done; + } -async function _downloadFile(params: IDownloadParams): Promise> { - const { networkApiUrl, bucketId, fileId, token, creds } = params; + reader.releaseLock(); + }, + cancel() { + keepReading = false; + }, + }); - let metadata: MetadataRequiredForDownload; + return stream; +} - if (creds) { - metadata = await getRequiredFileMetadataWithAuth(networkApiUrl, bucketId, fileId, creds); - } else if (token) { - metadata = await getRequiredFileMetadataWithToken(networkApiUrl, bucketId, fileId, token); - } else { - throw new Error('Download error 1'); - } +export function getDecryptedStream( + encryptedContentSlices: ReadableStream[], + decipher: Decipher, +): ReadableStream { + const encryptedStream = joinReadableBinaryStreams(encryptedContentSlices); - const { mirrors, fileMeta } = metadata; - const downloadUrls: string[] = mirrors.map((m) => m.url); + let keepReading = true; - const index = Buffer.from(fileMeta.index, 'hex'); - const iv = index.slice(0, 16); - let key: Buffer; + const decryptedStream = new ReadableStream({ + async pull(controller) { + if (!keepReading) return; - if (params.encryptionKey) { - key = params.encryptionKey; - } else if (params.mnemonic) { - key = await GenerateFileKey(params.mnemonic, bucketId, index); - } else { - throw new Error('Download error code 1'); - } + const reader = encryptedStream.getReader(); + const status = await reader.read(); - const downloadStream = await getFileDownloadStream( - downloadUrls, - createDecipheriv('aes-256-ctr', key, iv), - params.options?.abortController, - ); + if (status.done) { + controller.close(); + } else { + controller.enqueue(decipher.update(status.value)); + } - return buildProgressStream(downloadStream, (readBytes) => { - params.options?.notifyProgress(metadata.fileMeta.size, readBytes); + reader.releaseLock(); + }, + cancel() { + keepReading = false; + }, }); + + return decryptedStream; } async function getFileDownloadStream( @@ -250,6 +165,30 @@ async function getFileDownloadStream( return getDecryptedStream(encryptedContentParts, decipher); } +async function getRequiredFileMetadataWithAuth( + networkApiUrl: string, + bucketId: string, + fileId: string, + creds: NetworkCredentials, +): Promise { + const fileMeta: FileInfo = await getFileInfoWithAuth(networkApiUrl, bucketId, fileId, creds); + const mirrors: Mirror[] = await getMirrors(networkApiUrl, bucketId, fileId, creds); + + return { fileMeta, mirrors }; +} + +async function getRequiredFileMetadataWithToken( + networkApiUrl: string, + bucketId: string, + fileId: string, + token: string, +): Promise { + const fileMeta: FileInfo = await getFileInfoWithToken(networkApiUrl, bucketId, fileId, token); + const mirrors: Mirror[] = await getMirrors(networkApiUrl, bucketId, fileId, null, token); + + return { fileMeta, mirrors }; +} + export function buildProgressStream( source: ReadableStream, onRead: (readBytes: number) => void, @@ -276,72 +215,131 @@ export function buildProgressStream( }); } -function joinReadableBinaryStreams(streams: ReadableStream[]): ReadableStream { - const streamsCopy = streams.map((s) => s); - let keepReading = true; - - const flush = () => streamsCopy.forEach((s) => s.cancel()); - - const stream = new ReadableStream({ - async pull(controller) { - if (!keepReading) return flush(); +async function _downloadFile(params: IDownloadParams): Promise> { + const { networkApiUrl, bucketId, fileId, token, creds } = params; - const downStream = streamsCopy.shift(); + let metadata: MetadataRequiredForDownload; - if (!downStream) { - return controller.close(); - } + if (creds) { + metadata = await getRequiredFileMetadataWithAuth(networkApiUrl, bucketId, fileId, creds); + } else if (token) { + metadata = await getRequiredFileMetadataWithToken(networkApiUrl, bucketId, fileId, token); + } else { + throw new Error('Download error 1'); + } - const reader = downStream.getReader(); - let done = false; + const { mirrors, fileMeta } = metadata; + const downloadUrls: string[] = mirrors.map((m) => m.url); - while (!done && keepReading) { - const status = await reader.read(); + const index = Buffer.from(fileMeta.index, 'hex'); + const iv = index.slice(0, 16); + let key: Buffer; - if (!status.done) { - controller.enqueue(status.value); - } + if (params.encryptionKey) { + key = params.encryptionKey; + } else if (params.mnemonic) { + key = await GenerateFileKey(params.mnemonic, bucketId, index); + } else { + throw new Error('Download error code 1'); + } - done = status.done; - } + const downloadStream = await getFileDownloadStream( + downloadUrls, + createDecipheriv('aes-256-ctr', Uint8Array.from(key), Uint8Array.from(iv)), + params.options?.abortController, + ); - reader.releaseLock(); - }, - cancel() { - keepReading = false; - }, + return buildProgressStream(downloadStream, (readBytes) => { + params.options?.notifyProgress(metadata.fileMeta.size, readBytes); }); +} - return stream; +async function downloadFile(params: IDownloadParams): Promise> { + return downloadFileV2(params).catch((err: Error) => { + if (err instanceof FileVersionOneError) { + return _downloadFile(params); + } + throw err; + }); } -export function getDecryptedStream( - encryptedContentSlices: ReadableStream[], - decipher: Decipher, -): ReadableStream { - const encryptedStream = joinReadableBinaryStreams(encryptedContentSlices); +export async function downloadFolderAsZip( + deviceName: string, + networkApiUrl: string, + folderUuid: string, + fullPath: PathLike, + environment: { + bridgeUser: string; + bridgePass: string; + encryptionKey: string; + }, + opts: { + abortController?: AbortController; + updateProgress?: (progress: number) => void; + }, +) { + const writeStream = fs.createWriteStream(fullPath); + const destination = convertToWritableStream(writeStream); - let keepReading = true; + const { abortController, updateProgress } = opts; + const { bridgeUser, bridgePass, encryptionKey } = environment; + const { tree, folderDecryptedNames, fileDecryptedNames, size } = await fetchFolderTree(folderUuid); + tree.plainName = deviceName; + folderDecryptedNames[tree.id] = deviceName; + const pendingFolders: { path: string; data: FolderTree }[] = [{ path: '', data: tree }]; - const decryptedStream = new ReadableStream({ - async pull(controller) { - if (!keepReading) return; + const zip = new FlatFolderZip(destination, { + abortController: opts.abortController, + // possible zip corruption caused by progress ?? + progress: (loadedBytes) => updateProgress?.(loadedBytes / size), + }); - const reader = encryptedStream.getReader(); - const status = await reader.read(); + while (pendingFolders.length > 0 && !abortController?.signal.aborted) { + const currentFolder = pendingFolders.shift() as { + path: string; + data: FolderTree; + }; + const folderPath = + currentFolder.path + (currentFolder.path === '' ? '' : '/') + folderDecryptedNames[currentFolder.data.id]; - if (status.done) { - controller.close(); - } else { - controller.enqueue(decipher.update(status.value)); + zip.addFolder(folderPath); + + const { files, children: folders } = currentFolder.data; + + for (const file of files) { + if (abortController?.signal.aborted) { + throw new Error('Download cancelled'); } - reader.releaseLock(); - }, - cancel() { - keepReading = false; - }, - }); + const displayFilename = items.getItemDisplayName({ + name: fileDecryptedNames[file.id], + type: file.type, + }); - return decryptedStream; + const fileStreamPromise = downloadFile({ + networkApiUrl, + bucketId: file.bucket, + fileId: file.fileId, + creds: { + pass: bridgePass, + user: bridgeUser, + }, + mnemonic: encryptionKey, + options: { + notifyProgress: () => null, + abortController: opts.abortController, + }, + }); + + zip.addFile(folderPath + '/' + displayFilename, await fileStreamPromise); + } + + pendingFolders.push(...folders.map((tree) => ({ path: folderPath, data: tree }))); + } + + if (abortController?.signal.aborted) { + throw new Error('Download cancelled'); + } + + return zip.close(); } diff --git a/src/apps/main/network/downloadv2.ts b/src/apps/main/network/downloadv2.ts index b928ccdd28..4b4ac452b0 100644 --- a/src/apps/main/network/downloadv2.ts +++ b/src/apps/main/network/downloadv2.ts @@ -30,12 +30,21 @@ interface DownloadSharedFileParams extends DownloadFileParams { creds?: never; mnemonic?: never; token: string; - encryptionKey: string; + encryptionKey: Buffer | string; } type DownloadSharedFileFunction = (params: DownloadSharedFileParams) => DownloadFileResponse; type DownloadOwnFileFunction = (params: DownloadOwnFileParams) => DownloadFileResponse; -type DownloadFileFunction = (params: DownloadSharedFileParams | DownloadOwnFileParams) => DownloadFileResponse; +type DownloadFileRawParams = { + bucketId: string; + fileId: string; + creds?: NetworkCredentials; + mnemonic?: string; + token?: string; + encryptionKey?: Buffer | string; + options?: DownloadFileOptions; +}; +type DownloadFileFunction = (params: DownloadFileRawParams) => DownloadFileResponse; const downloadSharedFile: DownloadSharedFileFunction = (params) => { const { bucketId, fileId, encryptionKey, token, options } = params; @@ -55,7 +64,7 @@ const downloadSharedFile: DownloadSharedFileFunction = (params) => { }, ), ).download(bucketId, fileId, '', { - key: Buffer.from(encryptionKey, 'hex'), + key: typeof encryptionKey === 'string' ? Buffer.from(encryptionKey, 'hex') : encryptionKey, token, downloadingCallback: options?.notifyProgress, abortController: options?.abortController, @@ -95,9 +104,9 @@ const downloadOwnFile: DownloadOwnFileFunction = (params) => { const downloadFileV2: DownloadFileFunction = (params) => { if (params.token && params.encryptionKey) { - return downloadSharedFile(params); + return downloadSharedFile(params as DownloadSharedFileParams); } else if (params.creds && params.mnemonic) { - return downloadOwnFile(params); + return downloadOwnFile(params as DownloadOwnFileParams); } else { throw new Error('DOWNLOAD ERRNO. 0'); } diff --git a/src/apps/main/network/requests.ts b/src/apps/main/network/requests.ts index 1adc232cda..69998791f7 100644 --- a/src/apps/main/network/requests.ts +++ b/src/apps/main/network/requests.ts @@ -1,6 +1,12 @@ import axios, { AxiosBasicCredentials, AxiosRequestConfig } from 'axios'; import { createHash } from 'crypto'; +/** + * v.2.5.5 + * Esteban Galvis Triana + * TODO: Move this request logic to driveServerClient + */ + export interface FileInfo { bucket: string; mimetype: string; @@ -99,6 +105,61 @@ export interface Mirror { operation: string; } +function isFarmerOk(farmer?: Partial) { + return farmer && farmer.nodeID && farmer.port && farmer.address; +} + +function getFileMirrors( + networkApiUrl: string, + bucketId: string, + fileId: string, + limit: number | 3, + skip: number | 0, + excludeNodes: string[] = [], + opts?: AxiosRequestConfig, +): Promise { + const excludeNodeIds: string = excludeNodes.join(','); + const path = `${networkApiUrl}/buckets/${bucketId}/files/${fileId}`; + const queryParams = `?limit=${limit}&skip=${skip}&exclude=${excludeNodeIds}`; + + const defaultOpts: AxiosRequestConfig = { + responseType: 'json', + url: path + queryParams, + }; + + return axios + .request({ ...defaultOpts, ...opts }) + .then((res) => { + return res.data; + }) + .catch((err) => { + throw err; + }); +} + +async function replaceMirror( + networkApiUrl: string, + bucketId: string, + fileId: string, + pointerIndex: number, + excludeNodes: string[] = [], + opts?: AxiosRequestConfig, +): Promise { + let mirrorIsOk = false; + let mirror: Mirror; + + while (!mirrorIsOk) { + const [newMirror] = await getFileMirrors(networkApiUrl, bucketId, fileId, 1, pointerIndex, excludeNodes, opts); + + mirror = newMirror; + mirrorIsOk = + newMirror.farmer && newMirror.farmer.nodeID && newMirror.farmer.port && newMirror.farmer.address ? true : false; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return mirror!; +} + export async function getMirrors( networkApiUrl: string, bucketId: string, @@ -145,58 +206,3 @@ export async function getMirrors( return mirrors; } - -async function replaceMirror( - networkApiUrl: string, - bucketId: string, - fileId: string, - pointerIndex: number, - excludeNodes: string[] = [], - opts?: AxiosRequestConfig, -): Promise { - let mirrorIsOk = false; - let mirror: Mirror; - - while (!mirrorIsOk) { - const [newMirror] = await getFileMirrors(networkApiUrl, bucketId, fileId, 1, pointerIndex, excludeNodes, opts); - - mirror = newMirror; - mirrorIsOk = - newMirror.farmer && newMirror.farmer.nodeID && newMirror.farmer.port && newMirror.farmer.address ? true : false; - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return mirror!; -} - -function getFileMirrors( - networkApiUrl: string, - bucketId: string, - fileId: string, - limit: number | 3, - skip: number | 0, - excludeNodes: string[] = [], - opts?: AxiosRequestConfig, -): Promise { - const excludeNodeIds: string = excludeNodes.join(','); - const path = `${networkApiUrl}/buckets/${bucketId}/files/${fileId}`; - const queryParams = `?limit=${limit}&skip=${skip}&exclude=${excludeNodeIds}`; - - const defaultOpts: AxiosRequestConfig = { - responseType: 'json', - url: path + queryParams, - }; - - return axios - .request({ ...defaultOpts, ...opts }) - .then((res) => { - return res.data; - }) - .catch((err) => { - throw err; - }); -} - -function isFarmerOk(farmer?: Partial) { - return farmer && farmer.nodeID && farmer.port && farmer.address; -} diff --git a/src/apps/main/network/zip.service.ts b/src/apps/main/network/zip.service.ts index 4eddb1d51c..c3a484440d 100644 --- a/src/apps/main/network/zip.service.ts +++ b/src/apps/main/network/zip.service.ts @@ -1,5 +1,6 @@ import { AsyncZipDeflate, Zip } from 'fflate'; import { ReadableStream, WritableStream } from 'node:stream/web'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; type FlatFolderZipOpts = { abortController?: AbortController; @@ -17,48 +18,6 @@ export interface ZipStream { end: () => void; } -export class FlatFolderZip { - private finished!: Promise; - private zip: ZipStream; - private passThrough: ReadableStream; - private abortController?: AbortController; - - constructor(destination: WritableStream, opts: FlatFolderZipOpts) { - this.zip = createFolderWithFilesWritable(opts.progress); - this.abortController = opts.abortController; - - this.passThrough = this.zip.stream; - - this.finished = this.passThrough.pipeTo(destination, { - signal: opts.abortController?.signal, - }); - } - - addFile(name: string, source: ReadableStream): void { - if (this.abortController?.signal.aborted) return; - - this.zip.addFile(name, source); - } - - addFolder(name: string): void { - if (this.abortController?.signal.aborted) return; - - this.zip.addFolder(name); - } - - async close(): Promise { - if (this.abortController?.signal.aborted) return; - - this.zip.end(); - - await this.finished; - } - - abort(): void { - this.abortController?.abort(); - } -} - export function createFolderWithFilesWritable(progress?: FlatFolderZipOpts['progress']): ZipStream { const zip = new Zip(); let passthroughController: ReadableStreamDefaultController | null = null; @@ -81,7 +40,7 @@ export function createFolderWithFilesWritable(progress?: FlatFolderZipOpts['prog zip.ondata = (err, data, final) => { if (err) { - console.error('Error in ZIP data event:', err); + logger.error({ msg: 'Error in ZIP data event', err }); return; } @@ -134,3 +93,45 @@ export function createFolderWithFilesWritable(progress?: FlatFolderZipOpts['prog }, }; } + +export class FlatFolderZip { + private finished!: Promise; + private zip: ZipStream; + private passThrough: ReadableStream; + private abortController?: AbortController; + + constructor(destination: WritableStream, opts: FlatFolderZipOpts) { + this.zip = createFolderWithFilesWritable(opts.progress); + this.abortController = opts.abortController; + + this.passThrough = this.zip.stream; + + this.finished = this.passThrough.pipeTo(destination, { + signal: opts.abortController?.signal, + }); + } + + addFile(name: string, source: ReadableStream): void { + if (this.abortController?.signal.aborted) return; + + this.zip.addFile(name, source); + } + + addFolder(name: string): void { + if (this.abortController?.signal.aborted) return; + + this.zip.addFolder(name); + } + + async close(): Promise { + if (this.abortController?.signal.aborted) return; + + this.zip.end(); + + await this.finished; + } + + abort(): void { + this.abortController?.abort(); + } +} diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index 11a06ee726..cb03df7b82 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -5,7 +5,9 @@ import { BackupErrorRecord } from '../../backend/features/backup/backup.types'; declare interface Window { electron: { - getConfigKey(key: import('./config/service.types').StoredValues): Promise; + getConfigKey( + key: T, + ): Promise; listenToConfigKeyChange(key: import('./config/service.types').StoredValues, fn: (value: T) => void): () => void; diff --git a/src/apps/main/realtime.ts b/src/apps/main/realtime.ts index 6475c9d676..f88985bd28 100644 --- a/src/apps/main/realtime.ts +++ b/src/apps/main/realtime.ts @@ -22,6 +22,13 @@ export type EventPayload = { let user = getUser(); +function stopRemoteNotifications() { + if (socket) { + socket.close(); + socket = undefined; + } +} + function cleanAndStartRemoteNotifications() { stopRemoteNotifications(); const { newToken } = getCredentials(); @@ -124,12 +131,5 @@ function cleanAndStartRemoteNotifications() { }); } -function stopRemoteNotifications() { - if (socket) { - socket.close(); - socket = undefined; - } -} - eventBus.on('USER_LOGGED_IN', cleanAndStartRemoteNotifications); eventBus.on('USER_LOGGED_OUT', stopRemoteNotifications); diff --git a/src/apps/main/remote-sync/RemoteSyncManager.ts b/src/apps/main/remote-sync/RemoteSyncManager.ts index ffc1e2f884..a07a57aa1d 100644 --- a/src/apps/main/remote-sync/RemoteSyncManager.ts +++ b/src/apps/main/remote-sync/RemoteSyncManager.ts @@ -373,35 +373,29 @@ export class RemoteSyncManager { }; } - private patchDriveFolderResponseItem = (payload: any): RemoteSyncedFolder => { - // We will assume that we received an status - let status: RemoteSyncedFolder['status'] = payload.status; - - if (!status && !payload.removed) { - status = 'EXISTS'; - } - - if (!status && payload.removed) { - status = 'REMOVED'; - } - - if (!status && payload.deleted) { - status = 'DELETED'; - } + private patchDriveFolderResponseItem = (payload: Record): RemoteSyncedFolder => { + const status = this.resolveFolderStatus(payload); return { - ...payload, + ...(payload as Omit), status, - name: payload.name ?? undefined, + name: typeof payload.name === 'string' ? payload.name : undefined, }; }; - private patchDriveFileResponseItem = (payload: any): RemoteSyncedFile => { + private resolveFolderStatus(payload: Record): RemoteSyncedFolder['status'] { + if (typeof payload.status === 'string' && payload.status) return payload.status; + if (payload.removed) return 'REMOVED'; + if (payload.deleted) return 'DELETED'; + return 'EXISTS'; + } + + private readonly patchDriveFileResponseItem = (payload: Record): RemoteSyncedFile => { return { - ...payload, - fileId: payload.fileId ?? '', - size: typeof payload.size === 'string' ? parseInt(payload.size) : payload.size, - name: payload.name ?? undefined, + ...(payload as Omit), + fileId: typeof payload.fileId === 'string' ? payload.fileId : '', + size: typeof payload.size === 'string' ? Number.parseInt(payload.size) : (payload.size as number), + name: typeof payload.name === 'string' ? payload.name : undefined, }; }; } diff --git a/src/apps/main/remote-sync/errors.ts b/src/apps/main/remote-sync/errors.ts index f02bc7cb96..e6ac4bd04e 100644 --- a/src/apps/main/remote-sync/errors.ts +++ b/src/apps/main/remote-sync/errors.ts @@ -2,10 +2,10 @@ * Base class for RemoteSync errors. */ export class RemoteSyncError extends Error { - public context?: any; + public context?: Record; public code?: string; - constructor(message: string, code?: string, context?: any) { + constructor(message: string, code?: string, context?: Record) { super(message); this.name = 'RemoteSyncError'; this.code = code; @@ -17,7 +17,7 @@ export class RemoteSyncError extends Error { * Error thrown when the response does not contain an array of files. */ export class RemoteSyncInvalidResponseError extends RemoteSyncError { - constructor(response: any) { + constructor(response: unknown) { super(`Expected an array of files, but received: ${JSON.stringify(response, null, 2)}`, 'INVALID_RESPONSE', { response, }); @@ -39,7 +39,7 @@ export class RemoteSyncNetworkError extends RemoteSyncError { * Error thrown when the server responds with an error status (example, status 500). */ export class RemoteSyncServerError extends RemoteSyncError { - constructor(status: number, data: any) { + constructor(status: number, data: unknown) { super(`Server error: request failed with status code ${status} while sync`, 'SERVER_ERROR', { status, data }); this.name = 'RemoteSyncServerError'; } diff --git a/src/apps/main/virtual-root-folder/service.ts b/src/apps/main/virtual-root-folder/service.ts index e90086038a..4f31ed8242 100644 --- a/src/apps/main/virtual-root-folder/service.ts +++ b/src/apps/main/virtual-root-folder/service.ts @@ -9,16 +9,6 @@ import { PATHS } from '../../../core/electron/paths'; const VIRTUAL_DRIVE_FOLDER = PATHS.ROOT_DRIVE_FOLDER; -async function existsFolder(pathname: string): Promise { - try { - await fs.access(pathname); - - return true; - } catch { - return false; - } -} - export async function clearDirectory(pathname: string): Promise { try { await fs.rm(pathname, { recursive: true }); @@ -30,13 +20,7 @@ export async function clearDirectory(pathname: string): Promise { } } -async function isEmptyFolder(pathname: string): Promise { - const filesInFolder = await fs.readdir(pathname); - - return filesInFolder.length === 0; -} - -function setSyncRoot(pathname: string): void { +export function setupRootFolder(pathname: string): void { const pathNameWithSepInTheEnd = pathname[pathname.length - 1] === path.sep ? pathname : pathname + path.sep; configStore.set('syncRoot', pathNameWithSepInTheEnd); configStore.set('lastSavedListing', ''); @@ -47,23 +31,18 @@ export function getRootVirtualDrive(): string { ensureFolderExists(current); if (current !== VIRTUAL_DRIVE_FOLDER) { - setupRootFolder(); + setupRootFolder(VIRTUAL_DRIVE_FOLDER); } return configStore.get('syncRoot'); } -export async function setupRootFolder(n = 0): Promise { - setSyncRoot(VIRTUAL_DRIVE_FOLDER); - return; -} - export async function chooseSyncRootWithDialog(): Promise { const result = await dialog.showOpenDialog({ properties: ['openDirectory'] }); if (!result.canceled) { const chosenPath = result.filePaths[0]; - setSyncRoot(chosenPath); + setupRootFolder(chosenPath); eventBus.emit('SYNC_ROOT_CHANGED', chosenPath); return chosenPath; diff --git a/src/apps/main/windows/index.ts b/src/apps/main/windows/index.ts index 7b507e92d7..0e98a75000 100644 --- a/src/apps/main/windows/index.ts +++ b/src/apps/main/windows/index.ts @@ -16,7 +16,7 @@ function closeAuxWindows() { eventBus.on('USER_LOGGED_OUT', closeAuxWindows); -export function broadcastToWindows(eventName: string, data: any) { +export function broadcastToWindows(eventName: string, data: unknown) { const renderers = [getWidget(), getProcessIssuesWindow(), getSettingsWindow(), getOnboardingWindow()]; renderers.forEach((r) => r?.webContents.send(eventName, data)); diff --git a/src/apps/main/windows/onboarding.ts b/src/apps/main/windows/onboarding.ts index dd678b0b03..5708dd77c8 100644 --- a/src/apps/main/windows/onboarding.ts +++ b/src/apps/main/windows/onboarding.ts @@ -8,8 +8,6 @@ import isDev from '../../../core/isDev/isDev'; let onboardingWindow: BrowserWindow | null = null; export const getOnboardingWindow = () => (onboardingWindow?.isDestroyed() ? null : onboardingWindow); -ipcMain.on('open-onboarding-window', () => openOnboardingWindow()); - export const openOnboardingWindow = () => { if (onboardingWindow) { onboardingWindow.focus(); @@ -45,3 +43,5 @@ export const openOnboardingWindow = () => { setUpCommonWindowHandlers(onboardingWindow); }; + +ipcMain.on('open-onboarding-window', () => openOnboardingWindow()); diff --git a/src/apps/main/windows/process-issues.ts b/src/apps/main/windows/process-issues.ts index f6262ded9d..3c838679d2 100644 --- a/src/apps/main/windows/process-issues.ts +++ b/src/apps/main/windows/process-issues.ts @@ -7,13 +7,6 @@ import isDev from '../../../core/isDev/isDev'; let processIssuesWindow: BrowserWindow | null = null; export const getProcessIssuesWindow = () => (processIssuesWindow?.isDestroyed() ? null : processIssuesWindow); -ipcMain.on('open-process-issues-window', openProcessIssuesWindow); -ipcMain.handle('open-process-issues-window', async () => { - await openProcessIssuesWindow(); - - return true; -}); - async function openProcessIssuesWindow() { if (processIssuesWindow) { processIssuesWindow.focus(); @@ -48,3 +41,10 @@ async function openProcessIssuesWindow() { setUpCommonWindowHandlers(processIssuesWindow); } + +ipcMain.on('open-process-issues-window', openProcessIssuesWindow); +ipcMain.handle('open-process-issues-window', async () => { + await openProcessIssuesWindow(); + + return true; +}); diff --git a/src/apps/main/windows/settings.ts b/src/apps/main/windows/settings.ts index 36178bf562..ff27a16a76 100644 --- a/src/apps/main/windows/settings.ts +++ b/src/apps/main/windows/settings.ts @@ -9,8 +9,6 @@ import isDev from '../../../core/isDev/isDev'; let settingsWindow: BrowserWindow | null = null; export const getSettingsWindow = () => (settingsWindow?.isDestroyed() ? null : settingsWindow); -ipcMain.on('open-settings-window', (_, section) => openSettingsWindow(section)); - async function openSettingsWindow(section?: string) { if (settingsWindow) { settingsWindow.focus(); @@ -55,6 +53,8 @@ async function openSettingsWindow(section?: string) { setUpCommonWindowHandlers(settingsWindow); } +ipcMain.on('open-settings-window', (_, section) => openSettingsWindow(section)); + ipcMain.on('settings-window-resized', (_, { height }: { width: number; height: number }) => { if (settingsWindow) { // Not truncating the height makes this function throw diff --git a/src/apps/main/windows/widget.ts b/src/apps/main/windows/widget.ts index 5a63e13d20..2c61e03012 100644 --- a/src/apps/main/windows/widget.ts +++ b/src/apps/main/windows/widget.ts @@ -66,7 +66,7 @@ export const createWidget = async () => { widget.webContents.on('ipc-message', (_, channel, payload) => { // Current widget pathname if (channel === 'path-changed') { - console.log('Renderer navigated to ', payload); + reportError(`Renderer navigated to ${payload}`); } }); diff --git a/src/apps/renderer/assets/icons/getIcon.tsx b/src/apps/renderer/assets/icons/getIcon.tsx index ee54ac7a4f..b4d186d609 100644 --- a/src/apps/renderer/assets/icons/getIcon.tsx +++ b/src/apps/renderer/assets/icons/getIcon.tsx @@ -20,12 +20,10 @@ import Zip from './zip.svg'; interface iconLibrary { id: string; - icon: any; + icon: JSX.Element; extensions: string[]; } -// const getSVG = (svg: any) => svg as React.SVGAttributes; - const file_type: iconLibrary[] = [ { id: 'audio', diff --git a/src/apps/renderer/context/CleanerContext.tsx b/src/apps/renderer/context/CleanerContext.tsx index 4bb7475d86..fa79fc02be 100644 --- a/src/apps/renderer/context/CleanerContext.tsx +++ b/src/apps/renderer/context/CleanerContext.tsx @@ -70,7 +70,7 @@ export function CleanerProvider({ children }: { children: ReactNode }) { try { window.electron.cleaner.startCleanup(viewModel); } catch (error) { - console.error('Failed to start cleanup:', error); + reportError(`Failed to start cleanup: ${error}`); } }; @@ -78,7 +78,7 @@ export function CleanerProvider({ children }: { children: ReactNode }) { try { window.electron.cleaner.stopCleanup(); } catch (error) { - console.error('Failed to stop cleanup:', error); + reportError(`Failed to stop cleanup: ${error}`); } }; diff --git a/src/apps/renderer/context/DeviceContext.tsx b/src/apps/renderer/context/DeviceContext.tsx index a354355a4f..8af34f8637 100644 --- a/src/apps/renderer/context/DeviceContext.tsx +++ b/src/apps/renderer/context/DeviceContext.tsx @@ -25,14 +25,15 @@ export function DeviceProvider({ children }: { children: ReactNode }) { const [selected, setSelected] = useState(); const { devices, getDevices } = useDevices(); - useEffect(() => { - refreshDevice(); - - const removeDeviceCreatedListener = window.electron.onDeviceCreated(setCurrentDevice); - return () => { - removeDeviceCreatedListener(); - }; - }, []); + const setCurrentDevice = (newDevice: Device) => { + try { + setDeviceState({ status: 'SUCCESS', device: newDevice }); + setCurrent(newDevice); + setSelected(newDevice); + } catch { + setDeviceState({ status: 'ERROR' }); + } + }; const refreshDevice = () => { setDeviceState({ status: 'LOADING' }); @@ -45,15 +46,14 @@ export function DeviceProvider({ children }: { children: ReactNode }) { }); }; - const setCurrentDevice = (newDevice: Device) => { - try { - setDeviceState({ status: 'SUCCESS', device: newDevice }); - setCurrent(newDevice); - setSelected(newDevice); - } catch { - setDeviceState({ status: 'ERROR' }); - } - }; + useEffect(() => { + refreshDevice(); + + const removeDeviceCreatedListener = window.electron.onDeviceCreated(setCurrentDevice); + return () => { + removeDeviceCreatedListener(); + }; + }, []); const deviceRename = async (deviceName: string) => { setDeviceState({ status: 'LOADING' }); @@ -64,7 +64,7 @@ export function DeviceProvider({ children }: { children: ReactNode }) { setCurrent(updatedDevice); setSelected(updatedDevice); } catch (err) { - console.log(err); + reportError(err); setDeviceState({ status: 'ERROR' }); } }; diff --git a/src/apps/renderer/hooks/antivirus/useAntivirus.tsx b/src/apps/renderer/hooks/antivirus/useAntivirus.tsx index a60ff093ee..352ec1d30e 100644 --- a/src/apps/renderer/hooks/antivirus/useAntivirus.tsx +++ b/src/apps/renderer/hooks/antivirus/useAntivirus.tsx @@ -38,6 +38,41 @@ export const useAntivirus = (): AntivirusContext => { const [showErrorState, setShowErrorState] = useState(false); const [view, setView] = useState('loading'); + const handleProgress = (progress: { + scanId?: string; + currentScanPath?: string; + infectedFiles?: string[]; + progress?: number; + totalScannedFiles?: number; + done?: boolean; + }) => { + if (!progress) return; + + if (progress.currentScanPath) { + setCurrentScanPath(progress.currentScanPath); + } + + if (typeof progress.totalScannedFiles === 'number') { + setCountScannedFiles(progress.totalScannedFiles); + } + + if (typeof progress.progress === 'number') { + setProgressRatio(progress.progress); + } + + if (Array.isArray(progress.infectedFiles) && progress.infectedFiles.length > 0) { + setInfectedFiles(progress.infectedFiles); + } + + if (progress.done) { + setProgressRatio(100); + setTimeout(() => { + setIsScanning(false); + setIsScanCompleted(true); + }, 500); + } + }; + useEffect(() => { window.electron.antivirus.onScanProgress(handleProgress); return () => { @@ -103,41 +138,6 @@ export const useAntivirus = (): AntivirusContext => { } }; - const handleProgress = (progress: { - scanId?: string; - currentScanPath?: string; - infectedFiles?: string[]; - progress?: number; - totalScannedFiles?: number; - done?: boolean; - }) => { - if (!progress) return; - - if (progress.currentScanPath) { - setCurrentScanPath(progress.currentScanPath); - } - - if (typeof progress.totalScannedFiles === 'number') { - setCountScannedFiles(progress.totalScannedFiles); - } - - if (typeof progress.progress === 'number') { - setProgressRatio(progress.progress); - } - - if (Array.isArray(progress.infectedFiles) && progress.infectedFiles.length > 0) { - setInfectedFiles(progress.infectedFiles); - } - - if (progress.done) { - setProgressRatio(100); - setTimeout(() => { - setIsScanning(false); - setIsScanCompleted(true); - }, 500); - } - }; - const resetStates = () => { setCurrentScanPath(''); setCountScannedFiles(0); @@ -192,9 +192,9 @@ export const useAntivirus = (): AntivirusContext => { const isDirectory = scanType === 'folders' || !seemsLikeFile; return { - path: path, + path, itemName: cleanPath.split('/').pop() || cleanPath, - isDirectory: isDirectory, + isDirectory, }; } return item; diff --git a/src/apps/renderer/hooks/backups/useBackupFatalIssue.tsx b/src/apps/renderer/hooks/backups/useBackupFatalIssue.tsx index c0b27f288b..7ea1d8dfae 100644 --- a/src/apps/renderer/hooks/backups/useBackupFatalIssue.tsx +++ b/src/apps/renderer/hooks/backups/useBackupFatalIssue.tsx @@ -4,6 +4,40 @@ import { BackupInfo } from '../../../backups/BackupInfo'; import { useTranslationContext } from '../../context/LocalContext'; import { shortMessages } from '../../messages/virtual-drive-error'; +async function findBackupFolder(backup: BackupInfo) { + const result = await window.electron.changeBackupPath(backup.pathname); + if (result) window.electron.startBackupsProcess(); +} + +type Action = { + name: string; + fn: undefined | ((backup: BackupInfo) => Promise); +}; + +type BackupErrorActionMap = Record; + +export const backupsErrorActions: BackupErrorActionMap = { + BASE_DIRECTORY_DOES_NOT_EXIST: { + name: 'issues.actions.find-folder', + fn: findBackupFolder, + }, + NOT_EXISTS: undefined, + NO_INTERNET: undefined, + NO_REMOTE_CONNECTION: undefined, + BAD_RESPONSE: undefined, + EMPTY_FILE: undefined, + FILE_TOO_BIG: undefined, + FILE_NON_EXTENSION: undefined, + UNKNOWN: undefined, + DUPLICATED_NODE: undefined, + ACTION_NOT_PERMITTED: undefined, + FILE_ALREADY_EXISTS: undefined, + COULD_NOT_ENCRYPT_NAME: undefined, + BAD_REQUEST: undefined, + INSUFFICIENT_PERMISSION: undefined, + NOT_ENOUGH_SPACE: undefined, +}; + type FixAction = { name: string; fn: () => Promise; @@ -46,37 +80,3 @@ export function useBackupFatalIssue(backup: BackupInfo) { return { issue, message, action }; } - -async function findBackupFolder(backup: BackupInfo) { - const result = await window.electron.changeBackupPath(backup.pathname); - if (result) window.electron.startBackupsProcess(); -} - -type Action = { - name: string; - fn: undefined | ((backup: BackupInfo) => Promise); -}; - -type BackupErrorActionMap = Record; - -export const backupsErrorActions: BackupErrorActionMap = { - BASE_DIRECTORY_DOES_NOT_EXIST: { - name: 'issues.actions.find-folder', - fn: findBackupFolder, - }, - NOT_EXISTS: undefined, - NO_INTERNET: undefined, - NO_REMOTE_CONNECTION: undefined, - BAD_RESPONSE: undefined, - EMPTY_FILE: undefined, - FILE_TOO_BIG: undefined, - FILE_NON_EXTENSION: undefined, - UNKNOWN: undefined, - DUPLICATED_NODE: undefined, - ACTION_NOT_PERMITTED: undefined, - FILE_ALREADY_EXISTS: undefined, - COULD_NOT_ENCRYPT_NAME: undefined, - BAD_REQUEST: undefined, - INSUFFICIENT_PERMISSION: undefined, - NOT_ENOUGH_SPACE: undefined, -}; diff --git a/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.test.ts b/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.test.ts index 7b5970aa8a..191ffef176 100644 --- a/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.test.ts +++ b/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.test.ts @@ -10,9 +10,11 @@ describe('useUserAvailableProducts', () => { antivirus: false, cleaner: true, }; + const reportErrorMock = vi.fn(); beforeEach(() => { vi.clearAllMocks(); + vi.stubGlobal('reportError', reportErrorMock); vi.mocked(window.electron.userAvailableProducts.get).mockResolvedValue(undefined); vi.mocked(window.electron.userAvailableProducts.onUpdate).mockReturnValue(mockListener); }); @@ -82,17 +84,13 @@ describe('useUserAvailableProducts', () => { const error = new Error('Failed to fetch products'); vi.mocked(window.electron.userAvailableProducts.get).mockRejectedValue(error); - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const { result } = renderHook(() => useUserAvailableProducts()); // Wait for the promise to reject and be handled await vi.waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch user available products:', error); + expect(reportErrorMock).toHaveBeenCalledWith(`Failed to fetch user available products: ${error}`); }); expect(result.current.products).toBeUndefined(); - - consoleErrorSpy.mockRestore(); }); }); diff --git a/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.ts b/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.ts index 57bd789962..7429da0d64 100644 --- a/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.ts +++ b/src/apps/renderer/hooks/useUserAvailableProducts/useUserAvailableProducts.ts @@ -10,7 +10,7 @@ export function useUserAvailableProducts() { .get() .then(setProducts) .catch((error) => { - console.error('Failed to fetch user available products:', error); + reportError(`Failed to fetch user available products: ${error}`); }); userAvailableProducts.subscribe(); diff --git a/src/apps/renderer/hooks/useVirtualDriveStatus.tsx b/src/apps/renderer/hooks/useVirtualDriveStatus.tsx index caa4e37efb..97e3e97b29 100644 --- a/src/apps/renderer/hooks/useVirtualDriveStatus.tsx +++ b/src/apps/renderer/hooks/useVirtualDriveStatus.tsx @@ -15,7 +15,7 @@ export default function useVirtualDriveStatus() { useEffect(() => { const removeListener = window.electron.onVirtualDriveStatusChange((status) => { - console.debug('status changed'); + reportError('status changed'); setVirtualDriveStatus(status.status); }); diff --git a/src/apps/renderer/pages/Login/index.tsx b/src/apps/renderer/pages/Login/index.tsx index e9ff0c0535..5827e268c0 100644 --- a/src/apps/renderer/pages/Login/index.tsx +++ b/src/apps/renderer/pages/Login/index.tsx @@ -12,7 +12,7 @@ export default function Login() { setIsLoading(true); await window.electron.openUrl(URL); } catch (error) { - console.error('Error opening URL:', error); + reportError(error); } finally { setIsLoading(false); } @@ -28,7 +28,7 @@ export default function Login() { return (
- +
diff --git a/src/apps/renderer/pages/Settings/Account/Usage.tsx b/src/apps/renderer/pages/Settings/Account/Usage.tsx index c563fcbbf3..b202335688 100644 --- a/src/apps/renderer/pages/Settings/Account/Usage.tsx +++ b/src/apps/renderer/pages/Settings/Account/Usage.tsx @@ -13,9 +13,9 @@ export default function Usage({ isInfinite, offerUpgrade, usageInBytes, limitInB if (isInfinite) { return { amount: '∞', unit: '' }; } else { - const amount = bytes.format(limitInBytes).match(/\d+/g)?.[0] ?? ''; - const unit = bytes.format(limitInBytes).match(/[a-zA-Z]+/g)?.[0] ?? ''; - return { amount: amount, unit: unit }; + const amount = bytes.format(limitInBytes)?.match(/\d+/g)?.[0] ?? ''; + const unit = bytes.format(limitInBytes)?.match(/[a-zA-Z]+/g)?.[0] ?? ''; + return { amount, unit }; } }; @@ -52,8 +52,8 @@ export default function Usage({ isInfinite, offerUpgrade, usageInBytes, limitInB

{translate('settings.account.usage.display', { - used: bytes.format(usageInBytes), - total: bytes.format(limitInBytes), + used: bytes.format(usageInBytes) || '0 B', + total: bytes.format(limitInBytes) || '0 B', })}

diff --git a/src/apps/renderer/pages/Settings/Antivirus/components/CustomScanItemsSelectorDropdown.test.tsx b/src/apps/renderer/pages/Settings/Antivirus/components/CustomScanItemsSelectorDropdown.test.tsx index 067132b28c..7644d6879c 100644 --- a/src/apps/renderer/pages/Settings/Antivirus/components/CustomScanItemsSelectorDropdown.test.tsx +++ b/src/apps/renderer/pages/Settings/Antivirus/components/CustomScanItemsSelectorDropdown.test.tsx @@ -3,7 +3,7 @@ import { CustomScanItemsSelectorDropdown } from './CustomScanItemsSelectorDropdo // Mock the DropdownItem component vi.mock('./DropdownItem', () => ({ - DropdownItem: ({ children, onClick }: any) => ( + DropdownItem: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( diff --git a/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicePill.test.tsx b/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicePill.test.tsx index 0ee708653e..71d61af387 100644 --- a/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicePill.test.tsx +++ b/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicePill.test.tsx @@ -19,7 +19,7 @@ const mockDevice: Device = { describe('DevicePill', () => { afterAll(() => { - // @ts-ignore + // @ts-expect-error - window.electron is defined by preload and not deletable by type delete window.electron; }); diff --git a/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicesList.test.tsx b/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicesList.test.tsx index e715577e66..6705f433e3 100644 --- a/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicesList.test.tsx +++ b/src/apps/renderer/pages/Settings/Backups/DevicesList/DevicesList.test.tsx @@ -73,7 +73,7 @@ describe('DevicesList', () => { }); afterAll(() => { - // @ts-ignore + // @ts-expect-error - window.electron is defined by preload and not deletable by type delete window.electron; }); diff --git a/src/apps/renderer/pages/Widget/AccountSection.test.tsx b/src/apps/renderer/pages/Widget/AccountSection.test.tsx index c7c4aaefa1..9c918ec35e 100644 --- a/src/apps/renderer/pages/Widget/AccountSection.test.tsx +++ b/src/apps/renderer/pages/Widget/AccountSection.test.tsx @@ -3,6 +3,7 @@ import { type Mock } from 'vitest'; import { useTranslationContext } from '../../context/LocalContext'; import { useUsage } from '../../context/UsageContext/useUsage'; import { AccountSection } from './AccountSection'; +import { type User } from '../../../main/types'; vi.mock('../../context/LocalContext'); vi.mock('../../context/UsageContext/useUsage'); @@ -13,7 +14,7 @@ describe('AccountSection', () => { beforeEach(() => { vi.clearAllMocks(); (useTranslationContext as Mock).mockReturnValue({ translate: (key: string) => key }); - getUserMock.mockResolvedValue(null as any); + getUserMock.mockResolvedValue(null); }); it('renders the account section container', () => { @@ -26,7 +27,11 @@ describe('AccountSection', () => { it('shows user initials when user is loaded', async () => { (useUsage as Mock).mockReturnValue({ status: 'ready', usage: null }); - getUserMock.mockResolvedValue({ name: 'John', lastname: 'Doe', email: 'john@example.com' } as any); + getUserMock.mockResolvedValue({ + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + } as Partial as User); render(); @@ -35,7 +40,11 @@ describe('AccountSection', () => { it('shows user email when user is loaded', async () => { (useUsage as Mock).mockReturnValue({ status: 'ready', usage: null }); - getUserMock.mockResolvedValue({ name: 'John', lastname: 'Doe', email: 'john@example.com' } as any); + getUserMock.mockResolvedValue({ + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + } as Partial as User); render(); diff --git a/src/apps/shared/IPC/TypedIPC.ts b/src/apps/shared/IPC/TypedIPC.ts index 7b2f1829af..733b1e5d5e 100644 --- a/src/apps/shared/IPC/TypedIPC.ts +++ b/src/apps/shared/IPC/TypedIPC.ts @@ -1,6 +1,6 @@ import { IpcMainEvent } from 'electron'; -type EventHandler = (...args: any) => any; +type EventHandler = (...args: unknown[]) => unknown; type CustomIPCEvents = Record; diff --git a/src/backend/features/cleaner/web-cache/utils/scan-firefox-cache-profiles.ts b/src/backend/features/cleaner/web-cache/utils/scan-firefox-cache-profiles.ts index 061d5a56b0..91aa469cad 100644 --- a/src/backend/features/cleaner/web-cache/utils/scan-firefox-cache-profiles.ts +++ b/src/backend/features/cleaner/web-cache/utils/scan-firefox-cache-profiles.ts @@ -25,7 +25,7 @@ export async function scanFirefoxCacheProfiles(firefoxCacheDir: string): Promise const profileDirsChecks = await Promise.allSettled( entries.map(async (entry) => { const isProfileDir = await isFirefoxProfileDirectory(entry, firefoxCacheDir); - return { entry: entry, isProfileDir }; + return { entry, isProfileDir }; }), ); diff --git a/src/backend/features/fuse/on-read/read-chunk-from-disk.test.ts b/src/backend/features/fuse/on-read/read-chunk-from-disk.test.ts index cf3088a286..aa7b47f172 100644 --- a/src/backend/features/fuse/on-read/read-chunk-from-disk.test.ts +++ b/src/backend/features/fuse/on-read/read-chunk-from-disk.test.ts @@ -1,17 +1,18 @@ import fs from 'fs/promises'; import { readChunkFromDisk } from './read-chunk-from-disk'; import { call } from '../../../../../tests/vitest/utils.helper'; +import { mockDeep } from 'vitest-mock-extended'; vi.mock(import('fs/promises')); -const fsMock = vi.mocked(fs); +const fsMock = mockDeep(fs); describe('readChunkFromDisk', () => { const closeMock = vi.fn(); const readMock = vi.fn(); beforeEach(() => { - fsMock.open.mockResolvedValue({ read: readMock, close: closeMock } as any); + fsMock.open.mockResolvedValue({ read: readMock, close: closeMock } as unknown as fs.FileHandle); }); it('should open the file in read mode', async () => { diff --git a/src/context/shared/domain/DomainEvent.ts b/src/context/shared/domain/DomainEvent.ts index db7baa9475..1b14600f76 100644 --- a/src/context/shared/domain/DomainEvent.ts +++ b/src/context/shared/domain/DomainEvent.ts @@ -1,6 +1,6 @@ import * as uuid from 'uuid'; -type DomainEventAttributes = any; +type DomainEventAttributes = Record; export abstract class DomainEvent { static EVENT_NAME: string; diff --git a/src/context/storage/StorageFiles/__mocks__/StorageFilesRepositoryMock.ts b/src/context/storage/StorageFiles/__mocks__/StorageFilesRepositoryMock.ts index 7f5d15ba94..43121055e8 100644 --- a/src/context/storage/StorageFiles/__mocks__/StorageFilesRepositoryMock.ts +++ b/src/context/storage/StorageFiles/__mocks__/StorageFilesRepositoryMock.ts @@ -11,6 +11,7 @@ export class StorageFilesRepositoryMock implements StorageFilesRepository { private deleteMock = vi.fn(); private deleteAllMock = vi.fn(); private allMock = vi.fn(); + private registerMock = vi.fn(); async exists(id: StorageFileId): Promise { return this.existsMock(id); @@ -35,8 +36,8 @@ export class StorageFilesRepositoryMock implements StorageFilesRepository { this.retrieveMock.mockReturnValueOnce(file); } - async store(file: StorageFile, readable: Readable): Promise { - return this.storeMock(file, readable); + async store(file: StorageFile, readable: Readable, onProgress: (bytesWritten: number) => void): Promise { + return this.storeMock(file, readable, onProgress); } async read(id: StorageFileId): Promise { @@ -65,6 +66,10 @@ export class StorageFilesRepositoryMock implements StorageFilesRepository { return this.allMock(); } + async register(file: StorageFile): Promise { + return this.registerMock(file); + } + returnAll(files: Awaited>) { this.allMock.mockReturnValueOnce(files); } diff --git a/src/context/storage/StorageFiles/application/download/__test-helpers__/StorageFileDownloaderTestClass.ts b/src/context/storage/StorageFiles/application/download/__test-helpers__/StorageFileDownloaderTestClass.ts index daf5047dc1..28b06ea48e 100644 --- a/src/context/storage/StorageFiles/application/download/__test-helpers__/StorageFileDownloaderTestClass.ts +++ b/src/context/storage/StorageFiles/application/download/__test-helpers__/StorageFileDownloaderTestClass.ts @@ -4,6 +4,7 @@ import { StorageFile } from '../../../domain/StorageFile'; import { DownloadProgressTrackerMock } from '../../../__mocks__/DownloadProgressTrackerMock'; import { DownloaderHandlerFactoryMock } from '../../../domain/download/__mocks__/DownloaderHandlerFactoryMock'; import { DownloaderHandler } from '../../../domain/download/DownloaderHandler'; +import { partialSpyOn } from 'tests/vitest/utils.helper'; export class StorageFileDownloaderTestClass extends StorageFileDownloader { private mock = vi.fn(); @@ -24,7 +25,7 @@ export class StorageFileDownloaderTestClass extends StorageFileDownloader { returnsAReadable() { const factory = new DownloaderHandlerFactoryMock(); const handler = factory.downloader(); - (handler.elapsedTime as any).mockReturnValue(1000); + partialSpyOn(handler, 'elapsedTime').mockReturnValue(1000); this.mock.mockResolvedValue({ stream: Readable.from('Hello world!'), metadata: { name: 'test', type: 'txt', size: 12 }, @@ -39,6 +40,6 @@ export class StorageFileDownloaderTestClass extends StorageFileDownloader { } assertHasNotBeenCalled() { - expect(this.mock).not.toHaveBeenCalled(); + expect(this.mock).not.toBeCalled(); } } diff --git a/src/context/storage/StorageFiles/application/download/download-with-progress-tracking.test.ts b/src/context/storage/StorageFiles/application/download/download-with-progress-tracking.test.ts index fc95118b65..f66e89f6c1 100644 --- a/src/context/storage/StorageFiles/application/download/download-with-progress-tracking.test.ts +++ b/src/context/storage/StorageFiles/application/download/download-with-progress-tracking.test.ts @@ -4,7 +4,8 @@ import { DownloadProgressTrackerMock } from '../../__mocks__/DownloadProgressTra import { FileMother } from '../../../../virtual-drive/files/domain/__test-helpers__/FileMother'; import { StorageFilesRepositoryMock } from '../../__mocks__/StorageFilesRepositoryMock'; import { StorageFile } from '../../domain/StorageFile'; -import { call, calls } from 'tests/vitest/utils.helper'; +import { StorageFileDownloader } from './StorageFileDownloader/StorageFileDownloader'; +import { call, calls, partialSpyOn } from 'tests/vitest/utils.helper'; describe('downloadWithProgressTracking', () => { const elapsedTime = 123; @@ -22,6 +23,8 @@ describe('downloadWithProgressTracking', () => { }); it('tracks progress, stores the file, and returns the storage file', async () => { + const storeMock = partialSpyOn(repository, 'store'); + const virtualFile = FileMother.fromPartial({ size: 100, path: 'folder/test-file.txt', @@ -32,15 +35,14 @@ describe('downloadWithProgressTracking', () => { const metadata = { name: virtualFile.name, type: virtualFile.type, size: virtualFile.size }; downloader.run.mockResolvedValue({ stream, metadata, handler }); - const storeSpy = vi.fn(async (_file: StorageFile, _readable: Readable, onProgress: (bytes: number) => void) => { + storeMock.mockImplementation(async (_file, _readable, onProgress) => { [20, 200].forEach((bytes) => onProgress(bytes)); }); - (repository as any).store = storeSpy; const result = await downloadWithProgressTracking({ virtualFile, tracker, - downloader: downloader as any, + downloader: downloader as unknown as StorageFileDownloader, repository, }); @@ -53,9 +55,9 @@ describe('downloadWithProgressTracking', () => { call(tracker.downloadFinished).toMatchObject([metadata.name, metadata.type]); call(downloader.run).toMatchObject([expect.any(StorageFile), virtualFile]); - call(storeSpy).toMatchObject([expect.any(StorageFile), stream, expect.any(Function)]); + call(storeMock).toMatchObject([expect.any(StorageFile), stream, expect.any(Function)]); - expect(result.attributes()).toEqual({ + expect(result.attributes()).toStrictEqual({ id: virtualFile.contentsId, virtualId: virtualFile.uuid, size: virtualFile.size, diff --git a/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts b/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts index 548a352d9f..abd2b5c47e 100644 --- a/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts +++ b/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts @@ -23,12 +23,7 @@ export class EnvironmentContentFileDownloader implements DownloaderHandler { } forceStop(): void { - //@ts-ignore - // Logger.debug('Finish emitter type', this.state?.type); - // Logger.debug('Finish emitter stop method', this.state?.stop); this.state?.stop(); - // this.eventEmitter.emit('error'); - // this.eventEmitter.emit('finish'); } download(file: StorageFile): Promise { diff --git a/src/context/virtual-drive/folders/application/FolderDeleter.test.ts b/src/context/virtual-drive/folders/application/FolderDeleter.test.ts index 0d2894faac..d8c5e37cd0 100644 --- a/src/context/virtual-drive/folders/application/FolderDeleter.test.ts +++ b/src/context/virtual-drive/folders/application/FolderDeleter.test.ts @@ -6,6 +6,7 @@ import { FolderRepositoryMock } from '../__mocks__/FolderRepositoryMock'; import { FolderMother } from '../domain/__test-helpers__/FolderMother'; import * as addFolderToTrashModule from '../../../../infra/drive-server/services/folder/services/add-folder-to-trash'; import { call, partialSpyOn } from 'tests/vitest/utils.helper'; +import { DriveServerError } from 'src/infra/drive-server/drive-server.error'; describe('Folder deleter', () => { let repository: FolderRepositoryMock; @@ -69,7 +70,7 @@ describe('Folder deleter', () => { repository.searchByUuidMock.mockResolvedValueOnce(folder); vi.spyOn(allParentFoldersStatusIsExists, 'run').mockResolvedValueOnce(true); - addFolderToTrashMock.mockResolvedValue({ error: new Error('Error during the deletion') } as any); + addFolderToTrashMock.mockResolvedValue({ error: new DriveServerError('UNKNOWN') }); await SUT.run(folder.uuid); diff --git a/src/context/virtual-drive/folders/application/FolderMover.test.ts b/src/context/virtual-drive/folders/application/FolderMover.test.ts index 84d72a2319..fc1b7806a8 100644 --- a/src/context/virtual-drive/folders/application/FolderMover.test.ts +++ b/src/context/virtual-drive/folders/application/FolderMover.test.ts @@ -7,6 +7,7 @@ import { FolderMother } from '../domain/__test-helpers__/FolderMother'; import { FolderDescendantsPathUpdater } from './FolderDescendantsPathUpdater'; import * as moveFolderModule from '../../../../infra/drive-server/services/folder/services/move-folder'; import { call, partialSpyOn } from 'tests/vitest/utils.helper'; +import { DriveServerError } from 'src/infra/drive-server/drive-server.error'; describe('Folder Mover', () => { let repository: FolderRepositoryMock; @@ -59,7 +60,7 @@ describe('Folder Mover', () => { const destinationPath = new FolderPath(path.join(parentDestination.path, original.name)); - moveFolderMock.mockResolvedValue({ data: {} as any }); + moveFolderMock.mockResolvedValue({ data: {} }); repository.matchingPartialMock.mockReturnValueOnce([]).mockReturnValueOnce([parentDestination]); @@ -76,7 +77,7 @@ describe('Folder Mover', () => { const destinationPath = new FolderPath(path.join(parentDestination.path, original.name)); - moveFolderMock.mockResolvedValue({ data: {} as any }); + moveFolderMock.mockResolvedValue({ data: {} }); repository.matchingPartialMock.mockReturnValueOnce([]).mockReturnValueOnce([parentDestination]); @@ -96,8 +97,8 @@ describe('Folder Mover', () => { const destinationPath = new FolderPath(path.join(parentDestination.path, original.name)); - const error = new Error('move failed'); - moveFolderMock.mockResolvedValue({ error } as any); + const error = new DriveServerError('UNKNOWN'); + moveFolderMock.mockResolvedValue({ error }); repository.matchingPartialMock.mockReturnValueOnce([]).mockReturnValueOnce([parentDestination]); diff --git a/src/context/virtual-drive/folders/application/FolderRenamer.test.ts b/src/context/virtual-drive/folders/application/FolderRenamer.test.ts index 7a6d02838c..16d20890ad 100644 --- a/src/context/virtual-drive/folders/application/FolderRenamer.test.ts +++ b/src/context/virtual-drive/folders/application/FolderRenamer.test.ts @@ -8,6 +8,7 @@ import { FolderMother } from '../domain/__test-helpers__/FolderMother'; import { EventBusMock } from '../../shared/__mocks__/EventBusMock'; import { FolderDescendantsPathUpdater } from './FolderDescendantsPathUpdater'; import * as renameFolderModule from '../../../../infra/drive-server/services/folder/services/rename-folder'; +import { DriveServerError } from '../../../../infra/drive-server/drive-server.error'; import { call, partialSpyOn } from 'tests/vitest/utils.helper'; describe('Folder Renamer', () => { @@ -44,7 +45,7 @@ describe('Folder Renamer', () => { path: destination.value, }); - renameFolderMock.mockResolvedValue({ data: {} as any }); + renameFolderMock.mockResolvedValue({ data: {} }); return { folder, @@ -130,8 +131,8 @@ describe('Folder Renamer', () => { const folder = FolderMother.any(); const destination = FolderPathMother.onFolder(folder.dirname); - const error = new Error('rename failed'); - renameFolderMock.mockResolvedValue({ error } as any); + const error = new DriveServerError('UNKNOWN'); + renameFolderMock.mockResolvedValue({ error }); await expect(renamer.run(folder, destination)).rejects.toBe(error); diff --git a/src/context/virtual-drive/folders/application/create/FolderCreatorFromServerFolder.ts b/src/context/virtual-drive/folders/application/create/FolderCreatorFromServerFolder.ts index 999e2d161e..e903df7c6f 100644 --- a/src/context/virtual-drive/folders/application/create/FolderCreatorFromServerFolder.ts +++ b/src/context/virtual-drive/folders/application/create/FolderCreatorFromServerFolder.ts @@ -10,7 +10,7 @@ export function createFolderFromServerFolder(server: ServerFolder, relativePath: parentId: server.parentId as number, updatedAt: server.updatedAt, createdAt: server.createdAt, - path: path, + path, status: server.status, }); } diff --git a/src/context/virtual-drive/folders/infrastructure/SyncMessengers/MainProcessSyncFolderMessenger.ts b/src/context/virtual-drive/folders/infrastructure/SyncMessengers/MainProcessSyncFolderMessenger.ts index b4c6dddc26..516d814532 100644 --- a/src/context/virtual-drive/folders/infrastructure/SyncMessengers/MainProcessSyncFolderMessenger.ts +++ b/src/context/virtual-drive/folders/infrastructure/SyncMessengers/MainProcessSyncFolderMessenger.ts @@ -35,7 +35,7 @@ export class MainProcessSyncFolderMessenger implements SyncFolderMessenger { virtualDriveUpdate({ action: 'CREATING_FOLDER', oldName: undefined, - name: name, + name, progress: undefined, }); } @@ -46,7 +46,7 @@ export class MainProcessSyncFolderMessenger implements SyncFolderMessenger { virtualDriveUpdate({ action: 'FOLDER_CREATED', oldName: undefined, - name: name, + name, progress: undefined, }); } diff --git a/src/infra/device/getMachineId.ts b/src/infra/device/getMachineId.ts index fa5bc9da09..165a9b9c7b 100644 --- a/src/infra/device/getMachineId.ts +++ b/src/infra/device/getMachineId.ts @@ -18,11 +18,12 @@ export function getMachineId(): Result { try { const id = readFileSync('/etc/machine-id', 'utf-8').trim(); return id ? { data: id } : { error: new MachineIdError('NON_EXISTS') }; - } catch (err: any) { - if (err.code === 'ENOENT') { + } catch (err) { + const code = err instanceof Error ? (err as NodeJS.ErrnoException).code : undefined; + if (code === 'ENOENT') { return { error: new MachineIdError('NON_EXISTS', err) }; } - if (err.code === 'EACCES') { + if (code === 'EACCES') { return { error: new MachineIdError('NO_ACCESS', err) }; } return { error: new MachineIdError('UNKNOWN', err) }; diff --git a/src/infra/drive-server/client/drive-server.client.instance.test.ts b/src/infra/drive-server/client/drive-server.client.instance.test.ts index 219b00b040..7031088faa 100644 --- a/src/infra/drive-server/client/drive-server.client.instance.test.ts +++ b/src/infra/drive-server/client/drive-server.client.instance.test.ts @@ -1,19 +1,19 @@ -import { createClient } from '../drive-server.client'; -import { getNewApiHeaders, logout } from '../../../apps/main/auth/service'; -import { call } from 'tests/vitest/utils.helper'; - -vi.mock('../drive-server.client', () => ({ - createClient: vi.fn(() => ({})), -})); - -vi.mock('../../../apps/main/auth/service', () => ({ - getNewApiHeaders: vi.fn(() => ({ Authorization: 'Bearer token' })), - logout: vi.fn(), -})); +import { partialSpyOn } from 'tests/vitest/utils.helper'; describe('driveServerClient instance', () => { let originalEnv: string | undefined; + async function importAndSpy() { + const driveServerClientModule = await import('../drive-server.client'); + const createClientMock = partialSpyOn(driveServerClientModule, 'createClient'); + + await import('./drive-server.client.instance'); + + const authServiceModule = await import('../../../apps/main/auth/service'); + + return { createClientMock, authServiceModule }; + } + beforeEach(() => { originalEnv = process.env.NEW_DRIVE_URL; vi.resetModules(); @@ -23,45 +23,41 @@ describe('driveServerClient instance', () => { if (originalEnv !== undefined) { process.env.NEW_DRIVE_URL = originalEnv; } else { - delete (process.env as any).NEW_DRIVE_URL; + Reflect.deleteProperty(process.env, 'NEW_DRIVE_URL'); } }); it('should call createClient with expected options', async () => { - await import('./drive-server.client.instance'); - call(createClient).toMatchObject({ - baseUrl: expect.any(String), - authHeadersProvider: expect.any(Function), - onUnauthorized: expect.any(Function), - }); + const { createClientMock } = await importAndSpy(); + + expect(createClientMock).toBeCalledWith( + expect.objectContaining({ + baseUrl: expect.any(String), + authHeadersProvider: expect.any(Function), + onUnauthorized: expect.any(Function), + }), + ); }); - it('should call getNewApiHeaders when authHeadersProvider is triggered', async () => { - await import('./drive-server.client.instance'); - const clientOptions = vi.mocked(createClient).mock.calls[0]![0]!; - - clientOptions.authHeadersProvider!(); + it('should use getNewApiHeaders as authHeadersProvider', async () => { + const { createClientMock, authServiceModule } = await importAndSpy(); + const clientOptions = createClientMock.mock.lastCall![0]!; - expect(getNewApiHeaders).toHaveBeenCalled(); + expect(clientOptions.authHeadersProvider).toBe(authServiceModule.getNewApiHeaders); }); - it('should call logout when onUnauthorized is triggered', async () => { - await import('./drive-server.client.instance'); - const clientOptions = vi.mocked(createClient).mock.calls[0]![0]!; - - clientOptions.onUnauthorized!(); + it('should use logout as onUnauthorized', async () => { + const { createClientMock, authServiceModule } = await importAndSpy(); + const clientOptions = createClientMock.mock.lastCall![0]!; - expect(logout).toHaveBeenCalled(); + expect(clientOptions.onUnauthorized).toBe(authServiceModule.logout); }); it('should use process.env.NEW_DRIVE_URL as baseUrl', async () => { process.env.NEW_DRIVE_URL = 'https://mock.api'; - vi.clearAllMocks(); - vi.resetModules(); - - await import('./drive-server.client.instance'); + const { createClientMock } = await importAndSpy(); - call(createClient).toMatchObject({ baseUrl: 'https://mock.api' }); + expect(createClientMock).toBeCalledWith(expect.objectContaining({ baseUrl: 'https://mock.api' })); }); }); diff --git a/src/infra/drive-server/client/interceptors/auth/attach-auth-interceptors.test.ts b/src/infra/drive-server/client/interceptors/auth/attach-auth-interceptors.test.ts index 638ac5f3ca..bd17771144 100644 --- a/src/infra/drive-server/client/interceptors/auth/attach-auth-interceptors.test.ts +++ b/src/infra/drive-server/client/interceptors/auth/attach-auth-interceptors.test.ts @@ -3,6 +3,7 @@ import { call } from 'tests/vitest/utils.helper'; import { attachAuthInterceptors } from './attach-auth-interceptors'; import { createRequestInterceptor } from './create-request-interceptor'; import { createResponseInterceptor } from './create-response-interceptor'; +import { AxiosInstance } from 'axios'; vi.mock('./create-request-interceptor'); vi.mock('./create-response-interceptor'); @@ -20,7 +21,7 @@ describe('attachAuthInterceptors', () => { request: { use: mockRequestUse }, response: { use: mockResponseUse }, }, - } as any; + } as unknown as AxiosInstance; beforeEach(() => { vi.clearAllMocks(); @@ -52,8 +53,8 @@ describe('attachAuthInterceptors', () => { call(createRequestInterceptor).toMatchObject(authHeadersProvider); call(mockRequestUse).toMatchObject(mockRequestInterceptor); - expect(createResponseInterceptor).not.toHaveBeenCalled(); - expect(mockResponseUse).not.toHaveBeenCalled(); + expect(createResponseInterceptor).not.toBeCalled(); + expect(mockResponseUse).not.toBeCalled(); }); it('should only register response interceptor when only onUnauthorized is provided', () => { @@ -61,8 +62,8 @@ describe('attachAuthInterceptors', () => { attachAuthInterceptors(instance, { onUnauthorized }); - expect(createRequestInterceptor).not.toHaveBeenCalled(); - expect(mockRequestUse).not.toHaveBeenCalled(); + expect(createRequestInterceptor).not.toBeCalled(); + expect(mockRequestUse).not.toBeCalled(); call(createResponseInterceptor).toMatchObject(onUnauthorized); call(mockResponseUse).toMatchObject([mockOnFulfilled, mockOnRejected]); }); diff --git a/src/infra/drive-server/client/interceptors/auth/create-response-interceptor.test.ts b/src/infra/drive-server/client/interceptors/auth/create-response-interceptor.test.ts index 57a2400580..7d5ca2f010 100644 --- a/src/infra/drive-server/client/interceptors/auth/create-response-interceptor.test.ts +++ b/src/infra/drive-server/client/interceptors/auth/create-response-interceptor.test.ts @@ -1,4 +1,4 @@ -import { AxiosError } from 'axios'; +import { AxiosError, AxiosHeaders, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import { createResponseInterceptor } from './create-response-interceptor'; vi.unmock('axios'); @@ -6,13 +6,13 @@ vi.unmock('axios'); describe('createResponseInterceptor', () => { it('should return response unchanged on fulfilled', () => { const { onFulfilled } = createResponseInterceptor(vi.fn()); - const response = { + const response: AxiosResponse = { data: { ok: true }, status: 200, statusText: 'OK', - headers: {}, - config: {}, - } as any; + headers: new AxiosHeaders(), + config: { headers: new AxiosHeaders() } as InternalAxiosRequestConfig, + }; const result = onFulfilled(response); @@ -27,9 +27,9 @@ describe('createResponseInterceptor', () => { error.response = { status: 401, statusText: 'Unauthorized', - headers: {}, + headers: new AxiosHeaders(), data: {}, - config: {} as any, + config: { headers: new AxiosHeaders() } as InternalAxiosRequestConfig, }; await expect(onRejected(error)).rejects.toBe(error); @@ -44,9 +44,9 @@ describe('createResponseInterceptor', () => { error.response = { status: 500, statusText: 'Server Error', - headers: {}, + headers: new AxiosHeaders(), data: {}, - config: {} as any, + config: { headers: new AxiosHeaders() } as InternalAxiosRequestConfig, }; await expect(onRejected(error)).rejects.toBe(error); diff --git a/src/infra/drive-server/client/interceptors/rate-limiter/attach-rate-limiter-interceptors.test.ts b/src/infra/drive-server/client/interceptors/rate-limiter/attach-rate-limiter-interceptors.test.ts index 61532eade3..5b350ae7d7 100644 --- a/src/infra/drive-server/client/interceptors/rate-limiter/attach-rate-limiter-interceptors.test.ts +++ b/src/infra/drive-server/client/interceptors/rate-limiter/attach-rate-limiter-interceptors.test.ts @@ -3,6 +3,7 @@ import { call } from 'tests/vitest/utils.helper'; import { attachRateLimiterInterceptors } from './attach-rate-limiter-interceptors'; import { createRequestInterceptor } from './create-request-interceptor'; import { createResponseInterceptor } from './create-response-interceptor'; +import { AxiosInstance } from 'axios'; vi.mock('./create-request-interceptor'); vi.mock('./create-response-interceptor'); @@ -20,7 +21,7 @@ describe('attachRateLimiterInterceptors', () => { request: { use: mockRequestUse }, response: { use: mockResponseUse }, }, - } as any; + } as unknown as AxiosInstance; beforeEach(() => { (createRequestInterceptor as Mock).mockReturnValue(mockRequestInterceptor); diff --git a/src/infra/drive-server/drive-server.client.ts b/src/infra/drive-server/drive-server.client.ts index 05b9d9044d..27e04a2cc0 100644 --- a/src/infra/drive-server/drive-server.client.ts +++ b/src/infra/drive-server/drive-server.client.ts @@ -56,6 +56,38 @@ type OperationResponse = ? Res : never; +/** + * Infers the query parameters for an endpoint, if any. + */ +type OperationQuery = + MethodShape extends { + parameters: { query: infer Q }; + } + ? Q + : never; + +/** + * Infers the path parameters for an endpoint, if any. + */ +type OperationPath = + MethodShape extends { + parameters: { path: infer PP }; + } + ? PP extends Record + ? PP + : never + : never; + +/** + * Options for a typed HTTP request. + */ +type RequestOptions = { + path?: OperationPath; + headers?: Record; + query?: OperationQuery; + body?: OperationRequestBody; +}; + /** * Creates a client bound to a specific OpenAPI `paths` record. * @@ -86,12 +118,7 @@ export function createClient(opts: ClientOptions) { async function request>( method: M, path: P, - o?: { - path?: Record; - headers?: Record; - query?: Record; - body?: OperationRequestBody; - }, + o?: RequestOptions, ): Promise, DriveServerError>> { let url = path as string; @@ -128,12 +155,12 @@ export function createClient(opts: ClientOptions) { }; } } - // TODO: type `o` properly instead of `any` — currently callers get no type checking on body, path, headers, or query return { - GET:

>(p: P, o?: any) => request('get', p, o), - POST:

>(p: P, o?: any) => request('post', p, o), - PUT:

>(p: P, o?: any) => request('put', p, o), - PATCH:

>(p: P, o?: any) => request('patch', p, o), - DELETE:

>(p: P, o?: any) => request('delete', p, o), + GET:

>(p: P, o?: RequestOptions) => request('get', p, o), + POST:

>(p: P, o?: RequestOptions) => request('post', p, o), + PUT:

>(p: P, o?: RequestOptions) => request('put', p, o), + PATCH:

>(p: P, o?: RequestOptions) => request('patch', p, o), + DELETE:

>(p: P, o?: RequestOptions) => + request('delete', p, o), }; } diff --git a/src/infra/drive-server/services/auth/auth.service.test.ts b/src/infra/drive-server/services/auth/auth.service.test.ts index 91c3080810..1a38efedc1 100644 --- a/src/infra/drive-server/services/auth/auth.service.test.ts +++ b/src/infra/drive-server/services/auth/auth.service.test.ts @@ -1,9 +1,11 @@ import { AuthService } from './auth.service'; -import { authClient } from './auth.client'; -import { getBaseApiHeaders, getNewApiHeaders } from '../../../../apps/main/auth/service'; +import * as authClientModule from './auth.client'; +import * as authServiceModule from '../../../../apps/main/auth/service'; import { LoginAccessRequest, LoginAccessResponse, LoginResponse } from './auth.types'; import { logger } from '@internxt/drive-desktop-core/build/backend'; -import { Mock } from 'vitest'; +import { partialSpyOn } from 'tests/vitest/utils.helper'; +import { MockInstance } from 'vitest'; + vi.mock('axios', async (importOriginal) => { const actual = await importOriginal(); return { @@ -12,39 +14,30 @@ vi.mock('axios', async (importOriginal) => { }; }); -vi.mock('@internxt/drive-desktop-core/build/backend', () => ({ - logger: { - error: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - }, -})); - -vi.mock('../../../../apps/main/auth/service', () => ({ - getNewApiHeaders: vi.fn(), - getBaseApiHeaders: vi.fn(), -})); - -vi.mock('./auth.client', () => ({ - authClient: { - GET: vi.fn(), - POST: vi.fn(), +vi.mock('../../drive-server.module', () => ({ + driveServerModule: { + auth: {}, + backup: {}, + user: {}, }, + DriveServerModule: vi.fn(), })); describe('AuthService', () => { let sut: AuthService; + const getNewApiHeadersMock = partialSpyOn(authServiceModule, 'getNewApiHeaders'); + const getBaseApiHeadersMock = partialSpyOn(authServiceModule, 'getBaseApiHeaders'); + const authGetMock = partialSpyOn(authClientModule.authClient, 'GET') as unknown as MockInstance; + const authPostMock = partialSpyOn(authClientModule.authClient, 'POST') as unknown as MockInstance; beforeEach(() => { sut = new AuthService(); - vi.clearAllMocks(); }); describe('refresh', () => { it('should return token and newToken when response is succesful', async () => { const data = { token: 'token', newToken: 'newToken' }; - (authClient.GET as Mock).mockResolvedValue({ data }); + authGetMock.mockResolvedValue({ data }); const mockedHeaders: Record = { Authorization: 'Bearer newToken', 'content-type': 'application/json; charset=utf-8', @@ -52,18 +45,18 @@ describe('AuthService', () => { 'internxt-version': '2.4.8', 'x-internxt-desktop-header': 'test-header', }; - (getNewApiHeaders as Mock).mockReturnValue(mockedHeaders); + getNewApiHeadersMock.mockReturnValue(mockedHeaders); const result = await sut.refresh(); expect(result.isRight()).toEqual(true); expect(result.getRight()).toEqual(data); - expect(authClient.GET).toHaveBeenCalledWith('/users/refresh', { + expect(authGetMock).toHaveBeenCalledWith('/users/refresh', { headers: mockedHeaders, }); }); it('should return error when response is not successful', async () => { - (authClient.GET as Mock).mockResolvedValue({ data: undefined }); + authGetMock.mockResolvedValue({ data: undefined }); const result = await sut.refresh(); @@ -86,7 +79,7 @@ describe('AuthService', () => { it('should return error when request throws an exception', async () => { const error = new Error('Request failed'); - (authClient.GET as Mock).mockRejectedValue(error); + authGetMock.mockRejectedValue(error); const result = await sut.refresh(); @@ -116,7 +109,7 @@ describe('AuthService', () => { hasKyberKeys: false, hasEccKeys: false, }; - (authClient.POST as Mock).mockResolvedValue({ data }); + authPostMock.mockResolvedValue({ data }); const mockedHeaders: Record = { Authorization: 'Bearer token', 'content-type': 'application/json; charset=utf-8', @@ -124,13 +117,13 @@ describe('AuthService', () => { 'internxt-version': '2.4.8', 'x-internxt-desktop-header': 'test-header', }; - (getBaseApiHeaders as Mock).mockReturnValue(mockedHeaders); + getBaseApiHeadersMock.mockReturnValue(mockedHeaders); const result = await sut.login(email); expect(result.isRight()).toBe(true); expect(result.getRight()).toEqual(data); - expect(authClient.POST).toHaveBeenCalledWith('/auth/login', { + expect(authPostMock).toHaveBeenCalledWith('/auth/login', { body: { email }, headers: mockedHeaders, }); @@ -138,9 +131,8 @@ describe('AuthService', () => { it('should return error when request is not successful', async () => { const email = 'test@example.com'; - (authClient.POST as Mock).mockResolvedValue({ data: undefined }); - (getBaseApiHeaders as Mock).mockReturnValue({}); - + authPostMock.mockResolvedValue({ data: undefined }); + getBaseApiHeadersMock.mockReturnValue({}); const result = await sut.login(email); expect(result.isLeft()).toBe(true); @@ -162,8 +154,8 @@ describe('AuthService', () => { it('should return error when request throws an exception', async () => { const email = 'test@example.com'; const error = new Error('Network error'); - (authClient.POST as Mock).mockRejectedValue(error); - (getBaseApiHeaders as Mock).mockReturnValue({}); + authPostMock.mockRejectedValue(error); + getBaseApiHeadersMock.mockReturnValue({}); const result = await sut.login(email); @@ -191,18 +183,17 @@ describe('AuthService', () => { tfa: '123456', }; - const data: LoginAccessResponse = { + const data = { user: { - id: 'user-1', email: 'test@example.com', name: 'Test User', - } as any, + }, token: 'jwt-token', userTeam: {}, newToken: 'refresh-jwt', - }; + } as unknown as LoginAccessResponse; - (authClient.POST as Mock).mockResolvedValue({ data }); + authPostMock.mockResolvedValue({ data }); const mockedHeaders: Record = { Authorization: 'Bearer token', @@ -211,13 +202,13 @@ describe('AuthService', () => { 'internxt-version': '2.4.8', 'x-internxt-desktop-header': 'test-header', }; - (getBaseApiHeaders as Mock).mockReturnValue(mockedHeaders); + getBaseApiHeadersMock.mockReturnValue(mockedHeaders); const result = await sut.access(credentials); expect(result.isRight()).toBe(true); expect(result.getRight()).toEqual(data); - expect(authClient.POST).toHaveBeenCalledWith('/auth/login/access', { + expect(authPostMock).toHaveBeenCalledWith('/auth/login/access', { body: credentials, headers: mockedHeaders, }); @@ -230,8 +221,8 @@ describe('AuthService', () => { tfa: '123456', }; - (authClient.POST as Mock).mockResolvedValue({ data: undefined }); - (getBaseApiHeaders as Mock).mockReturnValue({}); + authPostMock.mockResolvedValue({ data: undefined }); + getBaseApiHeadersMock.mockReturnValue({}); const result = await sut.access(credentials); @@ -259,8 +250,8 @@ describe('AuthService', () => { }; const error = new Error('Network error'); - (authClient.POST as Mock).mockRejectedValue(error); - (getBaseApiHeaders as Mock).mockReturnValue({}); + authPostMock.mockRejectedValue(error); + getBaseApiHeadersMock.mockReturnValue({}); const result = await sut.access(credentials); diff --git a/src/infra/drive-server/services/utils/mapError.test.ts b/src/infra/drive-server/services/utils/mapError.test.ts index 6c7d475330..4303d098e1 100644 --- a/src/infra/drive-server/services/utils/mapError.test.ts +++ b/src/infra/drive-server/services/utils/mapError.test.ts @@ -22,7 +22,7 @@ describe('mapError', () => { const err = mapError(axiosError); expect(err).toBeInstanceOf(Error); expect(err.message).toBe('Invalid token'); - expect((err as any).cause).toBe(axiosError); + expect(err.cause).toBe(axiosError); }); it('should fall back to Axios error message if response.data.message is missing', () => { diff --git a/src/infra/ipc/auth-ipc-handlers.test.ts b/src/infra/ipc/auth-ipc-handlers.test.ts index 35a6a038eb..13a7bea575 100644 --- a/src/infra/ipc/auth-ipc-handlers.test.ts +++ b/src/infra/ipc/auth-ipc-handlers.test.ts @@ -1,39 +1,30 @@ +import { IpcMainEvent } from 'electron'; import { registerAuthIPCHandlers } from './auth-ipc-handlers'; import { AuthIPCMain } from './auth-ipc-main'; import { driveServerModule } from '../drive-server/drive-server.module'; -import { LoginResponse } from '../drive-server/services/auth/auth.types'; -import { Mock } from 'vitest'; - -vi.mock('../drive-server/drive-server.module', () => ({ - driveServerModule: { - auth: { - login: vi.fn(), - access: vi.fn(), - }, - }, -})); - -vi.mock('./auth-ipc-main', () => ({ - AuthIPCMain: { - handle: vi.fn(), - }, -})); +import { LoginAccessResponse, LoginResponse } from '../drive-server/services/auth/auth.types'; +import { partialSpyOn } from 'tests/vitest/utils.helper'; describe('registerAuthIPCHandlers', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + const loginMock = partialSpyOn(driveServerModule.auth, 'login'); + const accessMock = partialSpyOn(driveServerModule.auth, 'access'); + const authIPCMainHandleMock = partialSpyOn(AuthIPCMain, 'handle'); + + function getHandler(eventName: string) { + registerAuthIPCHandlers(); + const call = authIPCMainHandleMock.mock.calls.find(([name]) => name === eventName); + if (!call) throw new Error(`Handler for '${eventName}' not registered`); + return call[1]; + } describe('auth:login', () => { it('should register the auth:login handler', () => { registerAuthIPCHandlers(); - expect(AuthIPCMain.handle).toHaveBeenCalledWith('auth:login', expect.any(Function)); + expect(authIPCMainHandleMock).toBeCalledWith('auth:login', expect.any(Function)); }); it('should return a successful response for auth:login', async () => { - registerAuthIPCHandlers(); - const loginMock = driveServerModule.auth.login as Mock; const response: LoginResponse = { hasKeys: true, sKey: 'test-sKey', @@ -42,31 +33,28 @@ describe('registerAuthIPCHandlers', () => { hasEccKeys: false, }; loginMock.mockResolvedValueOnce({ - fold: (_onLeft: any, onRight: any) => onRight(response), + fold: (_onLeft: (err: Error) => T, onRight: (data: LoginResponse) => T): T => onRight(response), }); - const handler = (AuthIPCMain.handle as Mock).mock.calls.find(([eventName]) => eventName === 'auth:login')![1]; + const handler = getHandler('auth:login'); + const result = await handler({} as IpcMainEvent, 'test@example.com'); - const result = await handler({}, 'test@example.com'); - - expect(result).toEqual({ + expect(result).toStrictEqual({ success: true, data: response, }); }); it('should return an error response for auth:login', async () => { - registerAuthIPCHandlers(); - const loginMock = driveServerModule.auth.login as Mock; loginMock.mockResolvedValueOnce({ - fold: (onLeft: any, _onRight: any) => onLeft(new Error('Login failed')), + fold: (onLeft: (err: Error) => T, _onRight: (data: LoginResponse) => T): T => + onLeft(new Error('Login failed')), }); - const handler = (AuthIPCMain.handle as Mock).mock.calls.find(([eventName]) => eventName === 'auth:login')![1]; + const handler = getHandler('auth:login'); + const result = await handler({} as IpcMainEvent, 'test@example.com'); - const result = await handler({}, 'test@example.com'); - - expect(result).toEqual({ + expect(result).toStrictEqual({ success: false, error: 'Login failed', }); @@ -76,39 +64,34 @@ describe('registerAuthIPCHandlers', () => { describe('auth:access', () => { it('should register the auth:access handler', () => { registerAuthIPCHandlers(); - expect(AuthIPCMain.handle).toHaveBeenCalledWith('auth:access', expect.any(Function)); + expect(authIPCMainHandleMock).toBeCalledWith('auth:access', expect.any(Function)); }); it('should return a successful response for auth:access', async () => { - registerAuthIPCHandlers(); - const accessMock = driveServerModule.auth.access as Mock; - const mockAccessData = { sessionId: 'abc123' }; + const mockAccessData = { sessionId: 'abc123' } as unknown as LoginAccessResponse; accessMock.mockResolvedValueOnce({ - fold: (_onLeft: any, onRight: any) => onRight(mockAccessData), + fold: (_onLeft: (err: Error) => T, onRight: (data: LoginAccessResponse) => T): T => onRight(mockAccessData), }); - const handler = (AuthIPCMain.handle as Mock).mock.calls.find(([eventName]) => eventName === 'auth:access')![1]; - - const result = await handler({}, { email: 'test@example.com', code: '123456' }); + const handler = getHandler('auth:access'); + const result = await handler({} as IpcMainEvent, { email: 'test@example.com', password: '123456' }); - expect(result).toEqual({ + expect(result).toStrictEqual({ success: true, data: mockAccessData, }); }); it('should return an error response for auth:access', async () => { - registerAuthIPCHandlers(); - const accessMock = driveServerModule.auth.access as Mock; accessMock.mockResolvedValueOnce({ - fold: (onLeft: any, _onRight: any) => onLeft(new Error('Access denied')), + fold: (onLeft: (err: Error) => T, _onRight: (data: LoginAccessResponse) => T): T => + onLeft(new Error('Access denied')), }); - const handler = (AuthIPCMain.handle as Mock).mock.calls.find(([eventName]) => eventName === 'auth:access')![1]; - - const result = await handler({}, { email: 'test@example.com', code: '123456' }); + const handler = getHandler('auth:access'); + const result = await handler({} as IpcMainEvent, { email: 'test@example.com', password: '123456' }); - expect(result).toEqual({ + expect(result).toStrictEqual({ success: false, error: 'Access denied', }); diff --git a/src/types/NodeClamError.d.ts b/src/types/NodeClamError.d.ts index 4a45a137ca..b9e795dc8a 100644 --- a/src/types/NodeClamError.d.ts +++ b/src/types/NodeClamError.d.ts @@ -3,7 +3,6 @@ declare module '@internxt/scan/lib/NodeClamError' { constructor(message: string); data?: { err?: Error; - [key: string]: any; }; } }