Compare commits

...

30 Commits
0.7.2 ... 0.7.6

Author SHA1 Message Date
undergroundwires
d9d7f62d81 run tests on all operating systems: macos, ubuntu, windows 2020-10-18 02:10:20 +01:00
undergroundwires
11e0613165 update dependencies to latest 2020-10-17 23:17:44 +01:00
undergroundwires
77c3d2bbb8 simplify "why" section 2020-09-23 20:42:05 +01:00
undergroundwires
784a67afff refactor to read more from package.json 2020-09-22 20:41:12 +01:00
undergroundwires
19a092dd31 add more reversibility 2020-09-21 23:05:31 +01:00
undergroundwires
4c2f74949b add robots.txt to explicitly allow indexing 2020-09-19 01:23:33 +01:00
undergroundwires
a3fc3782ef add docs for default0 pointing to github discussion (#30) 2020-09-20 13:58:19 +01:00
undergroundwires-bot
cdc93f032a ⬆️ bumped to 0.7.5 2020-09-19 13:41:58 +00:00
undergroundwires
7dd15ed064 fix typo 2020-09-19 15:39:48 +01:00
undergroundwires
d169434157 fix pasting in search bar after page load showing no results 2020-09-18 20:07:03 +01:00
undergroundwires
6efed72bf2 fix rendering issue in older edge/IE 2020-09-17 15:41:46 +01:00
Clayton Errington
15db311801 fix the recycling bin option (#32)
* update the recycling bin option

Powershell has a module in PS 5.1+ called Clear-Recyclebin that works better than the CMD method - rd /s %systemdrive%\$Recycle.bin

* update recycling bin delete command

one liner of ComObject in powershell instead of cmd asking for confirmation. adds better backwards compatibility.
2020-09-16 20:22:33 +00:00
undergroundwires
82d509129b fix tests and checks are not running on PRs 2020-09-16 19:10:25 +01:00
undergroundwires
939d838e35 fix reverting (reinstalling) capabilities not working 2020-09-16 02:09:23 +01:00
undergroundwires-bot
6de4ce58c4 ⬆️ bumped to 0.7.4 2020-09-14 13:57:39 +00:00
undergroundwires
ee66196d9a fix wrong path in clear all firefox user profile settings 2020-09-14 16:04:38 +01:00
undergroundwires
3c13a9e837 fix missing reg value in denying app access to account 2020-09-14 16:03:03 +01:00
undergroundwires
22b23a9ece fix spectre protection getting single lined #31 2020-09-14 16:00:20 +01:00
undergroundwires
4ae385b7fc fix checked checkbox has blue border 2020-09-13 18:42:19 +01:00
undergroundwires-bot
d9abc7f0b2 ⬆️ bumped to 0.7.3 2020-09-12 13:14:13 +00:00
undergroundwires
1f19b2528a fix typo in a test 2020-09-12 14:42:27 +01:00
undergroundwires
1f11c39773 add more detailed error message 2020-09-12 00:14:27 +01:00
undergroundwires
b6ccb5927a fix comment lines are being detected as duplicate in validation 2020-09-12 00:13:58 +01:00
undergroundwires
1d465ee318 add reversibility and more scripts to denying app access with better structure 2020-09-12 00:11:10 +01:00
undergroundwires
3ab48b1cf5 fix naming of firefox cleanup to mention profiles 2020-09-11 14:26:32 +01:00
undergroundwires
de4ac978bd fix wrong path to the main telemetry file 2020-09-10 12:52:29 +01:00
undergroundwires
8df5faf4ef improve CPU specific tweaks by conditional platform checks and reversibility 2020-09-09 13:55:21 +01:00
undergroundwires
99a2035fdb fix nvidia tweak error message, categorize and add reversibility 2020-09-08 19:41:03 +01:00
undergroundwires
a0d61728ea fix vscode settings file override and add more configs 2020-09-07 13:42:59 +01:00
undergroundwires-bot
312bf6102c ⬆️ bumped to 0.7.2 2020-09-06 18:10:12 +00:00
48 changed files with 1986 additions and 1266 deletions

View File

@@ -1,6 +1,6 @@
name: Quality checks name: Quality checks
on: push on: [ push, pull_request ]
jobs: jobs:
lint: lint:

View File

@@ -2,6 +2,7 @@ name: Security checks
on: on:
push: push:
pull_request:
schedule: schedule:
- cron: '0 0 * * 0' - cron: '0 0 * * 0'

View File

@@ -1,10 +1,13 @@
name: Test name: Test
on: push on: [ push, pull_request ]
jobs: jobs:
run-tests: run-tests:
runs-on: ubuntu-latest strategy:
matrix:
os: [macos, ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
steps: steps:
- -
name: Checkout name: Checkout

View File

@@ -1,5 +1,51 @@
# Changelog # Changelog
## 0.7.5 (2020-09-14)
* fix reverting (reinstalling) capabilities not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/939d838e3535bb1c9b00c8ea9dacb735ae41d700)
* fix tests and checks are not running on PRs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/82d509129b4e4a5df4b84786a0d6842a7d26e888)
* fix the recycling bin option (#32) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/15db3118012a172a2191a2afad57084a65b34642)
* fix rendering issue in older edge/IE | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6efed72bf25c2ddf0901caab7f22966ca13cd47a)
* fix pasting in search bar after page load showing no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d1694341578288eeaf8b80caf9296a38d76789f0)
* fix typo | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7dd15ed06433e0e6583ab0fa46a683ce6554bbea)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.4...0.7.5)
## 0.7.4 (2020-09-12)
* fix checked checkbox has blue border | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0)
* fix spectre protection getting single lined #31 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a)
* fix missing reg value in denying app access to account | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb)
* fix wrong path in clear all firefox user profile settings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4)
## 0.7.3 (2020-09-12)
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
* fix nvidia tweak error message, categorize and add reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99a2035fdb0766a4dfc2753133eab0d7666516cd)
* improve CPU specific tweaks by conditional platform checks and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8df5faf4ef05a49da63973bd0fbb5c5d07d5bd93)
* fix wrong path to the main telemetry file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/de4ac978bdda79573b36d355697b8a028d2c0beb)
* fix naming of firefox cleanup to mention profiles | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3ab48b1cf5f7f934f07e468ef2318ccee07f530c)
* add reversibility and more scripts to denying app access with better structure | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d465ee3189d0e5a827453b3f0eb4361efe23770)
* fix comment lines are being detected as duplicate in validation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6ccb5927a20412976a54fd2215eb645092f98a8)
* add more detailed error message | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f11c39773c12eccfb3efb898b58c2f6f37ab9ca)
* fix typo in a test | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f19b2528a69383e63e579d2885f01cd804abf6c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.2...0.7.3)
## 0.7.2 (2020-09-06)
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
* add reversibility for biometric disabling and do not recommend it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/db74531cd4139615c6d595959217d3651f099019)
* fix bad highlighting of selected nodes when using keyboard navigation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/255133af4dfae40171406648a3e2920f16d71cb3)
* add reversibility to removing bloatware | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c7b2a703128470a05f12c9c6e8002444def37ef8)
* fix indeterminate state being lost | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f266c33535f72b69c65985bf2eff27cd2c5a104)
* fix wording in default text in text area | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ca63a0979ef55d07d09d9443e5cea9aa888870a5)
* add best practice suggestion to come back | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f4885b6f1c82752f2143934e336d6d1b1af03015)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.1...0.7.2)
## 0.7.1 (2020-09-04) ## 0.7.1 (2020-09-04)
* fix some browsers (including firefox) downloading the script as a text file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea) * fix some browsers (including firefox) downloading the script as a text file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea)

