From 1e80ee1fb0208d92943619468dc427853cbe8de7 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Tue, 1 Aug 2023 17:50:36 +0200 Subject: [PATCH] Change subtitle heading to new slogan - Unify reading subtitle/slogan throughout the application. - Refactor related unit tests for easier future changes. - Add typed constants for Vue app environment variables. --- README.md | 2 +- package.json | 1 + .../Parser/ProjectInformationParser.ts | 23 +- src/domain/IProjectInformation.ts | 2 + src/domain/ProjectInformation.ts | 4 + .../components/Code/TheCodeArea.vue | 3 +- src/presentation/components/TheHeader.vue | 3 +- .../electron/Update/ManualUpdater.ts | 1 + .../Parser/ProjectInformationParser.spec.ts | 95 +++--- tests/unit/domain/ProjectInformation.spec.ts | 290 +++++++++++------- .../shared/Stubs/ProcessEnvironmentStub.ts | 5 +- .../shared/Stubs/ProjectInformationStub.ts | 16 +- vue.config.js | 1 + 13 files changed, 276 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index e14f57e7..52c69799 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# privacy.sexy +# privacy.sexy — Now you have the choice > Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆 diff --git a/package.json b/package.json index 33b7f008..f65b0ec1 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "privacy.sexy", "version": "0.11.4", "private": true, + "slogan": "Now you have the choice", "description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆", "author": "undergroundwires", "scripts": { diff --git a/src/application/Parser/ProjectInformationParser.ts b/src/application/Parser/ProjectInformationParser.ts index 1ae13c79..9ef434d1 100644 --- a/src/application/Parser/ProjectInformationParser.ts +++ b/src/application/Parser/ProjectInformationParser.ts @@ -3,13 +3,26 @@ import { ProjectInformation } from '@/domain/ProjectInformation'; import { Version } from '@/domain/Version'; export function parseProjectInformation( - environment: NodeJS.ProcessEnv, + environment: NodeJS.ProcessEnv | VueAppEnvironment, ): IProjectInformation { - const version = new Version(environment.VUE_APP_VERSION); + const version = new Version(environment[VueAppEnvironmentKeys.VUE_APP_VERSION]); return new ProjectInformation( - environment.VUE_APP_NAME, + environment[VueAppEnvironmentKeys.VUE_APP_NAME], version, - environment.VUE_APP_REPOSITORY_URL, - environment.VUE_APP_HOMEPAGE_URL, + environment[VueAppEnvironmentKeys.VUE_APP_SLOGAN], + environment[VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL], + environment[VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL], ); } + +export const VueAppEnvironmentKeys = { + VUE_APP_VERSION: 'VUE_APP_VERSION', + VUE_APP_NAME: 'VUE_APP_NAME', + VUE_APP_SLOGAN: 'VUE_APP_SLOGAN', + VUE_APP_REPOSITORY_URL: 'VUE_APP_REPOSITORY_URL', + VUE_APP_HOMEPAGE_URL: 'VUE_APP_HOMEPAGE_URL', +} as const; + +export type VueAppEnvironment = { + [K in keyof typeof VueAppEnvironmentKeys]: string; +}; diff --git a/src/domain/IProjectInformation.ts b/src/domain/IProjectInformation.ts index 0eb5bbdf..6c0dac7f 100644 --- a/src/domain/IProjectInformation.ts +++ b/src/domain/IProjectInformation.ts @@ -4,6 +4,8 @@ import { Version } from '@/domain/Version'; export interface IProjectInformation { readonly name: string; readonly version: Version; + + readonly slogan: string; readonly repositoryUrl: string; readonly homepage: string; readonly feedbackUrl: string; diff --git a/src/domain/ProjectInformation.ts b/src/domain/ProjectInformation.ts index dbfb3b9a..a353875c 100644 --- a/src/domain/ProjectInformation.ts +++ b/src/domain/ProjectInformation.ts @@ -9,6 +9,7 @@ export class ProjectInformation implements IProjectInformation { constructor( public readonly name: string, public readonly version: Version, + public readonly slogan: string, public readonly repositoryUrl: string, public readonly homepage: string, ) { @@ -18,6 +19,9 @@ export class ProjectInformation implements IProjectInformation { if (!version) { throw new Error('undefined version'); } + if (!slogan) { + throw new Error('undefined slogan'); + } if (!repositoryUrl) { throw new Error('repositoryUrl is undefined'); } diff --git a/src/presentation/components/Code/TheCodeArea.vue b/src/presentation/components/Code/TheCodeArea.vue index 5aa09039..b83466ca 100644 --- a/src/presentation/components/Code/TheCodeArea.vue +++ b/src/presentation/components/Code/TheCodeArea.vue @@ -148,7 +148,8 @@ function getLanguage(language: ScriptingLanguage) { function getDefaultCode(language: ScriptingLanguage): string { return new CodeBuilderFactory() .create(language) - .appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows and macOS') + .appendCommentLine('privacy.sexy — Now you have the choice.') + .appendCommentLine(' 🔐 Enforce privacy & security best-practices on Windows, macOS and Linux.') .appendLine() .appendCommentLine('-- 🤔 How to use') .appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.') diff --git a/src/presentation/components/TheHeader.vue b/src/presentation/components/TheHeader.vue index 356a5c23..4f9f7db0 100644 --- a/src/presentation/components/TheHeader.vue +++ b/src/presentation/components/TheHeader.vue @@ -1,7 +1,7 @@ @@ -18,6 +18,7 @@ export default class TheHeader extends Vue { public async created() { const app = await ApplicationFactory.Current.getApp(); this.title = app.info.name; + this.subtitle = app.info.slogan; } } diff --git a/src/presentation/electron/Update/ManualUpdater.ts b/src/presentation/electron/Update/ManualUpdater.ts index d3706558..1efe32ff 100644 --- a/src/presentation/electron/Update/ManualUpdater.ts +++ b/src/presentation/electron/Update/ManualUpdater.ts @@ -32,6 +32,7 @@ function getTargetProject(targetVersion: string) { const targetProject = new ProjectInformation( existingProject.name, new Version(targetVersion), + existingProject.slogan, existingProject.repositoryUrl, existingProject.homepage, ); diff --git a/tests/unit/application/Parser/ProjectInformationParser.spec.ts b/tests/unit/application/Parser/ProjectInformationParser.spec.ts index 5cd7d604..b0e27b78 100644 --- a/tests/unit/application/Parser/ProjectInformationParser.spec.ts +++ b/tests/unit/application/Parser/ProjectInformationParser.spec.ts @@ -1,50 +1,61 @@ import 'mocha'; import { expect } from 'chai'; -import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; +import { VueAppEnvironmentKeys, parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { getProcessEnvironmentStub } from '@tests/unit/shared/Stubs/ProcessEnvironmentStub'; +import { IProjectInformation } from '@/domain/IProjectInformation'; describe('ProjectInformationParser', () => { describe('parseProjectInformation', () => { - it('parses expected repository version', () => { - // arrange - const expected = '0.11.3'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_VERSION = expected; - // act - const info = parseProjectInformation(env); - // assert - const actual = info.version.toString(); - expect(actual).to.be.equal(expected); - }); - it('parses expected repository url', () => { - // arrange - const expected = 'https://expected-repository.url'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_REPOSITORY_URL = expected; - // act - const info = parseProjectInformation(env); - // assert - expect(info.repositoryUrl).to.be.equal(expected); - }); - it('parses expected name', () => { - // arrange - const expected = 'expected-app-name'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_NAME = expected; - // act - const info = parseProjectInformation(env); - // assert - expect(info.name).to.be.equal(expected); - }); - it('parses expected homepage url', () => { - // arrange - const expected = 'https://expected.sexy'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_HOMEPAGE_URL = expected; - // act - const info = parseProjectInformation(env); - // assert - expect(info.homepage).to.be.equal(expected); - }); + interface IEnvironmentParsingTestCase { + readonly testCaseName: string; + readonly environmentVariableName: string; + readonly environmentVariableValue: string; + readonly getActualValue: (info: IProjectInformation) => string; + } + const testCases: readonly IEnvironmentParsingTestCase[] = [ + { + testCaseName: 'version', + environmentVariableName: VueAppEnvironmentKeys.VUE_APP_VERSION, + environmentVariableValue: '0.11.3', + getActualValue: (info) => info.version.toString(), + }, + { + testCaseName: 'name', + environmentVariableName: VueAppEnvironmentKeys.VUE_APP_NAME, + environmentVariableValue: 'expected-app-name', + getActualValue: (info) => info.name, + }, + { + testCaseName: 'homepage', + environmentVariableName: VueAppEnvironmentKeys.VUE_APP_HOMEPAGE_URL, + environmentVariableValue: 'https://expected.sexy', + getActualValue: (info) => info.homepage, + }, + { + testCaseName: 'repositoryUrl', + environmentVariableName: VueAppEnvironmentKeys.VUE_APP_REPOSITORY_URL, + environmentVariableValue: 'https://expected-repository.url', + getActualValue: (info) => info.repositoryUrl, + }, + { + testCaseName: 'slogan', + environmentVariableName: VueAppEnvironmentKeys.VUE_APP_SLOGAN, + environmentVariableValue: 'expected-slogan', + getActualValue: (info) => info.slogan, + }, + ]; + for (const testCase of testCases) { + it(`${testCase.testCaseName}`, () => { + // act + const expected = testCase.environmentVariableValue; + const env = getProcessEnvironmentStub(); + env[testCase.environmentVariableName] = testCase.environmentVariableValue; + // act + const info = parseProjectInformation(env); + // assert + const actual = testCase.getActualValue(info); + expect(actual).to.be.equal(expected); + }); + } }); }); diff --git a/tests/unit/domain/ProjectInformation.spec.ts b/tests/unit/domain/ProjectInformation.spec.ts index d1e20dff..8fb3ceeb 100644 --- a/tests/unit/domain/ProjectInformation.spec.ts +++ b/tests/unit/domain/ProjectInformation.spec.ts @@ -4,122 +4,141 @@ import { ProjectInformation } from '@/domain/ProjectInformation'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner'; import { VersionStub } from '@tests/unit/shared/Stubs/VersionStub'; +import { Version } from '@/domain/Version'; describe('ProjectInformation', () => { - it('sets name as expected', () => { - // arrange - const expected = 'expected-name'; - const sut = new ProjectInformation(expected, new VersionStub('0.7.2'), 'repositoryUrl', 'homepage'); - // act - const actual = sut.name; - // assert - expect(actual).to.equal(expected); + describe('retrieval of property values', () => { + interface IPropertyTestCase { + readonly testCaseName: string; + readonly expectedValue: string; + readonly buildWithExpectedValue: ( + builder: ProjectInformationBuilder, + expected: string, + ) => ProjectInformationBuilder; + readonly getActualValue: (sut: ProjectInformation) => string; + } + const propertyTestCases: readonly IPropertyTestCase[] = [ + { + testCaseName: 'name', + expectedValue: 'expected-name', + buildWithExpectedValue: (builder, expected) => builder + .withName(expected), + getActualValue: (sut) => sut.name, + }, + { + testCaseName: 'version', + expectedValue: '0.11.3', + buildWithExpectedValue: (builder, expected) => builder + .withVersion(new VersionStub(expected)), + getActualValue: (sut) => sut.version.toString(), + }, + { + testCaseName: 'repositoryWebUrl - not ending with .git', + expectedValue: 'expected-repository-url', + buildWithExpectedValue: (builder, expected) => builder + .withRepositoryUrl(expected), + getActualValue: (sut) => sut.repositoryWebUrl, + }, + { + testCaseName: 'repositoryWebUrl - ending with .git', + expectedValue: 'expected-repository-url', + buildWithExpectedValue: (builder, expected) => builder + .withRepositoryUrl(`${expected}.git`), + getActualValue: (sut) => sut.repositoryWebUrl, + }, + { + testCaseName: 'slogan', + expectedValue: 'expected-slogan', + buildWithExpectedValue: (builder, expected) => builder + .withSlogan(expected), + getActualValue: (sut) => sut.slogan, + }, + { + testCaseName: 'homepage', + expectedValue: 'expected-homepage', + buildWithExpectedValue: (builder, expected) => builder + .withHomepage(expected), + getActualValue: (sut) => sut.homepage, + }, + { + testCaseName: 'feedbackUrl', + expectedValue: 'https://github.com/undergroundwires/privacy.sexy/issues', + buildWithExpectedValue: (builder) => builder + .withRepositoryUrl('https://github.com/undergroundwires/privacy.sexy.git'), + getActualValue: (sut) => sut.feedbackUrl, + }, + { + testCaseName: 'releaseUrl', + expectedValue: 'https://github.com/undergroundwires/privacy.sexy/releases/tag/0.7.2', + buildWithExpectedValue: (builder) => builder + .withRepositoryUrl('https://github.com/undergroundwires/privacy.sexy.git') + .withVersion(new VersionStub('0.7.2')), + getActualValue: (sut) => sut.releaseUrl, + }, + ]; + for (const testCase of propertyTestCases) { + it(`should return the expected ${testCase.testCaseName} value`, () => { + // arrange + const expected = testCase.expectedValue; + const builder = new ProjectInformationBuilder(); + const sut = testCase + .buildWithExpectedValue(builder, expected) + .build(); + + // act + const actual = testCase.getActualValue(sut); + + // assert + expect(actual).to.equal(expected); + }); + } }); - it('sets version as expected', () => { - // arrange - const expected = new VersionStub('0.11.3'); - const sut = new ProjectInformation('name', expected, 'repositoryUrl', 'homepage'); - // act - const actual = sut.version; - // assert - expect(actual).to.deep.equal(expected); - }); - it('sets repositoryUrl as expected', () => { - // arrange - const expected = 'expected-repository-url'; - const sut = new ProjectInformation('name', new VersionStub('0.7.2'), expected, 'homepage'); - // act - const actual = sut.repositoryUrl; - // assert - expect(actual).to.equal(expected); - }); - describe('sets repositoryWebUrl as expected', () => { - it('sets repositoryUrl when it does not end with .git', () => { + describe('correct retrieval of download URL per operating system', () => { + const testCases: ReadonlyArray<{ + readonly os: OperatingSystem, + readonly expected: string, + readonly repositoryUrl: string, + readonly version: string, + }> = [ + { + os: OperatingSystem.macOS, + expected: 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.dmg', + repositoryUrl: 'https://github.com/undergroundwires/privacy.sexy.git', + version: '0.7.2', + }, + { + os: OperatingSystem.Linux, + expected: 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.AppImage', + repositoryUrl: 'https://github.com/undergroundwires/privacy.sexy.git', + version: '0.7.2', + }, + { + os: OperatingSystem.Windows, + expected: 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-Setup-0.7.2.exe', + repositoryUrl: 'https://github.com/undergroundwires/privacy.sexy.git', + version: '0.7.2', + }, + ]; + for (const testCase of testCases) { + it(`should return the expected download URL for ${OperatingSystem[testCase.os]}`, () => { + // arrange + const { + expected, version, repositoryUrl, os, + } = testCase; + const sut = new ProjectInformationBuilder() + .withVersion(new VersionStub(version)) + .withRepositoryUrl(repositoryUrl) + .build(); + // act + const actual = sut.getDownloadUrl(os); + // assert + expect(actual).to.equal(expected); + }); + } + it('should throw an error when provided with an invalid operating system', () => { // arrange - const expected = 'expected-repository-url'; - const sut = new ProjectInformation('name', new VersionStub('0.7.2'), expected, 'homepage'); - // act - const actual = sut.repositoryWebUrl; - // assert - expect(actual).to.equal(expected); - }); - it('removes ".git" from the end when it ends with ".git"', () => { - // arrange - const expected = 'expected-repository-url'; - const sut = new ProjectInformation('name', new VersionStub('0.7.2'), `${expected}.git`, 'homepage'); - // act - const actual = sut.repositoryWebUrl; - // assert - expect(actual).to.equal(expected); - }); - }); - it('sets homepage as expected', () => { - // arrange - const expected = 'expected-homepage'; - const sut = new ProjectInformation('name', new VersionStub('0.7.2'), 'repositoryUrl', expected); - // act - const actual = sut.homepage; - // assert - expect(actual).to.equal(expected); - }); - it('sets feedbackUrl to github issues page', () => { - // arrange - const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git'; - const expected = 'https://github.com/undergroundwires/privacy.sexy/issues'; - const sut = new ProjectInformation('name', new VersionStub('0.7.2'), repositoryUrl, 'homepage'); - // act - const actual = sut.feedbackUrl; - // assert - expect(actual).to.equal(expected); - }); - it('sets releaseUrl to github releases page', () => { - // arrange - const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git'; - const version = new VersionStub('0.7.2'); - const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/tag/0.7.2'; - const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage'); - // act - const actual = sut.releaseUrl; - // assert - expect(actual).to.equal(expected); - }); - describe('getDownloadUrl', () => { - it('gets expected url for macOS', () => { - // arrange - const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.dmg'; - const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git'; - const version = new VersionStub('0.7.2'); - const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage'); - // act - const actual = sut.getDownloadUrl(OperatingSystem.macOS); - // assert - expect(actual).to.equal(expected); - }); - it('gets expected url for Linux', () => { - // arrange - const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.AppImage'; - const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git'; - const version = new VersionStub('0.7.2'); - const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage'); - // act - const actual = sut.getDownloadUrl(OperatingSystem.Linux); - // assert - expect(actual).to.equal(expected); - }); - it('gets expected url for Windows', () => { - // arrange - const expected = 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-Setup-0.7.2.exe'; - const repositoryUrl = 'https://github.com/undergroundwires/privacy.sexy.git'; - const version = new VersionStub('0.7.2'); - const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage'); - // act - const actual = sut.getDownloadUrl(OperatingSystem.Windows); - // assert - expect(actual).to.equal(expected); - }); - describe('throws when os is invalid', () => { - // arrange - const sut = new ProjectInformation('name', new VersionStub(), 'repositoryUrl', 'homepage'); + const sut = new ProjectInformationBuilder() + .build(); // act const act = (os: OperatingSystem) => sut.getDownloadUrl(os); // assert @@ -130,3 +149,50 @@ describe('ProjectInformation', () => { }); }); }); + +class ProjectInformationBuilder { + private name = 'default-name'; + + private version: Version = new VersionStub(); + + private repositoryUrl = 'default-repository-url'; + + private homepage = 'default-homepage'; + + private slogan = 'default-slogan'; + + public withName(name: string): ProjectInformationBuilder { + this.name = name; + return this; + } + + public withVersion(version: VersionStub): ProjectInformationBuilder { + this.version = version; + return this; + } + + public withSlogan(slogan: string): ProjectInformationBuilder { + this.slogan = slogan; + return this; + } + + public withRepositoryUrl(repositoryUrl: string): ProjectInformationBuilder { + this.repositoryUrl = repositoryUrl; + return this; + } + + public withHomepage(homepage: string): ProjectInformationBuilder { + this.homepage = homepage; + return this; + } + + public build(): ProjectInformation { + return new ProjectInformation( + this.name, + this.version, + this.slogan, + this.repositoryUrl, + this.homepage, + ); + } +} diff --git a/tests/unit/shared/Stubs/ProcessEnvironmentStub.ts b/tests/unit/shared/Stubs/ProcessEnvironmentStub.ts index f28ba0c4..69aeb6ab 100644 --- a/tests/unit/shared/Stubs/ProcessEnvironmentStub.ts +++ b/tests/unit/shared/Stubs/ProcessEnvironmentStub.ts @@ -1,7 +1,10 @@ -export function getProcessEnvironmentStub(): NodeJS.ProcessEnv { +import { VueAppEnvironment } from '@/application/Parser/ProjectInformationParser'; + +export function getProcessEnvironmentStub(): VueAppEnvironment { return { VUE_APP_VERSION: '0.11.3', VUE_APP_NAME: 'stub-name', + VUE_APP_SLOGAN: 'stub-slogan', VUE_APP_REPOSITORY_URL: 'stub-repository-url', VUE_APP_HOMEPAGE_URL: 'stub-homepage-url', }; diff --git a/tests/unit/shared/Stubs/ProjectInformationStub.ts b/tests/unit/shared/Stubs/ProjectInformationStub.ts index 4b5cf52d..4f1bc144 100644 --- a/tests/unit/shared/Stubs/ProjectInformationStub.ts +++ b/tests/unit/shared/Stubs/ProjectInformationStub.ts @@ -3,21 +3,23 @@ import { Version } from '@/domain/Version'; import { VersionStub } from './VersionStub'; export class ProjectInformationStub implements IProjectInformation { - public name = 'name'; + public name = 'stub-name'; public version = new VersionStub(); - public repositoryUrl = 'repositoryUrl'; + public repositoryUrl = 'stub-repositoryUrl'; - public homepage = 'homepage'; + public homepage = 'stub-homepage'; - public feedbackUrl = 'feedbackUrl'; + public feedbackUrl = 'stub-feedbackUrl'; - public releaseUrl = 'releaseUrl'; + public releaseUrl = 'stub-releaseUrl'; - public repositoryWebUrl = 'repositoryWebUrl'; + public repositoryWebUrl = 'stub-repositoryWebUrl'; - public downloadUrl = 'downloadUrl'; + public downloadUrl = 'stub-downloadUrl'; + + public slogan = 'stub-slogan'; public withName(name: string): ProjectInformationStub { this.name = name; diff --git a/vue.config.js b/vue.config.js index 874adbb9..12b75448 100644 --- a/vue.config.js +++ b/vue.config.js @@ -71,6 +71,7 @@ function loadVueAppRuntimeVariables() { process.env.VUE_APP_NAME = packageJson.name; process.env.VUE_APP_REPOSITORY_URL = packageJson.repository.url; process.env.VUE_APP_HOMEPAGE_URL = packageJson.homepage; + process.env.VUE_APP_SLOGAN = packageJson.slogan; } function ignorePolyfills(...moduleNames) {