Files
privacy.sexy/tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts
undergroundwires c546a33eff Show native save dialogs in desktop app #50, #264
This commit introduces native operating system file dialogs in the
desktop application replacing the existing web-based dialogs.

It lays the foundation for future enhancements such as:

- Providing error messages when saving or executing files, addressing
  #264.
- Creating system restore points, addressing #50.

Documentation updates:

- Update `desktop-vs-web-features.md` with added functionality.
- Update `README.md` with security feature highlights.
- Update home page documentation to emphasize security features.

Other supporting changes include:

- Integrate IPC communication channels for secure Electron dialog API
  interactions.
- Refactor `IpcRegistration` for more type-safety and simplicity.
- Introduce a Vue hook to encapsulate dialog functionality.
- Improve errors during IPC registration for easier troubleshooting.
- Move `ClientLoggerFactory` for consistency in hooks organization and
  remove `LoggerFactory` interface for simplicity.
- Add tests for the save file dialog in the browser context.
- Add `Blob` polyfill in tests to compensate for the missing
  `blob.text()` function in `jsdom` (see jsdom/jsdom#2555).

Improve environment detection logic:

- Treat test environment as browser environments to correctly activate
  features based on the environment. This resolves issues where the
  environment is misidentified as desktop, but Electron preloader APIs
  are missing.
- Rename `isDesktop` environment identification variable to
  `isRunningAsDesktopApplication` for better clarity and to avoid
  confusion with desktop environments in web/browser/test environments.
- Simplify `BrowserRuntimeEnvironment` to consistently detect
  non-desktop application environments.
- Improve environment detection for Electron main process
  (electron/electron#2288).
2024-01-13 18:04:23 +01:00

224 lines
7.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
import { PropertyKeys } from '@/TypeHelpers';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
describe('WindowVariablesValidator', () => {
describe('validateWindowVariables', () => {
describe('validates window type', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const expectedError = 'window is not an object';
const window: Partial<WindowVariables> = invalidObjectValue as never;
// act
const act = () => validateWindowVariables(window);
// assert
expect(act).to.throw(expectedError);
});
});
describe('property validations', () => {
it('throws an error with a description of all invalid properties', () => {
// arrange
const invalidOs = 'invalid' as unknown as OperatingSystem;
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
const expectedError = getExpectedError(
{
name: 'os',
object: invalidOs,
},
{
name: 'isRunningAsDesktopApplication',
object: invalidIsRunningAsDesktopApplication,
},
);
const input = new WindowVariablesStub()
.withOs(invalidOs)
.withIsRunningAsDesktopApplication(invalidIsRunningAsDesktopApplication);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
describe('`os` property', () => {
it('throws an error when os is not a number', () => {
// arrange
const invalidOs = 'Linux' as unknown as OperatingSystem;
const expectedError = getExpectedError(
{
name: 'os',
object: invalidOs,
},
);
const input = new WindowVariablesStub()
.withOs(invalidOs);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
it('throws an error for an invalid numeric os value', () => {
// arrange
const invalidOs = Number.MAX_SAFE_INTEGER;
const expectedError = getExpectedError(
{
name: 'os',
object: invalidOs,
},
);
const input = new WindowVariablesStub()
.withOs(invalidOs);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
it('does not throw for a missing os value', () => {
// arrange
const input = new WindowVariablesStub()
.withIsRunningAsDesktopApplication(true)
.withOs(undefined);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
describe('`isRunningAsDesktopApplication` property', () => {
it('does not throw when true with valid services', () => {
// arrange
const windowVariables = new WindowVariablesStub();
const windowVariableConfigurators: Record< // Ensure types match for compile-time checking
PropertyKeys<Required<WindowVariables>>,
(stub: WindowVariablesStub) => WindowVariablesStub> = {
isRunningAsDesktopApplication: (s) => s.withIsRunningAsDesktopApplication(true),
codeRunner: (s) => s.withCodeRunner(new CodeRunnerStub()),
os: (s) => s.withOs(OperatingSystem.Windows),
log: (s) => s.withLog(new LoggerStub()),
dialog: (s) => s.withDialog(new DialogStub()),
};
Object
.values(windowVariableConfigurators)
.forEach((configure) => configure(windowVariables));
// act
const act = () => validateWindowVariables(windowVariables);
// assert
expect(act).to.not.throw();
});
describe('does not throw when false without services', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const absentCodeRunner = absentValue;
const input = new WindowVariablesStub()
.withIsRunningAsDesktopApplication(undefined)
.withCodeRunner(absentCodeRunner);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
}, { excludeNull: true });
});
});
describe('`codeRunner` property', () => {
expectObjectOnDesktop('codeRunner');
});
describe('`log` property', () => {
expectObjectOnDesktop('log');
});
});
it('does not throw for a valid object', () => {
const input = new WindowVariablesStub();
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
});
function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
describe('validates object type on desktop', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const isOnDesktop = true;
const invalidObject = invalidObjectValue as T;
const expectedError = getExpectedError({
name: key,
object: invalidObject,
});
const input: WindowVariables = {
...new WindowVariablesStub(),
isRunningAsDesktopApplication: isOnDesktop,
[key]: invalidObject,
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
});
describe('does not validate object type when not on desktop', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const invalidObject = invalidObjectValue as T;
const input: WindowVariables = {
...new WindowVariablesStub(),
isRunningAsDesktopApplication: undefined,
[key]: invalidObject,
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
}
function itEachInvalidObjectValue<T>(runner: (invalidObjectValue: T) => void) {
const testCases: Array<{
readonly name: string;
readonly value: T;
}> = [
{
name: 'given string',
value: 'invalid object' as unknown as T,
},
{
name: 'given array of objects',
value: [{}, {}] as unknown as T,
},
...getAbsentObjectTestCases().map((testCase) => ({
name: `given absent: ${testCase.valueName}`,
value: testCase.absentValue as unknown as T,
})),
];
testCases.forEach((testCase) => {
it(testCase.name, () => {
runner(testCase.value);
});
});
}
function getExpectedError(...unexpectedObjects: Array<{
readonly name: keyof WindowVariables;
readonly object: unknown;
}>) {
const errors = unexpectedObjects
.map(({ name, object }) => `Unexpected ${name} (${typeof object})`);
return errors.join('\n');
}