View File

@@ -15,21 +15,20 @@
## Get started ## Get started
- Online version: [https://privacy.sexy](https://privacy.sexy) - Online version: [https://privacy.sexy](https://privacy.sexy)
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.1/privacy.sexy-Setup-0.7.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.1/privacy.sexy-0.7.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.1/privacy.sexy-0.7.1.dmg) - or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.5/privacy.sexy-Setup-0.7.5.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.5/privacy.sexy-0.7.5.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.5/privacy.sexy-0.7.5.dmg)
- 💡 Come back regularly to apply latest version for stronger privacy and security. - 💡 Come back regularly to apply latest version for stronger privacy and security.
[![privacy.sexy application](img/screenshot.png)](https://privacy.sexy) [![privacy.sexy application](img/screenshot.png)](https://privacy.sexy)
## Why ## Why
- You don't need to run any compiled software that has access to your system, just run the generated scripts. - Rich tweak pool to harden security & privacy of the OS and other software on it
- Have full visibility into what the tweaks do as you enable them. - Free (both free as in beer and free as in speech)
- Ability to revert applied scripts - No need to run any compiled software that has access to your system, just run the generated scripts
- Have full visibility into what the tweaks do as you enable them
- Ability to revert (undo) applied scripts
- Easily extendable - Easily extendable
- Everything is open-sourced including both application and infrastructure - Everything is open-source and automated (both application and its infrastructure)
- Fully automated CI/CD pipeline using GitHub actions
- to AWS for provisioning serverless infrastructure
- for building and sharing the desktop applications
## Extend scripts ## Extend scripts
@@ -49,8 +48,8 @@
- Development: `npm run serve` to compile & hot-reload for development. - Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution. - Production: `npm run build` to prepare files for distribution.
- Or run using Docker: - Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.1 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.5 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.1 undergroundwires/privacy.sexy:0.7.1` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.5 undergroundwires/privacy.sexy:0.7.5`
## Architecture ## Architecture

1453
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.7.1", "version": "0.7.5",
"author": "undergroundwires", "author": "undergroundwires",
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆", "description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
"homepage": "https://privacy.sexy",
"private": true, "private": true,
"repository": {
"type": "git",
"url": "https://github.com/undergroundwires/privacy.sexy.git"
},
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@@ -21,44 +26,44 @@
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-brands-svg-icons": "^5.14.0", "@fortawesome/free-brands-svg-icons": "^5.15.1",
"@fortawesome/free-regular-svg-icons": "^5.14.0", "@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "^0.1.10", "@fortawesome/vue-fontawesome": "^2.0.0",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.12",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"inversify": "^5.0.1", "inversify": "^5.0.1",
"liquor-tree": "^0.2.70", "liquor-tree": "^0.2.70",
"v-tooltip": "2.0.2", "v-tooltip": "2.0.2",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-class-component": "^7.2.5", "vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.6", "vue-js-modal": "^2.0.0-rc.6",
"vue-property-decorator": "^9.0.0" "vue-property-decorator": "^9.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ace": "0.0.43", "@types/ace": "0.0.44",
"@types/chai": "^4.2.12", "@types/chai": "^4.2.14",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/mocha": "^8.0.3", "@types/mocha": "^8.0.3",
"@vue/cli-plugin-typescript": "^4.5.4", "@vue/cli-plugin-typescript": "^4.5.7",
"@vue/cli-plugin-unit-mocha": "^4.5.4", "@vue/cli-plugin-unit-mocha": "^4.5.7",
"@vue/cli-service": "^4.5.4", "@vue/cli-service": "^4.5.7",
"@vue/test-utils": "1.0.4", "@vue/test-utils": "1.1.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"electron": "^10.1.0", "electron": "^10.1.3",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-log": "^4.2.4", "electron-log": "^4.2.4",
"electron-updater": "^4.3.4", "electron-updater": "^4.3.5",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.23.2", "markdownlint-cli": "^0.24.0",
"remark-cli": "^8.0.1", "remark-cli": "^9.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^3.0.1", "remark-preset-lint-consistent": "^4.0.0",
"remark-validate-links": "^10.0.2", "remark-validate-links": "^10.0.2",
"sass": "^1.26.10", "sass": "^1.27.0",
"sass-loader": "^10.0.1", "sass-loader": "^10.0.3",
"typescript": "^4.0.2", "typescript": "^4.0.3",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4", "vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.2.4"

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '../OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { DetectorBuilder } from './DetectorBuilder'; import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector'; import { IBrowserOsDetector } from './IBrowserOsDetector';

View File

@@ -1,5 +1,5 @@
import { IBrowserOsDetector } from './IBrowserOsDetector'; import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '../OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export class DetectorBuilder { export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>(); private readonly existingPartsInUserAgent = new Array<string>();

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '../OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector { export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem; detect(userAgent: string): OperatingSystem;

View File

@@ -1,7 +1,7 @@
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment'; import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
interface IEnvironmentVariables { interface IEnvironmentVariables {
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment { export interface IEnvironment {
isDesktop: boolean; isDesktop: boolean;

View File

@@ -1,24 +1,35 @@
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml'; import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
import { parseCategory } from './CategoryParser'; import { parseCategory } from './CategoryParser';
import { ProjectInformation } from '../../domain/ProjectInformation';
export function parseApplication(content: ApplicationYaml): IApplication {
export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEnv = process.env): IApplication {
validate(content); validate(content);
const categories = new Array<Category>(); const categories = new Array<Category>();
for (const action of content.actions) { for (const action of content.actions) {
const category = parseCategory(action); const category = parseCategory(action);
categories.push(category); categories.push(category);
} }
const info = readAppInformation(env);
const app = new Application( const app = new Application(
content.name, info,
content.repositoryUrl,
process.env.VUE_APP_VERSION,
categories); categories);
return app; return app;
} }
function readAppInformation(environment): IProjectInformation {
return new ProjectInformation(
environment.VUE_APP_NAME,
environment.VUE_APP_VERSION,
environment.VUE_APP_REPOSITORY_URL,
environment.VUE_APP_HOMEPAGE_URL,
);
}
function validate(content: ApplicationYaml): void { function validate(content: ApplicationYaml): void {
if (!content) { if (!content) {
throw new Error('application is null or undefined'); throw new Error('application is null or undefined');

View File

@@ -52,7 +52,7 @@ function parseCategoryChild(
children.subScripts.push(script); children.subScripts.push(script);
} else { } else {
throw new Error(`Child element is neither a category or a script. throw new Error(`Child element is neither a category or a script.
Parent: ${parent.category}, element: ${categoryOrScript}`); Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
} }
} }

View File

@@ -39,7 +39,7 @@ export class ApplicationState implements IApplicationState {
/** Initially selected scripts */ /** Initially selected scripts */
public readonly defaultScripts: Script[]) { public readonly defaultScripts: Script[]) {
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false))); this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
this.code = new ApplicationCode(this.selection, app.version); this.code = new ApplicationCode(this.selection, app.info.version);
this.filter = new UserFilter(app); this.filter = new UserFilter(app);
} }
} }

View File

@@ -2,6 +2,7 @@ import { IFilterResult } from './IFilterResult';
import { ISignal } from '@/infrastructure/Events/Signal'; import { ISignal } from '@/infrastructure/Events/Signal';
export interface IUserFilter { export interface IUserFilter {
readonly currentFilter: IFilterResult | undefined;
readonly filtered: ISignal<IFilterResult>; readonly filtered: ISignal<IFilterResult>;
readonly filterRemoved: ISignal<void>; readonly filterRemoved: ISignal<void>;
setFilter(filter: string): void; setFilter(filter: string): void;

View File

@@ -8,6 +8,7 @@ import { Signal } from '@/infrastructure/Events/Signal';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new Signal<IFilterResult>(); public readonly filtered = new Signal<IFilterResult>();
public readonly filterRemoved = new Signal<void>(); public readonly filterRemoved = new Signal<void>();
public currentFilter: IFilterResult | undefined;
constructor(private application: IApplication) { constructor(private application: IApplication) {
@@ -28,11 +29,12 @@ export class UserFilter implements IUserFilter {
filteredCategories, filteredCategories,
filter, filter,
); );
this.currentFilter = matches;
this.filtered.notify(matches); this.filtered.notify(matches);
} }
public removeFilter(): void { public removeFilter(): void {
this.currentFilter = undefined;
this.filterRemoved.notify(); this.filterRemoved.notify();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,6 @@ declare module 'js-yaml-loader!*' {
} }
export interface ApplicationYaml { export interface ApplicationYaml {
name: string;
repositoryUrl: string;
actions: ReadonlyArray<YamlCategory>; actions: ReadonlyArray<YamlCategory>;
} }

View File

@@ -2,6 +2,7 @@ import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
import { IApplication } from './IApplication'; import { IApplication } from './IApplication';
import { IProjectInformation } from './IProjectInformation';
export class Application implements IApplication { export class Application implements IApplication {
public get totalScripts(): number { return this.flattened.allScripts.length; } public get totalScripts(): number { return this.flattened.allScripts.length; }
@@ -10,13 +11,11 @@ export class Application implements IApplication {
private readonly flattened: IFlattenedApplication; private readonly flattened: IFlattenedApplication;
constructor( constructor(
public readonly name: string, public readonly info: IProjectInformation,
public readonly repositoryUrl: string,
public readonly version: string,
public readonly actions: ReadonlyArray<ICategory>) { public readonly actions: ReadonlyArray<ICategory>) {
if (!name) { throw Error('Application has no name'); } if (!info) {
if (!repositoryUrl) { throw Error('Application has no repository url'); } throw new Error('info is undefined');
if (!version) { throw Error('Version cannot be empty'); } }
this.flattened = flatten(actions); this.flattened = flatten(actions);
ensureValid(this.flattened); ensureValid(this.flattened);
ensureNoDuplicates(this.flattened.allCategories); ensureNoDuplicates(this.flattened.allCategories);

View File

@@ -1,10 +1,9 @@
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IProjectInformation } from './IProjectInformation';
export interface IApplication { export interface IApplication {
readonly name: string; readonly info: IProjectInformation;
readonly repositoryUrl: string;
readonly version: string;
readonly totalScripts: number; readonly totalScripts: number;
readonly totalCategories: number; readonly totalCategories: number;
readonly actions: ReadonlyArray<ICategory>; readonly actions: ReadonlyArray<ICategory>;

View File

@@ -0,0 +1,11 @@
import { OperatingSystem } from './OperatingSystem';
export interface IProjectInformation {
readonly name: string;
readonly version: string;
readonly repositoryUrl: string;
readonly homepage: string;
readonly feedbackUrl: string;
readonly releaseUrl: string;
readonly repositoryWebUrl: string;
getDownloadUrl(os: OperatingSystem): string;
}

View File

@@ -0,0 +1,55 @@
import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem';
export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string;
constructor(
public readonly name: string,
public readonly version: string,
public readonly repositoryUrl: string,
public readonly homepage: string,
) {
if (!name) {
throw new Error('name is undefined');
}
if (!version || +version <= 0) {
throw new Error('version should be higher than zero');
}
if (!repositoryUrl) {
throw new Error('repositoryUrl is undefined');
}
if (!homepage) {
throw new Error('homepage is undefined');
}
this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
}
public getDownloadUrl(os: OperatingSystem): string {
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
}
public get feedbackUrl(): string {
return `${this.repositoryWebUrl}/issues`;
}
public get releaseUrl(): string {
return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
}
}
function getWebUrl(gitUrl: string) {
if (gitUrl.endsWith('.git')) {
return gitUrl.substring(0, gitUrl.length - 4);
}
return gitUrl;
}
function getFileName(os: OperatingSystem, version: string): string {
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}.dmg`;
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}

View File

@@ -41,6 +41,9 @@ function mayBeUniqueLine(codeLine: string): boolean {
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
return false; return false;
} }
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
return false;
}
return true; return true;
} }

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import App from './App.vue'; import App from './App.vue';
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper'; import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
import 'core-js/fn/array/flat-map'; // Here until Vue 3 & CLI v4 https://github.com/vuejs/vue-cli/issues/3834
new ApplicationBootstrapper() new ApplicationBootstrapper()
.bootstrap(Vue); .bootstrap(Vue);

View File

@@ -49,6 +49,7 @@
state.filter.filtered.on(this.handleFiltered); state.filter.filtered.on(this.handleFiltered);
// Update initial state // Update initial state
await this.initializeNodesAsync(this.categoryId); await this.initializeNodesAsync(this.categoryId);
await this.initializeFilter(state.filter.currentFilter);
} }
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) { public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
@@ -84,6 +85,14 @@
(category: ICategory) => node.id === getCategoryNodeId(category)); (category: ICategory) => node.id === getCategoryNodeId(category));
} }
private initializeFilter(currentFilter: IFilterResult | undefined) {
if (!currentFilter) {
this.handleFilterRemoved();
} else {
this.handleFiltered(currentFilter);
}
}
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void { private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
this.selectedNodeIds = selectedScripts this.selectedNodeIds = selectedScripts
.map((node) => node.id); .map((node) => node.id);

View File

@@ -35,7 +35,7 @@
Node, Node,
}, },
}) })
export default class SelectableTree extends Vue { export default class SelectableTree extends Vue { // Keep it stateless to make it easier to switch out
@Prop() public filterPredicate?: FilterPredicate; @Prop() public filterPredicate?: FilterPredicate;
@Prop() public filterText?: string; @Prop() public filterText?: string;
@Prop() public selectedNodeIds?: ReadonlyArray<string>; @Prop() public selectedNodeIds?: ReadonlyArray<string>;

View File

@@ -72,10 +72,9 @@
public isSearching = false; public isSearching = false;
public searchHasMatches = false; public searchHasMatches = false;
public async mounted() { public async mounted() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl; this.repositoryUrl = state.app.info.repositoryWebUrl;
state.filter.filterRemoved.on(() => { state.filter.filterRemoved.on(() => {
this.isSearching = false; this.isSearching = false;
}); });

View File

@@ -22,10 +22,10 @@ const NothingChosenCode =
.appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.') .appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
.appendLine() .appendLine()
.appendCommentLine('-- 🧐 Why privacy.sexy') .appendCommentLine('-- 🧐 Why privacy.sexy')
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other softwares on it.') .appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other software on it.')
.appendCommentLine(' ✔️ You don\'t need to run any compiled software on your system, just run the generated scripts.') .appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.') .appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
.appendCommentLine(' ✔️ Free software, 100% transparency: both application & infrastructure code are open-sourced.') .appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
.toString(); .toString();
@Component @Component

View File

@@ -17,7 +17,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import DownloadUrlListItem from './DownloadUrlListItem.vue'; import DownloadUrlListItem from './DownloadUrlListItem.vue';
@Component({ @Component({

View File

@@ -12,7 +12,7 @@
import { Component, Prop, Watch } from 'vue-property-decorator'; import { Component, Prop, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
@Component @Component
export default class DownloadUrlListItem extends StatefulVue { export default class DownloadUrlListItem extends StatefulVue {
@@ -39,7 +39,7 @@ export default class DownloadUrlListItem extends StatefulVue {
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> { private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
return `${state.app.repositoryUrl}/releases/download/${state.app.version}/${getFileName(os, state.app.version)}`; return state.app.info.getDownloadUrl(os);
} }
} }
@@ -62,18 +62,6 @@ function getOperatingSystemName(os: OperatingSystem): string {
} }
} }
function getFileName(os: OperatingSystem, version: string): string {
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}.dmg`;
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -48,8 +48,8 @@ export default class TheFooter extends StatefulVue {
public async mounted() { public async mounted() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl; this.repositoryUrl = state.app.info.repositoryWebUrl;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`; this.feedbackUrl = state.app.info.feedbackUrl;
} }
} }
</script> </script>

