Fix file retention after updates on macOS #417
This fixes issue #417 where autoupdate installer files were not deleted on macOS, leading to accumulation of old installers. Key changes: - Store update files in application-specific directory - Clear update files directory on every app launch Other supporting changes: - Refactor file system operations to be more testable and reusable - Improve separation of concerns in directory management - Enhance dependency injection for auto-update logic - Fix async completion to support `await` operations - Add additional logging and revise some log messages during updates
This commit is contained in:
41
tests/unit/shared/Stubs/ApplicationDirectoryProviderStub.ts
Normal file
41
tests/unit/shared/Stubs/ApplicationDirectoryProviderStub.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
DirectoryCreationOutcome,
|
||||
ApplicationDirectoryProvider,
|
||||
DirectoryType,
|
||||
DirectoryCreationError,
|
||||
} from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
|
||||
|
||||
export class ApplicationDirectoryProviderStub implements ApplicationDirectoryProvider {
|
||||
private directoryPaths: Record<DirectoryType, string> = {
|
||||
'update-installation-files': `[${ApplicationDirectoryProviderStub.name}]update installation files directory`,
|
||||
'script-runs': `[${ApplicationDirectoryProviderStub.name}]scripts directory`,
|
||||
};
|
||||
|
||||
private failure: DirectoryCreationError | undefined = undefined;
|
||||
|
||||
public withDirectoryPath(type: DirectoryType, directoryPath: string): this {
|
||||
this.directoryPaths[type] = directoryPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public provideDirectory(type: DirectoryType): Promise<DirectoryCreationOutcome> {
|
||||
if (this.failure) {
|
||||
return Promise.resolve({
|
||||
success: false,
|
||||
error: this.failure,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
directoryAbsolutePath: this.directoryPaths[type],
|
||||
});
|
||||
}
|
||||
|
||||
public withFailure(error?: DirectoryCreationError): this {
|
||||
this.failure = error ?? {
|
||||
type: 'DirectoryWriteError',
|
||||
message: `[${ApplicationDirectoryProviderStub.name}]injected failure`,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
}
|
||||
21
tests/unit/shared/Stubs/FileSystemAccessorWithRetryStub.ts
Normal file
21
tests/unit/shared/Stubs/FileSystemAccessorWithRetryStub.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { FileSystemAccessorWithRetry } from '@/presentation/electron/main/Update/ManualUpdater/FileSystemAccessorWithRetry';
|
||||
|
||||
export class FileSystemAccessorWithRetryStub {
|
||||
private retryAmount = 0;
|
||||
|
||||
public withAlwaysRetry(retryAmount: number): this {
|
||||
this.retryAmount = retryAmount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public get(): FileSystemAccessorWithRetry {
|
||||
return async (fileOperation) => {
|
||||
const result = await fileOperation();
|
||||
for (let i = 0; i < this.retryAmount; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fileOperation();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
161
tests/unit/shared/Stubs/FileSystemOperationsStub.ts
Normal file
161
tests/unit/shared/Stubs/FileSystemOperationsStub.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class FileSystemOperationsStub
|
||||
extends StubWithObservableMethodCalls<FileSystemOperations>
|
||||
implements FileSystemOperations {
|
||||
private readonly writtenFiles: Map<string, string> = new Map();
|
||||
|
||||
private readonly fileAvailability: Map<string, boolean> = new Map();
|
||||
|
||||
private directoryContents: Map<string, string[]> = new Map();
|
||||
|
||||
private userDataDirectory = `/${FileSystemOperationsStub.name}-user-data-dir/`;
|
||||
|
||||
private combinePathSequence = new Array<string>();
|
||||
|
||||
private combinePathScenarios = new Map<string, string>();
|
||||
|
||||
private combinePathDefaultSeparator = `/[${FileSystemOperationsStub.name}]PATH-SEGMENT-SEPARATOR/`;
|
||||
|
||||
public setFilePermissions(filePath: string, mode: string | number): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'setFilePermissions',
|
||||
args: [filePath, mode],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public writeFile = (filePath: string, fileContents: string, encoding: NodeJS.BufferEncoding) => {
|
||||
this.registerMethodCall({
|
||||
methodName: 'writeFile',
|
||||
args: [filePath, fileContents, encoding],
|
||||
});
|
||||
this.writtenFiles.set(filePath, fileContents);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
public readFile = (filePath: string, encoding: NodeJS.BufferEncoding) => {
|
||||
this.registerMethodCall({
|
||||
methodName: 'readFile',
|
||||
args: [filePath, encoding],
|
||||
});
|
||||
const fileContents = this.writtenFiles.get(filePath);
|
||||
return Promise.resolve(fileContents ?? `[${FileSystemOperationsStub.name}] file-contents`);
|
||||
};
|
||||
|
||||
public createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'createDirectory',
|
||||
args: [directoryPath, isRecursive],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public isFileAvailable(filePath: string): Promise<boolean> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'isFileAvailable',
|
||||
args: [filePath],
|
||||
});
|
||||
const availability = this.fileAvailability.get(filePath);
|
||||
if (availability !== undefined) {
|
||||
return Promise.resolve(availability);
|
||||
}
|
||||
const fileContents = this.writtenFiles.get(filePath);
|
||||
if (fileContents !== undefined) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public isDirectoryAvailable(directoryPath: string): Promise<boolean> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'isDirectoryAvailable',
|
||||
args: [directoryPath],
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
public deletePath(filePath: string): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'deletePath',
|
||||
args: [filePath],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public withUserDirectoryResult(directory: string): this {
|
||||
this.userDataDirectory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getUserDataDirectory(): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'getUserDataDirectory',
|
||||
args: [],
|
||||
});
|
||||
return this.userDataDirectory;
|
||||
}
|
||||
|
||||
public listDirectoryContents(directoryPath: string): Promise<string[]> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'listDirectoryContents',
|
||||
args: [directoryPath],
|
||||
});
|
||||
const contents = this.directoryContents.get(directoryPath);
|
||||
return Promise.resolve(contents ?? []);
|
||||
}
|
||||
|
||||
public withDirectoryContents(
|
||||
directoryPath: string,
|
||||
fileOrFolderNames: readonly string[],
|
||||
): this {
|
||||
this.directoryContents.set(directoryPath, [...fileOrFolderNames]);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFileAvailability(
|
||||
filePath: string,
|
||||
isAvailable: boolean,
|
||||
): this {
|
||||
this.fileAvailability.set(filePath, isAvailable);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withJoinResult(returnValue: string, ...paths: string[]): this {
|
||||
this.combinePathScenarios.set(getCombinePathsScenarioKey(paths), returnValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withJoinResultSequence(...valuesToReturn: string[]): this {
|
||||
this.combinePathSequence.push(...valuesToReturn);
|
||||
this.combinePathSequence.reverse();
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDefaultSeparator(defaultSeparator: string): this {
|
||||
this.combinePathDefaultSeparator = defaultSeparator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public combinePaths(...pathSegments: string[]): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'combinePaths',
|
||||
args: pathSegments,
|
||||
});
|
||||
const nextInSequence = this.combinePathSequence.pop();
|
||||
if (nextInSequence) {
|
||||
return nextInSequence;
|
||||
}
|
||||
const key = getCombinePathsScenarioKey(pathSegments);
|
||||
const foundScenario = this.combinePathScenarios.get(key);
|
||||
if (foundScenario) {
|
||||
return foundScenario;
|
||||
}
|
||||
return pathSegments.join(this.combinePathDefaultSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
function getCombinePathsScenarioKey(paths: string[]): string {
|
||||
return paths.join('|');
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { FileSystemOps } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class FileSystemOpsStub
|
||||
extends StubWithObservableMethodCalls<FileSystemOps>
|
||||
implements FileSystemOps {
|
||||
public setFilePermissions(filePath: string, mode: string | number): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'setFilePermissions',
|
||||
args: [filePath, mode],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void> {
|
||||
this.registerMethodCall({
|
||||
methodName: 'createDirectory',
|
||||
args: [directoryPath, isRecursive],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { LocationOps } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class LocationOpsStub
|
||||
extends StubWithObservableMethodCalls<LocationOps>
|
||||
implements LocationOps {
|
||||
private sequence = new Array<string>();
|
||||
|
||||
private scenarios = new Map<string, string>();
|
||||
|
||||
private defaultSeparator = `/[${LocationOpsStub.name}]PATH-SEGMENT-SEPARATOR/`;
|
||||
|
||||
public withJoinResult(returnValue: string, ...paths: string[]): this {
|
||||
this.scenarios.set(LocationOpsStub.getScenarioKey(paths), returnValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withJoinResultSequence(...valuesToReturn: string[]): this {
|
||||
this.sequence.push(...valuesToReturn);
|
||||
this.sequence.reverse();
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDefaultSeparator(defaultSeparator: string): this {
|
||||
this.defaultSeparator = defaultSeparator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public combinePaths(...pathSegments: string[]): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'combinePaths',
|
||||
args: pathSegments,
|
||||
});
|
||||
const nextInSequence = this.sequence.pop();
|
||||
if (nextInSequence) {
|
||||
return nextInSequence;
|
||||
}
|
||||
const key = LocationOpsStub.getScenarioKey(pathSegments);
|
||||
const foundScenario = this.scenarios.get(key);
|
||||
if (foundScenario) {
|
||||
return foundScenario;
|
||||
}
|
||||
return pathSegments.join(this.defaultSeparator);
|
||||
}
|
||||
|
||||
private static getScenarioKey(paths: string[]): string {
|
||||
return paths.join('|');
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { OperatingSystemOps } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class OperatingSystemOpsStub
|
||||
extends StubWithObservableMethodCalls<OperatingSystemOps>
|
||||
implements OperatingSystemOps {
|
||||
private userDataDirectory = `/${OperatingSystemOpsStub.name}-user-data-dir/`;
|
||||
|
||||
public withUserDirectoryResult(directory: string): this {
|
||||
this.userDataDirectory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getUserDataDirectory(): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'getUserDataDirectory',
|
||||
args: [],
|
||||
});
|
||||
return this.userDataDirectory;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FileWriteErrorType, FileWriteOutcome, ReadbackFileWriter } from '@/infrastructure/ReadbackFileWriter/ReadbackFileWriter';
|
||||
import type { FileWriteErrorType, FileWriteOutcome, ReadbackFileWriter } from '@/infrastructure/FileSystem/ReadbackFileWriter/ReadbackFileWriter';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class ReadbackFileWriterStub
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ScriptDirectoryOutcome, ScriptDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||
|
||||
export class ScriptDirectoryProviderStub implements ScriptDirectoryProvider {
|
||||
private directoryPath = `[${ScriptDirectoryProviderStub.name}]scriptDirectory`;
|
||||
|
||||
public withDirectoryPath(directoryPath: string): this {
|
||||
this.directoryPath = directoryPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public provideScriptDirectory(): Promise<ScriptDirectoryOutcome> {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
directoryAbsolutePath: this.directoryPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,17 @@
|
||||
import type {
|
||||
CommandOps,
|
||||
FileSystemOps,
|
||||
OperatingSystemOps,
|
||||
LocationOps,
|
||||
SystemOperations,
|
||||
} from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
|
||||
import { CommandOpsStub } from './CommandOpsStub';
|
||||
import { FileSystemOpsStub } from './FileSystemOpsStub';
|
||||
import { LocationOpsStub } from './LocationOpsStub';
|
||||
import { OperatingSystemOpsStub } from './OperatingSystemOpsStub';
|
||||
import { FileSystemOperationsStub } from './FileSystemOperationsStub';
|
||||
|
||||
export class SystemOperationsStub implements SystemOperations {
|
||||
public operatingSystem: OperatingSystemOps = new OperatingSystemOpsStub();
|
||||
|
||||
public location: LocationOps = new LocationOpsStub();
|
||||
|
||||
public fileSystem: FileSystemOps = new FileSystemOpsStub();
|
||||
public fileSystem: FileSystemOperations = new FileSystemOperationsStub();
|
||||
|
||||
public command: CommandOps = new CommandOpsStub();
|
||||
|
||||
public withOperatingSystem(operatingSystemOps: OperatingSystemOps): this {
|
||||
this.operatingSystem = operatingSystemOps;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLocation(location: LocationOps): this {
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFileSystem(fileSystem: FileSystemOps): this {
|
||||
public withFileSystem(fileSystem: FileSystemOperations): this {
|
||||
this.fileSystem = fileSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user