View File

@@ -4,7 +4,7 @@
<div class="footer__section"> <div class="footer__section">
<span v-if="isDesktop" class="footer__section__item"> <span v-if="isDesktop" class="footer__section__item">
<font-awesome-icon class="icon" :icon="['fas', 'globe']" /> <font-awesome-icon class="icon" :icon="['fas', 'globe']" />
<span>Online version at <a href="https://privacy.sexy" target="_blank">https://privacy.sexy</a></span> <span>Online version at <a :href="homepageUrl" target="_blank">{{ homepageUrl }}</a></span>
</span> </span>
<span v-else class="footer__section__item"> <span v-else class="footer__section__item">
<DownloadUrlList /> <DownloadUrlList />
@@ -66,6 +66,7 @@ export default class TheFooter extends StatefulVue {
public repositoryUrl: string = ''; public repositoryUrl: string = '';
public releaseUrl: string = ''; public releaseUrl: string = '';
public feedbackUrl: string = ''; public feedbackUrl: string = '';
public homepageUrl: string = '';
constructor() { constructor() {
super(); super();
@@ -74,12 +75,15 @@ export default class TheFooter extends StatefulVue {
public async mounted() { public async mounted() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.version = state.app.version; const info = state.app.info;
this.repositoryUrl = state.app.repositoryUrl; this.version = info.version;
this.releaseUrl = `${state.app.repositoryUrl}/releases/tag/${state.app.version}`; this.homepageUrl = info.homepage;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`; this.repositoryUrl = info.repositoryWebUrl;
this.releaseUrl = info.releaseUrl;
this.feedbackUrl = info.feedbackUrl;
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -16,7 +16,7 @@ export default class TheHeader extends StatefulVue {
public async mounted() { public async mounted() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.title = state.app.name; this.title = state.app.info.name;
} }
} }
</script> </script>

View File

@@ -25,6 +25,7 @@
&-checkbox { &-checkbox {
&.checked { &.checked {
background: $accent !important; background: $accent !important;
border-color: $accent !important;
} }
&.indeterminate { &.indeterminate {
border-color: $gray !important; border-color: $gray !important;

View File

@@ -1,5 +1,5 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector'; import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector';
import { BrowserOsTestCases } from './BrowserOsTestCases'; import { BrowserOsTestCases } from './BrowserOsTestCases';

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
interface IBrowserOsTestCase { interface IBrowserOsTestCase {
userAgent: string; userAgent: string;

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
interface IDesktopTestCase { interface IDesktopTestCase {
processPlatform: string; processPlatform: string;

View File

@@ -1,5 +1,5 @@
import { IBrowserOsDetector } from '@/application/Environment/BrowserOs/IBrowserOsDetector'; import { IBrowserOsDetector } from '@/application/Environment/BrowserOs/IBrowserOsDetector';
import { OperatingSystem } from '@/application/Environment/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { DesktopOsTestCases } from './DesktopOsTestCases'; import { DesktopOsTestCases } from './DesktopOsTestCases';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { expect } from 'chai'; import { expect } from 'chai';

View File

@@ -5,8 +5,6 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser'; import { parseCategory } from '@/application/Parser/CategoryParser';
declare var process;
describe('ApplicationParser', () => { describe('ApplicationParser', () => {
describe('parseApplication', () => { describe('parseApplication', () => {
it('can parse current application file', () => { it('can parse current application file', () => {
@@ -16,74 +14,64 @@ describe('ApplicationParser', () => {
expect(() => parseApplication(undefined)).to.throw('application is null or undefined'); expect(() => parseApplication(undefined)).to.throw('application is null or undefined');
}); });
it('throws when undefined actions', () => { it('throws when undefined actions', () => {
const sut: ApplicationYaml = { const sut: ApplicationYaml = { actions: undefined };
name: 'test',
repositoryUrl: 'https://privacy.sexy',
actions: undefined,
};
expect(() => parseApplication(sut)).to.throw('application does not define any action'); expect(() => parseApplication(sut)).to.throw('application does not define any action');
}); });
it('throws when has no actions', () => { it('throws when has no actions', () => {
const sut: ApplicationYaml = { const sut: ApplicationYaml = { actions: [] };
name: 'test',
repositoryUrl: 'https://privacy.sexy',
actions: [],
};
expect(() => parseApplication(sut)).to.throw('application does not define any action'); expect(() => parseApplication(sut)).to.throw('application does not define any action');
}); });
it('returns expected name', () => { describe('information', () => {
// arrange it('returns expected repository version', () => {
const expected = 'test-app-name'; // arrange
const sut: ApplicationYaml = { const expected = 'expected-version';
name: expected, const env = getProcessEnvironmentStub();
repositoryUrl: 'https://privacy.sexy', env.VUE_APP_VERSION = expected;
actions: [ getTestCategory() ], const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
}; // act
// act const actual = parseApplication(sut, env).info.version;
const actual = parseApplication(sut).name; // assert
// assert expect(actual).to.be.equal(expected);
expect(actual).to.be.equal(actual); });
}); it('returns expected repository url', () => {
it('returns expected repository url', () => { // arrange
// arrange const expected = 'https://expected-repository.url';
const expected = 'https://privacy.sexy'; const env = getProcessEnvironmentStub();
const sut: ApplicationYaml = { env.VUE_APP_REPOSITORY_URL = expected;
name: 'name', const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
repositoryUrl: expected, // act
actions: [ getTestCategory() ], const actual = parseApplication(sut, env).info.repositoryUrl;
}; // assert
// act expect(actual).to.be.equal(expected);
const actual = parseApplication(sut).repositoryUrl; });
// assert it('returns expected name', () => {
expect(actual).to.be.equal(actual); // arrange
}); const expected = 'expected-app-name';
it('returns expected repository version', () => { const env = getProcessEnvironmentStub();
// arrange env.VUE_APP_NAME = expected;
const expected = '1.0.0'; const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
process = { // act
env: { const actual = parseApplication(sut, env).info.name;
VUE_APP_VERSION: expected, // assert
}, expect(actual).to.be.equal(expected);
}; });
const sut: ApplicationYaml = { it('returns expected homepage url', () => {
name: 'name', // arrange
repositoryUrl: 'https://privacy.sexy', const expected = 'https://expected.sexy';
actions: [ getTestCategory() ], const env = getProcessEnvironmentStub();
}; env.VUE_APP_HOMEPAGE_URL = expected;
// act const sut: ApplicationYaml = { actions: [ getTestCategory() ] };
const actual = parseApplication(sut).version; // act
// assert const actual = parseApplication(sut, env).info.homepage;
expect(actual).to.be.equal(actual); // assert
expect(actual).to.be.equal(expected);
});
}); });
it('parses actions', () => { it('parses actions', () => {
// arrange // arrange
const actions = [ getTestCategory('test1'), getTestCategory('test2') ]; const actions = [ getTestCategory('test1'), getTestCategory('test2') ];
const expected = [ parseCategory(actions[0]), parseCategory(actions[1]) ]; const expected = [ parseCategory(actions[0]), parseCategory(actions[1]) ];
const sut: ApplicationYaml = { const sut: ApplicationYaml = { actions };
name: 'name',
repositoryUrl: 'https://privacy.sexy',
actions,
};
// act // act
const actual = parseApplication(sut).actions; const actual = parseApplication(sut).actions;
// assert // assert
@@ -113,3 +101,12 @@ function getTestScript(scriptName: string): YamlScript {
recommend: true, recommend: true,
}; };
} }
function getProcessEnvironmentStub(): NodeJS.ProcessEnv {
return {
VUE_APP_VERSION: 'stub-version',
VUE_APP_NAME: 'stub-name',
VUE_APP_REPOSITORY_URL: 'stub-repository-url',
VUE_APP_HOMEPAGE_URL: 'stub-homepage-url',
};
}

View File

@@ -7,129 +7,156 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
describe('UserFilter', () => { describe('UserFilter', () => {
it('signals when removing filter', () => { describe('removeFilter', () => {
// arrange it('signals when removing filter', () => {
let isCalled = false;
const sut = new UserFilter(new ApplicationStub());
sut.filterRemoved.on(() => isCalled = true);
// act
sut.removeFilter();
// assert
expect(isCalled).to.be.equal(true);
});
it('signals when no matches', () => {
// arrange
let actual: IFilterResult;
const nonMatchingFilter = 'non matching filter';
const sut = new UserFilter(new ApplicationStub());
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(nonMatchingFilter);
// assert
expect(actual.hasAnyMatches()).be.equal(false);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(0);
expect(actual.query).to.equal(nonMatchingFilter);
});
describe('signals when script matches', () => {
it('code matches', () => {
// arrange // arrange
const code = 'HELLO world'; let isCalled = false;
const filter = 'Hello WoRLD'; const sut = new UserFilter(new ApplicationStub());
let actual: IFilterResult; sut.filterRemoved.on(() => isCalled = true);
const script = new ScriptStub('id').withCode(code);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act // act
sut.setFilter(filter); sut.removeFilter();
// assert // assert
expect(actual.hasAnyMatches()).be.equal(true); expect(isCalled).to.be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
}); });
it('revertCode matches', () => { it('currentFilter is undefined', () => {
// arrange // arrange
const revertCode = 'HELLO world'; const sut = new UserFilter(new ApplicationStub());
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withRevertCode(revertCode);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act // act
sut.setFilter(filter); sut.removeFilter();
// assert // assert
expect(actual.hasAnyMatches()).be.equal(true); expect(sut.currentFilter).to.be.equal(undefined);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
});
it('name matches', () => {
// arrange
const name = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withName(name);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
}); });
}); });
it('signals when category matches', () => { describe('setFilter', () => {
// arrange it('signals when no matches', () => {
const categoryName = 'HELLO world'; // arrange
const filter = 'Hello WoRLD'; let actual: IFilterResult;
let actual: IFilterResult; const nonMatchingFilter = 'non matching filter';
const category = new CategoryStub(55).withName(categoryName); const sut = new UserFilter(new ApplicationStub());
const sut = new UserFilter(new ApplicationStub() sut.filtered.on((filterResult) => actual = filterResult);
.withAction(category)); // act
sut.filtered.on((filterResult) => actual = filterResult); sut.setFilter(nonMatchingFilter);
// act // assert
sut.setFilter(filter); expect(actual.hasAnyMatches()).be.equal(false);
// assert expect(actual.query).to.equal(nonMatchingFilter);
expect(actual.hasAnyMatches()).be.equal(true); });
expect(actual.categoryMatches).to.have.lengthOf(1); it('sets currentFilter as expected when no matches', () => {
expect(actual.categoryMatches[0]).to.deep.equal(category); // arrange
expect(actual.scriptMatches).to.have.lengthOf(0); const nonMatchingFilter = 'non matching filter';
expect(actual.query).to.equal(filter); const sut = new UserFilter(new ApplicationStub());
}); // act
it('signals when category and script matches', () => { sut.setFilter(nonMatchingFilter);
// arrange // assert
const matchingText = 'HELLO world'; const actual = sut.currentFilter;
const filter = 'Hello WoRLD'; expect(actual.hasAnyMatches()).be.equal(false);
let actual: IFilterResult; expect(actual.query).to.equal(nonMatchingFilter);
const script = new ScriptStub('script') });
.withName(matchingText); describe('signals when script matches', () => {
const category = new CategoryStub(55) it('code matches', () => {
.withName(matchingText) // arrange
.withScript(script); const code = 'HELLO world';
const app = new ApplicationStub() const filter = 'Hello WoRLD';
.withAction(category); let actual: IFilterResult;
const sut = new UserFilter(app); const script = new ScriptStub('id').withCode(code);
sut.filtered.on((filterResult) => actual = filterResult); const category = new CategoryStub(33).withScript(script);
// act const sut = new UserFilter(new ApplicationStub()
sut.setFilter(filter); .withAction(category));
// assert sut.filtered.on((filterResult) => actual = filterResult);
expect(actual.hasAnyMatches()).be.equal(true); // act
expect(actual.categoryMatches).to.have.lengthOf(1); sut.setFilter(filter);
expect(actual.categoryMatches[0]).to.deep.equal(category); // assert
expect(actual.scriptMatches).to.have.lengthOf(1); expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.scriptMatches[0]).to.deep.equal(script); expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.query).to.equal(filter); expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('revertCode matches', () => {
// arrange
const revertCode = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withRevertCode(revertCode);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('name matches', () => {
// arrange
const name = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withName(name);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('signals when category matches', () => {
// arrange
const categoryName = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const category = new CategoryStub(55).withName(categoryName);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(1);
expect(actual.categoryMatches[0]).to.deep.equal(category);
expect(actual.scriptMatches).to.have.lengthOf(0);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
it('signals when category and script matches', () => {
// arrange
const matchingText = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('script')
.withName(matchingText);
const category = new CategoryStub(55)
.withName(matchingText)
.withScript(script);
const app = new ApplicationStub()
.withAction(category);
const sut = new UserFilter(app);
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(1);
expect(actual.categoryMatches[0]).to.deep.equal(category);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
expect(sut.currentFilter).to.deep.equal(actual);
});
});
}); });
}); });

View File

@@ -1,8 +1,10 @@
import { ScriptStub } from './../stubs/ScriptStub'; import { ScriptStub } from './../stubs/ScriptStub';
import { CategoryStub } from './../stubs/CategoryStub'; import { CategoryStub } from './../stubs/CategoryStub';
import { Application } from './../../../src/domain/Application'; import { Application } from '@/domain/Application';
import 'mocha'; import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { IProjectInformation } from '@/domain/IProjectInformation';
describe('Application', () => { describe('Application', () => {
it('getRecommendedScripts returns as expected', () => { it('getRecommendedScripts returns as expected', () => {
@@ -11,53 +13,56 @@ describe('Application', () => {
new ScriptStub('S3').withIsRecommended(true), new ScriptStub('S3').withIsRecommended(true),
new ScriptStub('S4').withIsRecommended(true), new ScriptStub('S4').withIsRecommended(true),
]; ];
const sut = new Application('name', 'repo', '0.1.0', [ const sut = new Application(createInformation(), [
new CategoryStub(3).withScripts(expected[0], new ScriptStub('S1').withIsRecommended(false)), new CategoryStub(3).withScripts(expected[0], new ScriptStub('S1').withIsRecommended(false)),
new CategoryStub(2).withScripts(expected[1], new ScriptStub('S2').withIsRecommended(false)), new CategoryStub(2).withScripts(expected[1], new ScriptStub('S2').withIsRecommended(false)),
]); ]);
// act // act
const actual = sut.getRecommendedScripts(); const actual = sut.getRecommendedScripts();
// assert // assert
expect(expected[0]).to.deep.equal(actual[0]); expect(expected[0]).to.deep.equal(actual[0]);
expect(expected[1]).to.deep.equal(actual[1]); expect(expected[1]).to.deep.equal(actual[1]);
}); });
it('cannot construct without categories', () => { describe('parameter validation', () => {
// arrange it('cannot construct without categories', () => {
const categories = []; // arrange
const categories = [];
// act // act
function construct() { return new Application('name', 'repo', '0.1.0', categories); } function construct() { return new Application(createInformation(), categories); }
// assert
// assert expect(construct).to.throw('Application must consist of at least one category');
expect(construct).to.throw('Application must consist of at least one category'); });
}); it('cannot construct without scripts', () => {
it('cannot construct without scripts', () => { // arrange
// arrange const categories = [
const categories = [ new CategoryStub(3),
new CategoryStub(3), new CategoryStub(2),
new CategoryStub(2), ];
]; // act
function construct() { return new Application(createInformation(), categories); }
// act // assert
function construct() { return new Application('name', 'repo', '0.1.0', categories); } expect(construct).to.throw('Application must consist of at least one script');
});
// assert it('cannot construct without any recommended scripts', () => {
expect(construct).to.throw('Application must consist of at least one script'); // arrange
}); const categories = [
it('cannot construct without any recommended scripts', () => { new CategoryStub(3).withScripts(new ScriptStub('S1').withIsRecommended(false)),
// arrange new CategoryStub(2).withScripts(new ScriptStub('S2').withIsRecommended(false)),
const categories = [ ];
new CategoryStub(3).withScripts(new ScriptStub('S1').withIsRecommended(false)), // act
new CategoryStub(2).withScripts(new ScriptStub('S2').withIsRecommended(false)), function construct() { return new Application(createInformation(), categories); }
]; // assert
expect(construct).to.throw('Application must consist of at least one recommended script');
// act });
function construct() { return new Application('name', 'repo', '0.1.0', categories); } it('cannot construct without information', () => {
// arrange
// assert const categories = [new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true))];
expect(construct).to.throw('Application must consist of at least one recommended script'); const information = undefined;
// act
function construct() { return new Application(information, categories); }
// assert
expect(construct).to.throw('info is undefined');
});
}); });
it('totalScripts counts right', () => { it('totalScripts counts right', () => {
// arrange // arrange
@@ -67,9 +72,9 @@ describe('Application', () => {
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))), new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
]; ];
// act // act
const application = new Application('name', 'repo', '0.1.0', categories); const sut = new Application(createInformation(), categories);
// assert // assert
expect(application.totalScripts).to.equal(4); expect(sut.totalScripts).to.equal(4);
}); });
it('totalCategories counts right', () => { it('totalCategories counts right', () => {
// arrange // arrange
@@ -79,8 +84,22 @@ describe('Application', () => {
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))), new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
]; ];
// act // act
const application = new Application('name', 'repo', '0.1.0', categories); const sut = new Application(createInformation(), categories);
// assert // assert
expect(application.totalCategories).to.equal(4); expect(sut.totalCategories).to.equal(4);
});
it('sets information as expected', () => {
// arrange
const expected = createInformation();
// act
const sut = new Application(
expected,
[new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true))]);
// assert
expect(sut.info).to.deep.equal(expected);
}); });
}); });
function createInformation(): IProjectInformation {
return new ProjectInformation('name', 'repo', '0.1.0', 'homepage');
}

View File

@@ -0,0 +1,128 @@
import 'mocha';
import { expect } from 'chai';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { OperatingSystem } from '@/domain/OperatingSystem';
describe('ProjectInformation', () => {
it('sets name as expected', () => {
// arrange
const expected = 'expected-name';
const sut = new ProjectInformation(expected, 'version', 'repositoryUrl', 'homepage');
// act
const actual = sut.name;
// assert
expect(actual).to.equal(expected);
});
it('sets version as expected', () => {
// arrange
const expected = 'expected-version';
const sut = new ProjectInformation('name', expected, 'repositoryUrl', 'homepage');
// act
const actual = sut.version;
// assert
expect(actual).to.equal(expected);
});
it('sets repositoryUrl as expected', () => {
// arrange
const expected = 'expected-repository-url';
const sut = new ProjectInformation('name', 'version', 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', () => {
// arrange
const expected = 'expected-repository-url';
const sut = new ProjectInformation('name', 'version', 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', 'version', `${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', 'version', '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', 'version', 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 = '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 = '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 = '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 = '0.7.2';
const sut = new ProjectInformation('name', version, repositoryUrl, 'homepage');
// act
const actual = sut.getDownloadUrl(OperatingSystem.Windows);
// assert
expect(actual).to.equal(expected);
});
it('throws when OS is unknown', () => {
// arrange
const sut = new ProjectInformation('name', 'version', 'repositoryUrl', 'homepage');
const os = OperatingSystem.Unknown;
// act
const act = () => sut.getDownloadUrl(os);
// assert
expect(act).to.throw(`Unsupported os: ${OperatingSystem[os]}`);
});
});
});

View File

@@ -68,7 +68,7 @@ describe('ScriptReverter', () => {
selection: [ new SelectedScript(script, true)], revert: true, expectRevert: true, selection: [ new SelectedScript(script, true)], revert: true, expectRevert: true,
}, },
{ {
name: 'keeps revert state deselected when already selected wtih non revert state', name: 'keeps revert state deselected when already selected with non revert state',
selection: [ new SelectedScript(script, false)], revert: false, expectRevert: false, selection: [ new SelectedScript(script, false)], revert: false, expectRevert: false,
}, },
]; ];

View File

@@ -1,11 +1,10 @@
import { IApplication, ICategory, IScript } from '@/domain/IApplication'; import { IApplication, ICategory, IScript } from '@/domain/IApplication';
import { ProjectInformation } from '@/domain/ProjectInformation';
export class ApplicationStub implements IApplication { export class ApplicationStub implements IApplication {
public totalScripts = 0; public totalScripts = 0;
public totalCategories = 0; public totalCategories = 0;
public readonly name = 'StubApplication'; public readonly info = new ProjectInformation('StubApplication', '0.1.0', 'https://github.com/undergroundwires/privacy.sexy', 'https://privacy.sexy');
public readonly repositoryUrl = 'https://privacy.sexy';
public readonly version = '0.1.0';
public readonly actions = new Array<ICategory>(); public readonly actions = new Array<ICategory>();
public withAction(category: ICategory): ApplicationStub { public withAction(category: ICategory): ApplicationStub {

View File

@@ -1,4 +1,9 @@
process.env.VUE_APP_VERSION = require('./package.json').version; const packageJson = require('./package.json');
process.env.VUE_APP_VERSION = packageJson.version;
process.env.VUE_APP_NAME = packageJson.name;
process.env.VUE_APP_REPOSITORY_URL = packageJson.repository.url;
process.env.VUE_APP_HOMEPAGE_URL = packageJson.homepage;
module.exports = { module.exports = {
pluginOptions: { pluginOptions: {