├── src ├── assets │ ├── .gitkeep │ └── images │ │ ├── diagram.png │ │ ├── disguise_icon_1.png │ │ ├── disguise_icon_2.png │ │ ├── disguise_logo_1.png │ │ └── disguise_logo_2.png ├── favicon.ico ├── app │ ├── about │ │ ├── about.component.sass │ │ ├── about.component.ts │ │ └── about.component.html │ ├── shared │ │ ├── constants.ts │ │ ├── dialog.component.html │ │ ├── dialog.component.sass │ │ ├── timestamp.pipe.ts │ │ ├── download-dialog.component.html │ │ ├── dialog.component.ts │ │ ├── interfaces.ts │ │ ├── upload-dialog.component.html │ │ ├── upload-dialog.component.sass │ │ ├── download-dialog.component.sass │ │ ├── filter-dialog.component.sass │ │ ├── download-dialog.component.ts │ │ ├── shared.module.ts │ │ ├── upload-dialog.component.ts │ │ ├── add-stub-dialog.component.sass │ │ ├── dialog.service.ts │ │ ├── filter-dialog.component.html │ │ ├── filter-dialog.component.ts │ │ └── add-stub-dialog.component.ts │ ├── app.component.ts │ ├── logs │ │ ├── log.service.ts │ │ ├── log.module.ts │ │ ├── log.service.spec.ts │ │ ├── log.component.html │ │ ├── log.component.sass │ │ ├── log.component.spec.ts │ │ └── log.component.ts │ ├── imposters │ │ ├── imposter-list.component.sass │ │ ├── imposter.module.ts │ │ ├── imposter-add.component.sass │ │ ├── imposter.service.ts │ │ ├── imposter-add.component.ts │ │ ├── imposter-add.component.spec.ts │ │ ├── imposter-detail.component.spec.ts │ │ ├── imposter-detail.component.sass │ │ ├── imposter-list.component.html │ │ ├── imposter-list.component.ts │ │ ├── imposter-list.component.spec.ts │ │ ├── imposter-add.component.html │ │ ├── imposter.service.spec.ts │ │ ├── imposter-detail.component.ts │ │ └── imposter-detail.component.html │ ├── app.component.spec.ts │ ├── app.component.sass │ ├── app.component.html │ └── app.module.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── proxy.conf.json ├── index.html ├── main.ts ├── test.ts ├── styles.sass └── polyfills.ts ├── docs ├── favicon.ico ├── assets │ └── images │ │ ├── disguise_icon_1.png │ │ ├── disguise_icon_2.png │ │ ├── disguise_logo_1.png │ │ └── disguise_logo_2.png ├── 404.html ├── index.html ├── runtime-es2015.0811dcefd377500b5b1a.js ├── runtime-es5.0811dcefd377500b5b1a.js └── 3rdpartylicenses.txt ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── tsconfig.json ├── LICENSE ├── .gitignore ├── karma.conf.js ├── package.json ├── tslint.json ├── README.md └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /src/app/about/about.component.sass: -------------------------------------------------------------------------------- 1 | @import "../../styles.sass" 2 | 3 | .text 4 | color: white -------------------------------------------------------------------------------- /src/app/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | public static mb = '/api'; 3 | } 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/src/assets/images/diagram.png -------------------------------------------------------------------------------- /src/assets/images/disguise_icon_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/src/assets/images/disguise_icon_1.png -------------------------------------------------------------------------------- /src/assets/images/disguise_icon_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/src/assets/images/disguise_icon_2.png -------------------------------------------------------------------------------- /src/assets/images/disguise_logo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/src/assets/images/disguise_logo_1.png -------------------------------------------------------------------------------- /src/assets/images/disguise_logo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/src/assets/images/disguise_logo_2.png -------------------------------------------------------------------------------- /docs/assets/images/disguise_icon_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/docs/assets/images/disguise_icon_1.png -------------------------------------------------------------------------------- /docs/assets/images/disguise_icon_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/docs/assets/images/disguise_icon_2.png -------------------------------------------------------------------------------- /docs/assets/images/disguise_logo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/docs/assets/images/disguise_logo_1.png -------------------------------------------------------------------------------- /docs/assets/images/disguise_logo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Opus-Software/disguise/HEAD/docs/assets/images/disguise_logo_2.png -------------------------------------------------------------------------------- /src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:2525", 4 | "secure": false, 5 | "changeOrigin": true, 6 | "logLevel": "debug", 7 | "pathRewrite": { 8 | "^/api": "" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './about.component.html', 5 | styleUrls: ['./about.component.sass'] 6 | }) 7 | 8 | export class AboutComponent { 9 | 10 | constructor() { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.sass'] 7 | }) 8 | export class AppComponent { 9 | pageTitle = 'Disguise'; 10 | } 11 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [ 6 | "node" 7 | ] 8 | }, 9 | "files": [ 10 | "src/main.ts", 11 | "src/polyfills.ts" 12 | ], 13 | "include": [ 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disguise 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/shared/dialog.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | {{ message }} 6 |
7 |
8 | 9 | 10 |
-------------------------------------------------------------------------------- /src/app/logs/log.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Constants } from '../shared/constants'; 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | 8 | export class LogService { 9 | 10 | constructor(private http: HttpClient) {} 11 | 12 | getLog() { 13 | return this.http.get(`${Constants.mb}/logs`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/app/shared/dialog.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | ::ng-deep .confirm-dialog .modal-content 4 | background-color: white 5 | color: $black1 6 | font-size: 13px 7 | border-radius: 10px 8 | .mBody 9 | padding-top: 0 10 | padding-bottom: 0 11 | padding-left: 1em 12 | padding-right: 1em 13 | .mHeader 14 | padding: 1em 15 | .mFooter 16 | padding-bottom: 1em 17 | .btn 18 | color: $black1 19 | .red 20 | color: $red -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /src/app/shared/timestamp.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { DatePipe } from '@angular/common'; 3 | 4 | @Pipe({name: 'timestamp'}) 5 | export class TimestampPipe implements PipeTransform { 6 | constructor(private datePipe: DatePipe) { } 7 | transform(value: string): string { 8 | if (!value) { 9 | return ''; 10 | } 11 | return this.datePipe.transform(new Date(value), 'yyyy-MM-dd T HH:mm:ss'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/download-dialog.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | {{ message }} 6 | 7 |
8 |
9 | 10 | 11 |
-------------------------------------------------------------------------------- /src/app/logs/log.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared/shared.module'; 3 | import { LogComponent } from './log.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { DatePipe } from '@angular/common'; 6 | @NgModule({ 7 | declarations: [ 8 | LogComponent 9 | ], 10 | imports: [ 11 | RouterModule.forChild([ 12 | { path: 'logs', component: LogComponent } 13 | ]), 14 | SharedModule 15 | ], 16 | providers: [DatePipe] 17 | }) 18 | export class LogModule { } 19 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-list.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | .card-header 4 | margin-top: .85em 5 | .table-cell 6 | font-size: 14px 7 | background-color: $black2 8 | .row 9 | display: flex 10 | margin: 0 11 | .col1 12 | flex: 3 13 | .col2 14 | flex: 1 15 | .btn 16 | padding: .5em 17 | .text-btn 18 | color: white 19 | margin-left: .2em 20 | &:hover 21 | color: $main-color 22 | .tCol1 23 | width: 10% 24 | .tCol2 25 | width: 15% 26 | .tCol3 27 | width: 65% 28 | .tCol4 29 | width: 10% -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "fullTemplateTypeCheck": true, 21 | "strictInjectionParameters": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | imports: [RouterTestingModule] 12 | }).compileComponents(); 13 | })); 14 | 15 | it('should create the app', () => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.componentInstance; 18 | expect(app).toBeTruthy(); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/app.component.sass: -------------------------------------------------------------------------------- 1 | @import "../styles.sass" 2 | 3 | .navbar 4 | background-color: $main-color 5 | color: white 6 | .navbar-brand 7 | width: 70px 8 | .logo 9 | width: 100% 10 | .navbar-nav 11 | flex-direction: row 12 | padding: 0 13 | margin-top: auto 14 | .nav-item 15 | cursor: pointer 16 | font-size: 1.2em 17 | margin-right: 1em 18 | margin-left: .5em 19 | padding-bottom: 0 20 | .active 21 | font-weight: bold 22 | .main-container 23 | min-height: 100vh 24 | background-color: $black1 25 | color: $main-color 26 | .footer 27 | background-color: $main-color 28 | > .footer-logo > .-logo 29 | width: 5% 30 | margin: 1em 31 | -------------------------------------------------------------------------------- /src/app/shared/dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 3 | 4 | @Component({ 5 | selector: 'app-dialog', 6 | templateUrl: './dialog.component.html', 7 | styleUrls: ['./dialog.component.sass'] 8 | }) 9 | export class DialogComponent { 10 | 11 | @Input() title: string; 12 | @Input() message: string; 13 | @Input() btnOkText: string; 14 | @Input() btnCancelText: string; 15 | 16 | constructor(private activeModal: NgbActiveModal) { } 17 | 18 | public decline() { 19 | this.activeModal.dismiss(); 20 | } 21 | 22 | public accept() { 23 | this.activeModal.close(true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Response { 2 | statusCode?: string; 3 | headers?: string[][]; 4 | body?: string; 5 | wait?: string; 6 | decorate?: string; 7 | repeat?: string; 8 | shellTransform?: string; 9 | copy?: string; 10 | lookup?: string; 11 | bodyType?: string; 12 | } 13 | 14 | export interface Predicate { 15 | equals?: string; 16 | deepEquals?: string; 17 | contains?: string; 18 | startsWith?: string; 19 | endsWith?: string; 20 | matches?: string; 21 | exists?: string; 22 | not?: string; 23 | or?: string; 24 | and?: string; 25 | xpath?: string; 26 | jsonpath?: string; 27 | inject?: string; 28 | caseSensitive?: boolean; 29 | } 30 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('disguise app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 17 |
18 | 19 |
-------------------------------------------------------------------------------- /src/app/shared/upload-dialog.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | {{ message }} 6 |
7 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 |
-------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/app/shared/upload-dialog.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | ::ng-deep .upload-modal .modal-content 4 | background-color: white 5 | color: $black1 6 | font-size: 13px 7 | border-radius: 10px 8 | .mBody 9 | padding-top: 0 10 | padding-bottom: 0 11 | padding-left: 1em 12 | padding-right: 1em 13 | .mHeader 14 | padding: 1em 15 | .mFooter 16 | padding-bottom: 1em 17 | .msg 18 | margin-top: .5em 19 | .btn 20 | color: $black1 21 | &:disabled 22 | color: $black2 23 | cursor: default 24 | .file-input 25 | margin-top: 1em 26 | color: white 27 | .type-file 28 | display: none 29 | .file-btn 30 | background-color: $black1 31 | padding: .8em 32 | border-radius: 10px 33 | margin-right: .5em 34 | .filename 35 | border: none 36 | color: $black1 37 | .green 38 | color: $green -------------------------------------------------------------------------------- /src/app/shared/download-dialog.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | ::ng-deep .download-modal .modal-content 4 | background-color: white 5 | color: $black1 6 | font-size: 13px 7 | border-radius: 10px 8 | .mBody 9 | padding-top: 0 10 | padding-bottom: 0 11 | padding-left: 1em 12 | padding-right: 1em 13 | .mHeader 14 | padding: 1em 15 | .mFooter 16 | padding-bottom: 1em 17 | .msg 18 | margin-bottom: .5em 19 | .btn 20 | color: $black1 21 | &:disabled 22 | color: $black2 23 | cursor: default 24 | .txtA 25 | width: 100% 26 | padding: .375rem .75rem 27 | line-height: 2 28 | border: 2px solid white 29 | margin-top: 1em 30 | border-radius: 10px 31 | background-color: $black1 32 | color: white 33 | &:focus 34 | background-color: $black1 35 | color: white 36 | outline: none 37 | .green 38 | color: $green -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disguise 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disguise 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/imposters/imposter.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared/shared.module'; 3 | import { ImposterListComponent } from './imposter-list.component'; 4 | import { ImposterDetailComponent } from './imposter-detail.component'; 5 | import { ImposterAddComponent } from './imposter-add.component'; 6 | import { RouterModule } from '@angular/router'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | @NgModule({ 9 | declarations: [ 10 | ImposterListComponent, 11 | ImposterDetailComponent, 12 | ImposterAddComponent 13 | ], 14 | imports: [ 15 | RouterModule.forChild([ 16 | { path: 'imposters', component: ImposterListComponent }, 17 | { path: 'imposters/:port', component: ImposterDetailComponent }, 18 | { path: 'newImposter', component: ImposterAddComponent } 19 | ]), 20 | SharedModule, 21 | ReactiveFormsModule 22 | ] 23 | }) 24 | 25 | export class ImposterModule { } 26 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { AppComponent } from './app.component'; 5 | import { SharedModule } from './shared/shared.module'; 6 | import { LogModule } from './logs/log.module'; 7 | import { ImposterModule } from './imposters/imposter.module'; 8 | import { HttpClientModule } from '@angular/common/http'; 9 | import { AboutComponent } from './about/about.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | SharedModule, 18 | HttpClientModule, 19 | RouterModule.forRoot([ 20 | { path: 'about', component: AboutComponent}, 21 | { path: '', redirectTo: 'imposters', pathMatch: 'full' }, 22 | { path: '**', redirectTo: 'imposters', pathMatch: 'full' } 23 | ], { onSameUrlNavigation: 'reload' }), 24 | LogModule, 25 | ImposterModule 26 | ], 27 | bootstrap: [AppComponent] 28 | }) 29 | export class AppModule { } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Opus Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin-debug/ 3 | bin-release/ 4 | [Oo]bj/ 5 | [Bb]in/ 6 | 7 | # Other files and folders 8 | .settings/ 9 | 10 | # Executables 11 | *.swf 12 | *.air 13 | *.ipa 14 | *.apk 15 | 16 | # dependencies 17 | /node_modules 18 | 19 | # compiled output 20 | /dist 21 | /tmp 22 | /out-tsc 23 | # Only exists if Bazel was run 24 | /bazel-out 25 | 26 | # profiling files 27 | chrome-profiler-events*.json 28 | speed-measure-plugin*.json 29 | 30 | # IDEs and editors 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | .history/* 46 | 47 | # misc 48 | /.sass-cache 49 | /connect.lock 50 | /coverage 51 | /libpeerconnection.log 52 | npm-debug.log 53 | yarn-error.log 54 | testem.log 55 | /typings 56 | 57 | # System Files 58 | .DS_Store 59 | Thumbs.db 60 | 61 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` 62 | # should NOT be excluded as they contain compiler settings and other important 63 | # information for Eclipse / Flash Builder. -------------------------------------------------------------------------------- /src/app/imposters/imposter-add.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | .card-body 4 | color: $white 5 | .return 6 | padding-left: 1.25rem 7 | padding-right: 1.25rem 8 | margin-top: 1.5em 9 | color: white 10 | font-size: 12px 11 | .return-link 12 | cursor: pointer 13 | .btn 14 | border-color: white 15 | border-width: 1px 16 | color: white 17 | font-size: 13px 18 | margin: 1em 19 | &:hover 20 | color: white 21 | &.no-border 22 | border: none 23 | color: $main-color 24 | &.no-margin 25 | margin: 0 26 | .active 27 | background-color: $main-color 28 | .txtA 29 | background-color: $black1 30 | color: white 31 | &:focus 32 | background-color: $black1 33 | color: white 34 | &:read-only 35 | background-color: $black1 36 | color: white 37 | input:-webkit-autofill 38 | -webkit-box-shadow: 0 0 0 30px $black1 inset !important 39 | -webkit-text-fill-color: white !important 40 | &:hover, &:focus, &:active 41 | -webkit-box-shadow: 0 0 0 30px $black1 inset !important 42 | -webkit-text-fill-color: white !important 43 | .txtA::-webkit-scrollbar 44 | background-color: $black2 45 | width: 12px 46 | .txtA::-webkit-scrollbar-thumb 47 | background-color: $main-color 48 | border-radius: 10px 49 | .txtA::-webkit-scrollbar-corner 50 | background-color: $black2 -------------------------------------------------------------------------------- /src/styles.sass: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "../node_modules/bootstrap/scss/bootstrap.scss" 3 | 4 | $main-color1: #66fcf1 5 | $main-color: #45a29e 6 | $black1: #212529 7 | $black2: #2b2f33 8 | $grey: #c5c6c8 9 | $red: #991c1c 10 | $green: #2ca12c 11 | 12 | .btn 13 | color: $main-color 14 | &:hover 15 | color: $main-color 16 | .table 17 | color: $grey 18 | .table-cell 19 | background-color: $black2 20 | .card 21 | background-color: $black1 22 | margin-left: 1em 23 | margin-right: 1em 24 | border: none 25 | .card-header 26 | color: white 27 | border: none 28 | .card-body 29 | border: none 30 | ::ng-deep .pagination .page-link 31 | background-color: $black1 32 | color: white 33 | border: none 34 | &:hover 35 | cursor: pointer 36 | background-color: $black1 37 | ::ng-deep .pagination 38 | > .page-item.active .page-link 39 | color: $main-color 40 | background-color: $black1 41 | border: none 42 | > .page-item.disabled .page-link 43 | color: $grey 44 | background-color: $black1 45 | .list-group-item 46 | background-color: $black1 47 | color: $grey 48 | border-color: $grey 49 | .btn-tooltip .tooltip-inner 50 | background-color: $main-color 51 | color: $black1 52 | .invalid-field 53 | font-style: italic 54 | color: $red 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | customLaunchers: { 16 | ChromeHeadless: { 17 | base: 'Chrome', 18 | flags: [ 19 | '--headless', 20 | '--disable-gpu', 21 | '--no-sandbox', 22 | '--remote-debugging-port=9222' 23 | ] 24 | } 25 | }, 26 | client: { 27 | clearContext: false // leave Jasmine Spec Runner output visible in browser 28 | }, 29 | coverageIstanbulReporter: { 30 | dir: require('path').join(__dirname, './coverage/disguise'), 31 | reports: ['html', 'lcovonly', 'text-summary'], 32 | fixWebpackSourcePaths: true 33 | }, 34 | reporters: ['progress', 'kjhtml'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['ChromeHeadless'], 40 | singleRun: false, 41 | restartOnFileChange: true 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /docs/runtime-es2015.0811dcefd377500b5b1a.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c { 28 | const blob = new Blob([JSON.stringify(imposters, null, 2)], { type: 'text/json' }); 29 | const url = window.URL.createObjectURL(blob); 30 | const a = document.createElement('a'); 31 | document.body.appendChild(a); 32 | a.setAttribute('style', 'display: none'); 33 | a.href = url; 34 | a.download = this.filename.trim() ? this.filename + '.json' : 'config.json'; 35 | a.click(); 36 | window.URL.revokeObjectURL(url); 37 | a.remove(); 38 | this.activeModal.close(true); 39 | } 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 4 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | import { DialogComponent } from './dialog.component'; 7 | import { DialogService } from './dialog.service'; 8 | import { DownloadDialogComponent } from './download-dialog.component'; 9 | import { UploadDialogComponent } from './upload-dialog.component'; 10 | import { AddStubDialogComponent } from './add-stub-dialog.component'; 11 | import { FilterDialogComponent } from './filter-dialog.component'; 12 | import { TimestampPipe } from './timestamp.pipe'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | DialogComponent, 17 | DownloadDialogComponent, 18 | UploadDialogComponent, 19 | AddStubDialogComponent, 20 | FilterDialogComponent, 21 | TimestampPipe 22 | ], 23 | imports: [ 24 | CommonModule, 25 | FontAwesomeModule, 26 | NgbModule, 27 | FormsModule, 28 | ReactiveFormsModule 29 | ], 30 | exports: [ 31 | CommonModule, 32 | FontAwesomeModule, 33 | NgbModule, 34 | FormsModule, 35 | ReactiveFormsModule, 36 | TimestampPipe 37 | ], 38 | providers: [ DialogService ], 39 | entryComponents: [ 40 | DialogComponent, 41 | DownloadDialogComponent, 42 | UploadDialogComponent, 43 | AddStubDialogComponent, 44 | FilterDialogComponent 45 | ] 46 | }) 47 | 48 | export class SharedModule { } 49 | -------------------------------------------------------------------------------- /src/app/logs/log.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { LogService } from './log.service'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | import { Constants } from '../shared/constants'; 5 | 6 | describe('LogService', () => { 7 | let logService: LogService; 8 | let httpMock: HttpTestingController; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [HttpClientTestingModule], 13 | providers: [LogService] 14 | }); 15 | logService = TestBed.inject(LogService); 16 | httpMock = TestBed.inject(HttpTestingController); 17 | }); 18 | 19 | afterEach(() => { 20 | httpMock.verify(); 21 | }); 22 | 23 | it('should create the log service', async(() => { 24 | expect(LogService).toBeTruthy(); 25 | })); 26 | 27 | describe('getLog()', () => { 28 | it('should return the list of logs', async(() => { 29 | const logs = { 30 | logs: [ 31 | { message: 'test', level: 'info', timestamp: '2020-05-14T00:00:00.744Z' } 32 | ]}; 33 | 34 | logService.getLog().subscribe( 35 | response => { 36 | expect(response['logs'].length).toBe(1); 37 | expect(response['logs']).toEqual([{ message: 'test', level: 'info', timestamp: '2020-05-14T00:00:00.744Z' }]); 38 | }, 39 | () => fail('should have succeded') 40 | ); 41 | const req = httpMock.expectOne(`${Constants.mb}/logs`); 42 | expect(req.request.method).toBe('GET'); 43 | req.flush(logs, { status: 200, statusText: 'OK' }); 44 | })); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/shared/upload-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 3 | import { ImposterService } from '../imposters/imposter.service'; 4 | 5 | @Component({ 6 | selector: 'app-dialog', 7 | templateUrl: './upload-dialog.component.html', 8 | styleUrls: ['./upload-dialog.component.sass'] 9 | }) 10 | export class UploadDialogComponent { 11 | 12 | @Input() title: string; 13 | @Input() message: string; 14 | @Input() btnOkText: string; 15 | @Input() btnCancelText: string; 16 | @Input() ports: string[]; 17 | 18 | constructor(private activeModal: NgbActiveModal, 19 | private imposterService: ImposterService) { } 20 | fileReader; 21 | selectedFile; 22 | portsInUse = []; 23 | filename = ''; 24 | 25 | onFileChanged(event): void { 26 | this.selectedFile = event.target.files[0]; 27 | this.fileReader = new FileReader(); 28 | this.fileReader.readAsText(this.selectedFile, 'UTF-8'); 29 | this.filename = this.selectedFile['name']; 30 | } 31 | 32 | public decline() { 33 | this.activeModal.dismiss(); 34 | } 35 | 36 | public accept() { 37 | const text = JSON.parse(this.fileReader.result); 38 | text['imposters'].forEach(imposter => { 39 | if (this.ports.includes(imposter.port)) { 40 | this.portsInUse.push(imposter.port); 41 | } else { 42 | this.imposterService.postImposters(imposter).subscribe({ 43 | next: () => {} 44 | }); 45 | } 46 | }); 47 | if (this.portsInUse.length > 0) { 48 | alert('These ports are already in use: ' + this.portsInUse); 49 | this.portsInUse = []; 50 | this.activeModal.dismiss(); 51 | } 52 | this.activeModal.close(true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disguise", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "npm run single-test && npm run lint && ng serve -o", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "single-test": "ng test --watch=false" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~9.0.6", 16 | "@angular/common": "~9.0.6", 17 | "@angular/compiler": "~9.0.6", 18 | "@angular/core": "~9.0.6", 19 | "@angular/forms": "~9.0.6", 20 | "@angular/localize": "^9.1.1", 21 | "@angular/platform-browser": "~9.0.6", 22 | "@angular/platform-browser-dynamic": "~9.0.6", 23 | "@angular/router": "~9.0.6", 24 | "@fortawesome/angular-fontawesome": "^0.6.1", 25 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 26 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 27 | "@ng-bootstrap/ng-bootstrap": "^6.0.2", 28 | "@types/node": "^12.11.1", 29 | "bootstrap": "^4.4.1", 30 | "font-awesome": "^4.7.0", 31 | "jquery": "^3.4.1", 32 | "lodash": "^4.17.15", 33 | "module-alias": "^2.2.2", 34 | "ngx-xml2json": "^1.0.2", 35 | "rxjs": "~6.5.4", 36 | "tslib": "^1.10.0", 37 | "zone.js": "~0.10.2" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/build-angular": "~0.900.6", 41 | "@angular/cli": "^9.0.7", 42 | "@angular/compiler-cli": "~9.0.6", 43 | "@angular/language-service": "~9.0.6", 44 | "@types/jasmine": "~3.5.0", 45 | "@types/jasminewd2": "~2.0.3", 46 | "@types/node": "^12.11.1", 47 | "codelyzer": "^5.1.2", 48 | "jasmine-core": "~3.5.0", 49 | "jasmine-spec-reporter": "~4.2.1", 50 | "karma": "~4.3.0", 51 | "karma-chrome-launcher": "~3.1.0", 52 | "karma-coverage-istanbul-reporter": "~2.1.0", 53 | "karma-jasmine": "~2.0.1", 54 | "karma-jasmine-html-reporter": "^1.4.2", 55 | "protractor": "~5.4.3", 56 | "ts-node": "~8.3.0", 57 | "tslint": "~5.18.0", 58 | "typescript": "~3.7.5" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true, 87 | "no-string-literal": false 88 | }, 89 | "rulesDirectory": [ 90 | "codelyzer" 91 | ] 92 | } -------------------------------------------------------------------------------- /src/app/imposters/imposter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Constants } from '../shared/constants'; 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class ImposterService { 9 | 10 | constructor(private http: HttpClient) {} 11 | 12 | getImposters(headers?: object): Observable { 13 | return this.http.get(`${Constants.mb}/imposters`, headers); 14 | } 15 | 16 | getImposter(port: number): Observable { 17 | return this.http.get(`${Constants.mb}/imposters/${port}`); 18 | } 19 | 20 | addStub(port: number, body: object): Observable { 21 | return this.http.post(`${Constants.mb}/imposters/${port}/stubs`, body); 22 | } 23 | 24 | postImposters(list: object, add?: boolean): Observable { 25 | if (add) { 26 | if (list['stubs']) { 27 | const json = JSON.parse(list['stubs']); 28 | if (Array.isArray(json)) { 29 | list['stubs'] = json; 30 | } else { 31 | list['stubs'] = [json]; 32 | } 33 | } 34 | if (list['defaultResponse']) { 35 | list['defaultResponse'] = this.parseField('defaultResponse', list); 36 | } 37 | if (list['endOfRequestResolver']) { 38 | list['endOfRequestResolver'] = this.parseField('endOfRequestResolver', list); 39 | } 40 | } 41 | return this.http.post(`${Constants.mb}/imposters`, list); 42 | } 43 | 44 | deleteImposters(port: number): Observable { 45 | return this.http.delete(`${Constants.mb}/imposters/${port}`); 46 | } 47 | 48 | putStub(port: number, index: number, body: object): Observable { 49 | return this.http.put(`${Constants.mb}/imposters/${port}/stubs/${index}`, body); 50 | } 51 | 52 | deleteStub(port: number, index: number): Observable { 53 | return this.http.delete(`${Constants.mb}/imposters/${port}/stubs/${index}`); 54 | } 55 | 56 | parseField(field: string, obj: object): any { 57 | const json = JSON.parse(obj[field]); 58 | if (Array.isArray(json)) { 59 | return json[0]; 60 | } else { 61 | return json; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-add.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ImposterService } from './imposter.service'; 3 | import { Router } from '@angular/router'; 4 | import { faCheck } from '@fortawesome/free-solid-svg-icons'; 5 | import { FormGroup, FormBuilder, Validators, ValidationErrors, FormControl } from '@angular/forms'; 6 | import { DialogService } from '../shared/dialog.service'; 7 | @Component({ 8 | templateUrl: './imposter-add.component.html', 9 | styleUrls: ['./imposter-add.component.sass'] 10 | }) 11 | ​ 12 | export class ImposterAddComponent implements OnInit { 13 | form: FormGroup; 14 | submitted = false; 15 | faCheck = faCheck; 16 | 17 | constructor(private imposterService: ImposterService, 18 | private fb: FormBuilder, 19 | private router: Router, 20 | private dialogService: DialogService) { } 21 | 22 | ngOnInit() { 23 | this.form = this.fb.group({ 24 | port: [null, [Validators.required, Validators.minLength(1)]], 25 | protocol: [null, [Validators.required, Validators.minLength(1)]], 26 | name: '', 27 | recordRequests: '', 28 | key: '', 29 | cert: '', 30 | mutualAuth: '', 31 | defaultResponse: [null, [this.validateJson]], 32 | stubs: [null, [this.validateJson]], 33 | endOfRequestResolver: [null, [this.validateJson]] 34 | }); 35 | } 36 | addingImposter(): void { 37 | this.imposterService.postImposters(this.form.value, true).subscribe({ 38 | next: () => this.return() 39 | }); 40 | } 41 | 42 | hasError(field: string): ValidationErrors { 43 | return this.form.get(field).errors; 44 | } 45 | 46 | onSubmit(): void { 47 | this.submitted = true; 48 | if (this.form.valid) { 49 | this.addingImposter(); 50 | } 51 | } 52 | 53 | onCancel(): void { 54 | this.dialogService.confirm('Discard changes', `Do you really want to discard your changes?`, 'DISCARD').then( 55 | ok => { 56 | if (ok) { 57 | this.return(); 58 | } 59 | } 60 | ); 61 | } 62 | 63 | validateJson(control: FormControl) { 64 | try { 65 | JSON.parse(control.value); 66 | } catch (e) { 67 | return { invalidJson: true }; 68 | } 69 | return null; 70 | } 71 | 72 | return(): void { 73 | this.router.navigate(['imposters']); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-add.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { ImposterService } from '../imposters/imposter.service'; 3 | import { ImposterAddComponent } from './imposter-add.component'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { Router } from '@angular/router'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { of } from 'rxjs'; 8 | import { FormBuilder } from '@angular/forms'; 9 | import { DialogService } from '../shared/dialog.service'; 10 | 11 | describe('StubDetailComponent', () => { 12 | let mockImposterService; 13 | 14 | beforeEach(async(() => { 15 | mockImposterService = jasmine.createSpyObj('mockImposterService', ['postImposters']); 16 | TestBed.configureTestingModule({ 17 | declarations: [ImposterAddComponent], 18 | providers: [ 19 | { provide: ImposterService, useValue: mockImposterService }, 20 | FormBuilder, 21 | DialogService 22 | ], 23 | imports: [RouterTestingModule.withRoutes([])], 24 | schemas: [NO_ERRORS_SCHEMA] 25 | }) 26 | .compileComponents(); 27 | })); 28 | 29 | function setup() { 30 | const fixture = TestBed.createComponent(ImposterAddComponent); 31 | const imposterAdd = fixture.debugElement.componentInstance; 32 | const navSpy = spyOn(TestBed.inject(Router), 'navigate'); 33 | return { fixture, imposterAdd, navSpy }; 34 | } 35 | 36 | it('should create the add imposter component', async(() => { 37 | const { imposterAdd } = setup(); 38 | expect(imposterAdd).toBeTruthy(); 39 | })); 40 | 41 | it('should send the post imposter request', async(() => { 42 | const { fixture, imposterAdd, navSpy } = setup(); 43 | mockImposterService.postImposters.and.returnValue(of({})); 44 | fixture.detectChanges(); 45 | imposterAdd.form.controls.port.setValue('123'); 46 | imposterAdd.form.controls.protocol.setValue('http'); 47 | imposterAdd.onSubmit(); 48 | expect(mockImposterService.postImposters).toHaveBeenCalledWith({ port: '123', protocol: 'http', name: '', 49 | recordRequests: '', key: '', cert: '', mutualAuth: '', defaultResponse: null, stubs: null, 50 | endOfRequestResolver: null }, true); 51 | expect(navSpy).toHaveBeenCalledWith(['imposters']); 52 | })); 53 | 54 | it('should set the form as invalid', async(() => { 55 | const { fixture, imposterAdd } = setup(); 56 | fixture.detectChanges(); 57 | imposterAdd.onSubmit(); 58 | expect(imposterAdd.form.valid).toBeFalsy(); 59 | })); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { ImposterDetailComponent } from './imposter-detail.component'; 3 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 4 | import { ImposterService } from './imposter.service'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { of } from 'rxjs'; 8 | import { DialogService } from '../shared/dialog.service'; 9 | 10 | describe('ImposterDetailComponent', () => { 11 | let mockImposterService; 12 | let mockDialogService; 13 | beforeEach(async(() => { 14 | mockImposterService = jasmine.createSpyObj('mockImposterService', ['getImposter']); 15 | mockDialogService = jasmine.createSpyObj('mockDialogService', ['confirm']); 16 | TestBed.configureTestingModule({ 17 | declarations: [ImposterDetailComponent], 18 | providers: [ 19 | { provide: ImposterService, useValue: mockImposterService }, 20 | { provide: DialogService, useValue: mockDialogService }, 21 | { provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: () => 123 } } } } 22 | ], 23 | imports: [RouterTestingModule.withRoutes([])], 24 | schemas: [NO_ERRORS_SCHEMA] 25 | }).compileComponents(); 26 | })); 27 | 28 | function setup() { 29 | const fixture = TestBed.createComponent(ImposterDetailComponent); 30 | const imposterDetail = fixture.debugElement.componentInstance; 31 | const navSpy = spyOn(TestBed.inject(Router), 'navigate'); 32 | return { fixture, imposterDetail, navSpy }; 33 | } 34 | 35 | it('should create the imposter detail component', async(() => { 36 | const { imposterDetail } = setup(); 37 | expect(imposterDetail).toBeTruthy(); 38 | })); 39 | 40 | it('should get and set the imposter details', async(() => { 41 | const { fixture, imposterDetail } = setup(); 42 | mockImposterService.getImposter.and.returnValue(of({ port: 123, protocol: 'http', stubs: []})); 43 | fixture.detectChanges(); 44 | expect(imposterDetail.imposter).toEqual({ port: 123, protocol: 'http', stubs: [] }); 45 | })); 46 | 47 | it('should return to imposter list', async(() => { 48 | const { imposterDetail, navSpy } = setup(); 49 | imposterDetail.return(); 50 | expect(navSpy).toHaveBeenCalledWith(['imposters']); 51 | })); 52 | 53 | it('should go to logs page', async(() => { 54 | const { imposterDetail, navSpy } = setup(); 55 | imposterDetail.viewLogs(123); 56 | expect(navSpy).toHaveBeenCalledWith(['/logs']); 57 | })); 58 | }); 59 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 |

About

3 |
4 |

Summary

5 |
6 | Disguise is a project made with Angular that provides a user interface for the Mountebank server. 7 | It facilitates the user interactions with the API and helps visualize and manage all the informations of the imposters and stubs in the server. 8 | The source code for this application is located at https://github.com/Opus-Software/disguise. 9 |
10 |

Imposters Page

11 |
12 |
13 | This is the page where you can manage the imposters present in the Mountebank server. You can visualize the informations of all the imposters and their stubs, add new imposters and delete existing ones. 14 | You can also add new singular stubs to imposters, edit and delete existing ones. 15 |
16 |
17 | You can export the actual state of the Mountebank server, saving all the current imposters and stubs to a json file. It is also possible to import a configuration json file, adding the imposters and stubs 18 | defined in it to the running Mountebank server. 19 |
20 |
21 | If an imposter has the attribute recordRequests set to true, you can visualize all its recorded requests. 22 | The requests are shown in a list with their respective informations like timestamp, headers and request body. 23 |
24 |
25 |

Logs Page

26 |
27 |
28 | This is the page where you can visualize all the logs returned by the Mountebank server. The logs are presented in a table where you can filter them by the port number, the type 29 | of the log, the imposter related to the log and the log message. 30 |
31 |
32 |

Server Location

33 |
34 |
35 | The application assumes by default that the Mountebank server is running on the port 2525 on localhost. If the user wishes to change this, it is necessary to modify the source code, 36 | changing the value of the target property in the src/proxy.conf.json file to the desired server location. 37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /src/app/logs/log.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Imposters / 5 | Logs 6 | 7 | 8 | Imposters / 9 | Imposter Details / 10 | Logs 11 | 12 | 13 |

Logs

14 |
15 |
16 | 28 |
29 | 30 | 34 | Auto-Refresh 35 | 36 | 37 | 41 | 42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
TimestampLevelImposterMessage
{{log.timestamp | timestamp}}{{log.level}}{{log.imposter}}{{log.message}}
63 |
64 | 65 |
66 |
-------------------------------------------------------------------------------- /src/app/imposters/imposter-detail.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | .return 4 | padding-left: 1.25rem 5 | padding-right: 1.25rem 6 | margin-top: 1.5em 7 | color: white 8 | font-size: 12px 9 | .return-link 10 | cursor: pointer 11 | .json 12 | color: $grey 13 | .predicate-wrap 14 | oveflow: auto 15 | .row 16 | color: #fff 17 | h3 18 | color:#fff 19 | .table-editable 20 | border: 2px solid 21 | border-radius: 5px 22 | .btnText 23 | color: #fff 24 | cursor: pointer 25 | &:hover 26 | color: $main-color 27 | .modal-content 28 | background-color: #212529 29 | .texts 30 | border-radius: 5px 31 | border-width: 1px 32 | border-color: white 33 | border-style: solid 34 | margin-bottom: 1% 35 | .close 36 | color: $main-color 37 | &:hover 38 | color: $main-color 39 | .txtA 40 | background-color: $black1 41 | color: white 42 | &:focus 43 | background-color: $black1 44 | color: white 45 | &:read-only 46 | background-color: $black1 47 | color: white 48 | .txtA::-webkit-scrollbar 49 | background-color: $black2 50 | width: 12px 51 | .txtA::-webkit-scrollbar-thumb 52 | background-color: $main-color 53 | border-radius: 10px 54 | .txtA::-webkit-scrollbar-corner 55 | background-color: $black2 56 | .number 57 | margin: 0 58 | border-right: none 59 | padding-right: 10px 60 | resize: none 61 | width: 45px 62 | border-radius: .25rem 63 | border-color: white 64 | border-top-right-radius: 0 65 | border-bottom-right-radius: 0 66 | padding-top: 0.375rem 67 | padding-bottom: 0.375rem 68 | font-size: 1rem 69 | color: rgba(256, 256, 256, .5) 70 | overflow: hidden 71 | &.wider 72 | width: 55px 73 | .numberedTxt 74 | border-left: none 75 | border-top-left-radius: 0 76 | border-bottom-left-radius: 0 77 | .invalid 78 | border-color: #dc3545 79 | .modal-dialog 80 | border-radius: 5px 81 | border-color: white 82 | border-style: solid 83 | border-width: 1px 84 | .btn 85 | border-color: white 86 | border-width: 1px 87 | color: white 88 | font-size: 13px 89 | margin: 1em 90 | &:hover 91 | color: white 92 | &.no-border 93 | border: none 94 | color: $main-color 95 | &.no-margin 96 | margin: 0 97 | .active 98 | background-color: $main-color 99 | .btn-text 100 | cursor: pointer 101 | color: $main-color 102 | .dynamic-checkbox 103 | .stub-up 104 | display: none 105 | input[type='checkbox']:checked ~ 106 | .stub-up 107 | display: inline-block 108 | .stub-down 109 | display: none 110 | .stub-check 111 | display: none 112 | .see-more 113 | color: $main-color 114 | cursor: pointer 115 | .tCol1 116 | width: 10% 117 | .tCol2 118 | width: 20% 119 | .borderless td 120 | border: none 121 | padding-top: 0 122 | .borderless th 123 | border: none 124 | padding-bottom: 0 125 | .modal 126 | overflow: auto 127 | .list-group 128 | color: white 129 | .hline 130 | background-color: white -------------------------------------------------------------------------------- /src/app/shared/add-stub-dialog.component.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles.sass' 2 | 3 | ::ng-deep .add-stub-dialog .modal-content 4 | width: 100% 5 | margin-top: 12% 6 | background-color: $black1 7 | color: white 8 | font-size: 13px 9 | border-color: white 10 | border-style: solid 11 | border-width: 1px 12 | border-radius: 5px 13 | padding: 1em 14 | .btn 15 | border-color: white 16 | border-width: 1px 17 | color: white 18 | font-size: 13px 19 | margin: 1em 20 | &:hover 21 | color: white 22 | &.no-border 23 | border: none 24 | color: $main-color 25 | &.no-margin 26 | margin: 0 27 | .active 28 | background-color: $main-color 29 | .mFooter 30 | margin-top: 1em 31 | .txtA 32 | background-color: $black1 33 | color: white 34 | &:focus 35 | background-color: $black1 36 | color: white 37 | &:read-only 38 | background-color: $black1 39 | color: white 40 | .text-btn 41 | color: white 42 | margin-left: .2em 43 | &:hover 44 | color: $main-color 45 | .field-card 46 | border: 1px solid white 47 | border-radius: 5px 48 | padding: 1em 49 | .dropdown-toggle 50 | color: #495057 51 | white-space: normal 52 | margin-left: 0 53 | margin-top: 0 54 | padding-top: .7em 55 | padding-bottom: .7em 56 | .header-card 57 | border: 1px solid white 58 | padding: 1em 59 | border-radius: 5px 60 | .flex-fill 61 | width: 50% 62 | .dynamic-checkbox 63 | .beh-up 64 | display: none 65 | input[type='checkbox']:checked ~ 66 | .beh-up 67 | display: inline-block 68 | .beh-down 69 | display: none 70 | .dropdown-menu 71 | color: white 72 | background-color: $black1 73 | .dropdown-item 74 | background-color: $black1 75 | &:hover 76 | background-color: $black2 77 | .red 78 | color: $red 79 | .header-flex 80 | height: 3em 81 | input:-webkit-autofill 82 | -webkit-box-shadow: 0 0 0 30px $black1 inset !important 83 | -webkit-text-fill-color: white !important 84 | &:hover, &:focus, &:active 85 | -webkit-box-shadow: 0 0 0 30px $black1 inset !important 86 | -webkit-text-fill-color: white !important 87 | .txtA::-webkit-scrollbar 88 | background-color: $black2 89 | width: 12px 90 | .txtA::-webkit-scrollbar-thumb 91 | background-color: $main-color 92 | border-radius: 10px 93 | .txtA::-webkit-scrollbar-corner 94 | background-color: $black2 95 | .del-pred 96 | float: right 97 | .number 98 | margin: 0 99 | border-right: none 100 | padding-right: 10px 101 | resize: none 102 | width: 45px 103 | border-radius: .25rem 104 | border-color: white 105 | border-top-right-radius: 0 106 | border-bottom-right-radius: 0 107 | padding-top: 0.375rem 108 | padding-bottom: 0.375rem 109 | font-size: 1rem 110 | color: rgba(256, 256, 256, .5) 111 | overflow: hidden 112 | &.wider 113 | width: 55px 114 | .numberedTxt 115 | border-left: none 116 | border-top-left-radius: 0 117 | border-bottom-left-radius: 0 118 | .invalid 119 | border-color: #dc3545 -------------------------------------------------------------------------------- /src/app/imposters/imposter-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Imposters

3 |
4 |
5 |
6 | 7 | 11 | 12 | 13 | 17 | 18 | 19 | 23 | 24 |
25 |
26 | 27 | 31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 60 | 61 | 62 |
PortProtocolNameOptions
{{ imposter["port"] }}{{ imposter["protocol"] }}{{ imposter["name"] }} 50 | 53 | 56 | 59 |
63 | 64 |
65 |

There are no imposters running at the moment.

66 |
67 |
-------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 26 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 27 | 28 | /** 29 | * Web Animations `@angular/platform-browser/animations` 30 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 31 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 32 | */ 33 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 34 | 35 | /** 36 | * By default, zone.js will patch all possible macroTask and DomEvents 37 | * user can disable parts of macroTask/DomEvents patch by setting following flags 38 | * because those flags need to be set before `zone.js` being loaded, and webpack 39 | * will put import in the top of bundle, so user need to create a separate file 40 | * in this directory (for example: zone-flags.ts), and put the following flags 41 | * into that file, and then add the following code before importing zone.js. 42 | * import './zone-flags'; 43 | * 44 | * The flags allowed in zone-flags.ts are listed here. 45 | * 46 | * The following flags will work for all browsers. 47 | * 48 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 49 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 50 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 51 | * 52 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 53 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 54 | * 55 | * (window as any).__Zone_enable_cross_context_check = true; 56 | * 57 | */ 58 | 59 | /*************************************************************************************************** 60 | * Zone JS is required by default for Angular itself. 61 | */ 62 | import 'zone.js/dist/zone'; // Included with Angular CLI. 63 | 64 | 65 | /*************************************************************************************************** 66 | * APPLICATION IMPORTS 67 | */ 68 | -------------------------------------------------------------------------------- /src/app/shared/dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 3 | import { DownloadDialogComponent } from './download-dialog.component'; 4 | import { DialogComponent } from './dialog.component'; 5 | import { UploadDialogComponent } from './upload-dialog.component'; 6 | import { AddStubDialogComponent } from './add-stub-dialog.component'; 7 | import { FilterDialogComponent } from './filter-dialog.component'; 8 | 9 | @Injectable() 10 | export class DialogService { 11 | 12 | constructor(private modalService: NgbModal) { } 13 | 14 | confirm(title: string, message: string, btnOkText: string = 'DELETE', 15 | btnCancelText: string = 'CANCEL', dialogSize: 'sm'|'lg' = 'sm'): Promise { 16 | const modalRef = this.modalService.open(DialogComponent, { size: dialogSize, centered: true, windowClass: 'confirm-modal' }); 17 | modalRef.componentInstance.title = title; 18 | modalRef.componentInstance.message = message; 19 | modalRef.componentInstance.btnOkText = btnOkText; 20 | modalRef.componentInstance.btnCancelText = btnCancelText; 21 | return modalRef.result; 22 | } 23 | 24 | download(title: string, message: string, btnOkText: string = 'DOWNLOAD', 25 | btnCancelText: string = 'CANCEL', dialogSize: 'sm'|'lg' = 'sm'): Promise { 26 | const modalRef = this.modalService.open(DownloadDialogComponent, { size: dialogSize, centered: true, windowClass: 'download-modal' }); 27 | modalRef.componentInstance.title = title; 28 | modalRef.componentInstance.message = message; 29 | modalRef.componentInstance.btnOkText = btnOkText; 30 | modalRef.componentInstance.btnCancelText = btnCancelText; 31 | return modalRef.result; 32 | } 33 | 34 | upload(title: string, message: string, ports: string[], btnOkText: string = 'UPLOAD', 35 | btnCancelText: 'CANCEL', dialogSize: 'sm'|'md' = 'sm'): Promise { 36 | const modalRef = this.modalService.open(UploadDialogComponent, { size: dialogSize, centered: true, windowClass: 'upload-modal' }); 37 | modalRef.componentInstance.title = title; 38 | modalRef.componentInstance.message = message; 39 | modalRef.componentInstance.btnOkText = btnOkText; 40 | modalRef.componentInstance.btnCancelText = btnCancelText; 41 | modalRef.componentInstance.ports = ports; 42 | return modalRef.result; 43 | } 44 | 45 | addStub(imposterPort: number, btnOkText: string = 'DONE', btnCancelText: string = 'CANCEL', 46 | dialogSize: 'sm'|'xl' = 'xl'): Promise { 47 | const modalRef = this.modalService.open(AddStubDialogComponent, { size: dialogSize, keyboard: false, backdrop: 'static', 48 | windowClass: 'add-stub-dialog'}); 49 | modalRef.componentInstance.btnOkText = btnOkText; 50 | modalRef.componentInstance.btnCancelText = btnCancelText; 51 | modalRef.componentInstance.imposterPort = imposterPort; 52 | return modalRef.result; 53 | } 54 | 55 | openFilter(filters: object, imposters: object, dialogSize: 'sm'|'md' = 'md'): Promise { 56 | const modalRef = this.modalService.open(FilterDialogComponent, { size: dialogSize, centered: true, windowClass: 'filter-dialog' }); 57 | modalRef.componentInstance.inputFilters = filters; 58 | modalRef.componentInstance.imposters = imposters; 59 | return modalRef.result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ImposterService } from './imposter.service'; 3 | import { faList, faTrash, faInfoCircle, faPlusSquare, faSyncAlt, faFileDownload, 4 | faTimes, faCheck, faFileUpload } from '@fortawesome/free-solid-svg-icons'; 5 | import { Router } from '@angular/router'; 6 | import { DialogService } from '../shared/dialog.service'; 7 | @Component({ 8 | templateUrl: './imposter-list.component.html', 9 | styleUrls: ['./imposter-list.component.sass'], 10 | }) 11 | export class ImposterListComponent implements OnInit { 12 | imposters: object[] = []; 13 | faList = faList; 14 | faInfoCircle = faInfoCircle; 15 | faPlusSquare = faPlusSquare; 16 | faTrash = faTrash; 17 | faSyncAlt = faSyncAlt; 18 | faFileDownload = faFileDownload; 19 | faTimes = faTimes; 20 | faCheck = faCheck; 21 | faFileUpload = faFileUpload; 22 | page = 1; 23 | pageSize = 8; 24 | ports = []; 25 | 26 | constructor(private imposterService: ImposterService, 27 | private router: Router, 28 | private dialogService: DialogService) {} 29 | 30 | ngOnInit(): void { 31 | this.getImposters(); 32 | } 33 | 34 | getImposters(): void { 35 | this.ports = []; 36 | this.imposterService.getImposters().subscribe({ 37 | next: imposters => { 38 | this.imposters = imposters['imposters']; 39 | this.imposters.forEach(element => { 40 | this.getImposterName(element); 41 | this.ports.push(element['port']); 42 | }); 43 | } 44 | }); 45 | } 46 | 47 | refresh(): void { 48 | this.getImposters(); 49 | } 50 | 51 | getImposterName(imposter: object): void { 52 | this.imposterService.getImposter(imposter['port']).subscribe({ 53 | next: details => { 54 | if (details['name']) { 55 | imposter['name'] = details['name']; 56 | } else { 57 | imposter['name'] = '-'; 58 | } 59 | } 60 | }); 61 | } 62 | 63 | onImposterDetail(port: number): void { 64 | this.router.navigate(['imposters', port]); 65 | } 66 | 67 | onAddImposter(): void { 68 | this.router.navigate(['newImposter']); 69 | } 70 | 71 | uploadImposters(): void { 72 | this.dialogService.upload('Upload Config File', 'Choose a file to upload:', this.ports, 'UPLOAD', 'CANCEL').then( 73 | ok => { this.refresh(); }, 74 | fail => { } 75 | ); 76 | } 77 | 78 | deleteImposter(port: number): void { 79 | this.dialogService.confirm('Delete Imposter', `Do you really want to delete the imposter on the port ${port}?`).then( 80 | ok => { 81 | if (ok) { 82 | this.imposterService.deleteImposters(port).subscribe({ 83 | next: () => this.refresh() 84 | }); 85 | } 86 | }, 87 | fail => { } 88 | ); 89 | } 90 | viewLogs(port: string): void { 91 | localStorage.setItem('imposterFilter', port); 92 | localStorage.setItem('redirect', '1'); 93 | this.router.navigate(['/logs']); 94 | } 95 | 96 | downloadImposters(): void { 97 | this.dialogService.download('Download Current Config', 'Choose a name to your file:').then( 98 | ok => { }, 99 | fail => { } 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app/logs/log.component.sass: -------------------------------------------------------------------------------- 1 | @import "../../styles.sass" 2 | 3 | .return 4 | padding-left: 1.25rem 5 | padding-right: 1.25rem 6 | margin-top: 1.5em 7 | color: white 8 | font-size: 12px 9 | .return-link 10 | cursor: pointer 11 | .switch 12 | position: relative 13 | display: inline-block 14 | width: 40px 15 | height: 22px 16 | border: 1px solid $grey 17 | border-radius: 34px 18 | 19 | .switch input 20 | opacity: 0 21 | width: 0 22 | height: 0 23 | 24 | .slider 25 | position: absolute 26 | cursor: pointer 27 | top: 0 28 | left: 0 29 | right: 0 30 | bottom: 0 31 | background-color: $black2 32 | -webkit-transition: .4s 33 | transition: .4s 34 | 35 | .slider:before 36 | position: absolute 37 | content: "" 38 | height: 16px 39 | width: 16px 40 | left: 2px 41 | bottom: 2px 42 | background-color: $grey 43 | -webkit-transition: .4s 44 | transition: .4s 45 | 46 | input:checked + .slider 47 | background-color: $main-color 48 | 49 | input:focus + .slider 50 | box-shadow: 0 0 1px $main-color 51 | 52 | input:checked + .slider:before 53 | -webkit-transform: translateX(16px) 54 | -ms-transform: translateX(16px) 55 | transform: translateX(16px) 56 | 57 | /* Rounded sliders */ 58 | .slider.round 59 | border-radius: 34px 60 | 61 | .slider.round:before 62 | border-radius: 50% 63 | 64 | .dropdown-item 65 | background-color: $black1 66 | &:hover 67 | background-color: $black2 68 | 69 | .dropdown-menu 70 | background-color: $black1 71 | color: $main-color 72 | 73 | .messageFilter 74 | width: 90% 75 | 76 | .calendar-btn 77 | background-color: $black1 78 | color: $main-color 79 | border: .05em solid $main-color 80 | 81 | ::ng-deep .ngb-tp 82 | background-color: $black1 83 | color: $main-color 84 | ::ng-deep .btn-link 85 | color: $main-color 86 | &:hover 87 | color: $black1 88 | ::ng-deep .ngb-tp-input 89 | background-color: $grey 90 | border: none 91 | color: $black1 92 | &:focus 93 | background-color: $main-color 94 | color: $black 95 | ::ng-deep .popover-body 96 | background-color: $black1 97 | border: 2px solid white 98 | ::ng-deep .ngb-dp-arrow-btn 99 | color: $main-color 100 | &:hover 101 | color: $black1 102 | background-color: $main-color 103 | ::ng-deep ngb-datepicker > .ngb-dp-header 104 | background-color: $black1 105 | ngb-datepicker 106 | border: none 107 | ::ng-deep .ngb-dp-weekday.small 108 | background-color: $black2 109 | color: $grey 110 | ::ng-deep [ngbDatepickerDayView].outside 111 | color: red 112 | ::ng-deep .ngb-dp-week.ngb-dp-weekdays 113 | background-color: $black2 114 | margin-top: 5px 115 | ::ng-deep .btn-light 116 | color: $grey 117 | &:hover 118 | background-color: $main-color 119 | color: $black1 120 | ::ng-deep .btn-light 121 | &:active 122 | background-color: $main-color 123 | ::ng-deep .custom-select 124 | background-color: $grey 125 | border: none 126 | color: $black 127 | .table-cell 128 | font-size: 14px 129 | .text-btn 130 | color: white 131 | margin-left: .2em 132 | &:hover 133 | color: $main-color 134 | .tCol1 135 | width: 18% 136 | .tCol2 137 | width: 5% 138 | .tCol3 139 | width: 15% 140 | .tCol4 141 | width: 62% 142 | .txt 143 | color: white 144 | .txtA 145 | background-color: $black1 146 | color: white 147 | &:focus 148 | background-color: $black1 149 | color: white 150 | .search-btn 151 | color: white 152 | border: none 153 | border-color: white 154 | .search-bar 155 | border-top: 1em 156 | border-left: 1em 157 | border-right: 1em 158 | border-color: white -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Disguise 2 | 3 | Disguise is a project made with [Angular CLI](https://github.com/angular/angular-cli) version 9 that provides a user interface for the [Mountebank](http://www.mbtest.org/) server, facilitating the usage and interactions with the API. 4 |
5 | 6 | ## Demonstration 7 | A demonstration of the Disguise application can be found at https://opus-software.github.io/disguise. The UI in this URL only listens to the Mountebank server hosted at http://localhost:2525. If you need to change the server location, you will have to setup the application locally. 8 | 9 | ## Install and Setup 10 | 11 | ### Requirements 12 | - [Node.js and npm](https://nodejs.org/en/) Version 12 13 | - [Angular-CLI](https://github.com/angular/angular-cli) Version 9 14 | - [Mountebank](https://github.com/bbyars/mountebank) Version 2.2 15 | 16 | ### Installation 17 | 18 | After installing the Node.js, install the Angular-CLI using the npm package manager: 19 | ```bash 20 | npm install -g @angular/cli 21 | ``` 22 | 23 | Clone the project's repository and in the cloned folder run the command to install the dependencies of the project: 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | Then serve the project with the Angular-CLI and it's ready to use: 29 | ```bash 30 | ng serve 31 | ``` 32 | 33 | ### Setup 34 | 35 | The UI will be hosted in *http://localhost:4200* and the default port considered for the Mountebank server is 2525. To change the Mountebank location, modify the *target* property in the *src/proxy.conf.json* file: 36 | ```json 37 | { 38 | "/api": { 39 | "target": "http://localhost:2525", 40 | ... 41 | } 42 | } 43 | ``` 44 | 45 | ## Usage 46 | 47 | ### Imposters Page 48 | 49 | This is the page where you can manage the imposters present in the Mountebank server. You can visualize the informations of all the imposters and their stubs, add new imposters and delete existing ones. You can also add new singular stubs to imposters, edit and delete existing ones. 50 | 51 | You can export the actual state of the Mountebank server, saving all the current imposters and stubs, to a json file. It is also possible to import a configuration json file, adding the imposters and stubs defined in it to the running Mountebank server. 52 | 53 | If an imposter has the attribute *recordRequests* set to true, you can visualize all their recorded requests. The requests are shown in a list with their respective informations like timestamp, headers and request body. 54 | 55 | ### Logs Page 56 | 57 | This is the page where you can visualize the logs returned by the Mountebank server. The logs are presented in a table where you can filter them by the port number, the type of the log, the imposter related to the log and the log message. 58 | 59 | ## Copyright 60 | 61 | Copyright © 2020 Opus Software 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 64 | 65 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/app/imposters/imposter-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { ImposterListComponent } from './imposter-list.component'; 3 | import { ImposterService } from './imposter.service'; 4 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 5 | import { Router } from '@angular/router'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { of } from 'rxjs'; 8 | import { DialogService } from '../shared/dialog.service'; 9 | 10 | describe('ImposterListComponent', () => { 11 | let mockImposterService; 12 | let mockDialogService; 13 | 14 | beforeEach(async(() => { 15 | mockImposterService = jasmine.createSpyObj('mockImposterService', ['getImposters', 'getImposter']); 16 | mockDialogService = jasmine.createSpyObj('mockDialogService', ['confirm']); 17 | TestBed.configureTestingModule({ 18 | declarations: [ImposterListComponent], 19 | providers: [ 20 | { provide: ImposterService, useValue: mockImposterService }, 21 | { provide: DialogService, useValue: mockDialogService} 22 | ], 23 | imports: [RouterTestingModule.withRoutes([])], 24 | schemas: [NO_ERRORS_SCHEMA] 25 | }).compileComponents(); 26 | })); 27 | 28 | function setup() { 29 | const fixture = TestBed.createComponent(ImposterListComponent); 30 | const impostersList = fixture.debugElement.componentInstance; 31 | const navSpy = spyOn(TestBed.inject(Router), 'navigate'); 32 | return { fixture, impostersList, navSpy }; 33 | } 34 | 35 | it('should create the imposters list component', async(() => { 36 | const { impostersList } = setup(); 37 | expect(impostersList).toBeTruthy(); 38 | })); 39 | 40 | it('should get and set the imposters list', async(() => { 41 | const { fixture, impostersList } = setup(); 42 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 1234, protocol: 'http' }] })); 43 | mockImposterService.getImposter.and.returnValue(of({ name: 'test' })); 44 | fixture.detectChanges(); 45 | expect(impostersList.imposters).toEqual([{ port: 1234, protocol: 'http', name: 'test' }]); 46 | expect(impostersList.ports).toEqual([1234]); 47 | })); 48 | 49 | it('should refresh the imposters list', async(() => { 50 | const { fixture, impostersList } = setup(); 51 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 1234, protocol: 'http' }] })); 52 | mockImposterService.getImposter.and.returnValue(of({ name: 'test' })); 53 | fixture.detectChanges(); 54 | expect(impostersList.imposters).toEqual([{ port: 1234, protocol: 'http', name: 'test' }]); 55 | mockImposterService.getImposters.and.returnValue( 56 | of({ imposters: [{ port: 1234, protocol: 'http' }, { port: 4321, protocol: 'http' }] }) 57 | ); 58 | impostersList.refresh(); 59 | expect(impostersList.imposters).toEqual( 60 | [{ port: 1234, protocol: 'http', name: 'test' }, { port: 4321, protocol: 'http', name: 'test' }] 61 | ); 62 | })); 63 | 64 | it('should go to imposter detail', async(() => { 65 | const { impostersList, navSpy } = setup(); 66 | impostersList.onImposterDetail(123); 67 | expect(navSpy).toHaveBeenCalledWith(['imposters', 123 ]); 68 | })); 69 | 70 | it('should go to the logs page', async(() => { 71 | const { impostersList, navSpy } = setup(); 72 | impostersList.viewLogs(123); 73 | expect(navSpy).toHaveBeenCalledWith(['/logs']); 74 | })); 75 | 76 | it('should go to the imposters add page', async(() => { 77 | const { impostersList, navSpy } = setup(); 78 | impostersList.onAddImposter(); 79 | expect(navSpy).toHaveBeenCalledWith(['newImposter']); 80 | })); 81 | }); 82 | -------------------------------------------------------------------------------- /src/app/shared/filter-dialog.component.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
11 | Timestamp 12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | Level 44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | Imposter 58 | 59 |
60 |
61 | 64 | 71 |
72 |
73 |
74 | 75 |
-------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "disguise": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "sass" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "docs", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | 32 | "src/styles.sass", 33 | "node_modules/bootstrap/dist/css/bootstrap.min.css" 34 | ], 35 | "scripts": [ 36 | "node_modules/jquery/dist/jquery.min.js", 37 | "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" 38 | ] 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "extractCss": true, 52 | "namedChunks": false, 53 | "extractLicenses": true, 54 | "vendorChunk": false, 55 | "buildOptimizer": true, 56 | "budgets": [ 57 | { 58 | "type": "initial", 59 | "maximumWarning": "5mb", 60 | "maximumError": "10mb" 61 | }, 62 | { 63 | "type": "anyComponentStyle", 64 | "maximumWarning": "150kb", 65 | "maximumError": "250kb" 66 | } 67 | ] 68 | } 69 | } 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "options": { 74 | "browserTarget": "disguise:build", 75 | "proxyConfig": "src/proxy.conf.json" 76 | }, 77 | "configurations": { 78 | "production": { 79 | "browserTarget": "disguise:build:production" 80 | } 81 | } 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "disguise:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "assets": [ 97 | "src/favicon.ico", 98 | "src/assets" 99 | ], 100 | "styles": [ 101 | "src/styles.sass" 102 | ], 103 | "scripts": [] 104 | } 105 | }, 106 | "lint": { 107 | "builder": "@angular-devkit/build-angular:tslint", 108 | "options": { 109 | "tsConfig": [ 110 | "tsconfig.app.json", 111 | "tsconfig.spec.json", 112 | "e2e/tsconfig.json" 113 | ], 114 | "exclude": [ 115 | "**/node_modules/**" 116 | ] 117 | } 118 | }, 119 | "e2e": { 120 | "builder": "@angular-devkit/build-angular:protractor", 121 | "options": { 122 | "protractorConfig": "e2e/protractor.conf.js", 123 | "devServerTarget": "disguise:serve" 124 | }, 125 | "configurations": { 126 | "production": { 127 | "devServerTarget": "disguise:serve:production" 128 | } 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "disguise" 135 | } -------------------------------------------------------------------------------- /src/app/shared/filter-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 3 | import { faCalendar } from '@fortawesome/free-solid-svg-icons'; 4 | import { DatePipe } from '@angular/common'; 5 | 6 | @Component({ 7 | selector: 'app-filter-dialog', 8 | templateUrl: './filter-dialog.component.html', 9 | styleUrls: ['./filter-dialog.component.sass'] 10 | }) 11 | export class FilterDialogComponent implements OnInit { 12 | @Input() inputFilters: object; 13 | @Input() imposters: string[]; 14 | faCalendar = faCalendar; 15 | filter = { 16 | level: 'none', 17 | timestampFrom: '', 18 | timestampTo: '' , 19 | imposter: 'Choose Imposter' 20 | }; 21 | date = { from: '', to: ''}; 22 | time = { 23 | from: { hour: +'00', minute: +'00', second: +'00' }, 24 | to: { hour: +'00', minute: +'00', second: +'00' } 25 | }; 26 | 27 | constructor(private activeModal: NgbActiveModal, 28 | private datePipe: DatePipe) { } 29 | 30 | ngOnInit(): void { 31 | this.filter.level = this.inputFilters['level']; 32 | this.filter.timestampFrom = this.inputFilters['timestampFrom']; 33 | this.filter.timestampTo = this.inputFilters['timestampTo']; 34 | this.filter.imposter = this.inputFilters['imposter']; 35 | this.checkTimestampFilters(); 36 | } 37 | 38 | accept(): void { 39 | this.activeModal.close(this.filter); 40 | } 41 | 42 | onDateSelect(label: string) { 43 | if (label === 'to') { 44 | this.filter.timestampTo = `${this.date.to['year']}-${('0' + this.date.to['month']).slice(-2)}-` + 45 | `${('0' + this.date.to['day']).slice(-2)} ${('0' + this.time.to['hour']).slice(-2)}:` + 46 | `${('0' + this.time.to['minute']).slice(-2)}:${('0' + this.time.to['second']).slice(-2)}`; 47 | } else { 48 | this.filter.timestampFrom = `${this.date.from['year']}-${('0' + this.date.from['month']).slice(-2)}-` + 49 | `${('0' + this.date.from['day']).slice(-2)} ${('0' + this.time.from['hour']).slice(-2)}:` + 50 | `${('0' + this.time.from['minute']).slice(-2)}:${('0' + this.time.from['second']).slice(-2)}`; 51 | } 52 | } 53 | 54 | onTimeSelect(label: string) { 55 | if (label === 'to') { 56 | if (!this.date.to) { 57 | const currentDate = this.datePipe.transform(new Date(), 'yyyy-MM-dd'); 58 | this.filter.timestampTo = currentDate + ` ${('0' + this.time.to['hour']).slice(-2)}:` + 59 | `${('0' + this.time.to['minute']).slice(-2)}:${('0' + this.time.to['second']).slice(-2)}`; 60 | } else { 61 | const date = this.filter.timestampTo.substring(0, 10); 62 | this.filter.timestampTo = date + ` ${('0' + this.time.to['hour']).slice(-2)}:` + 63 | `${('0' + this.time.to['minute']).slice(-2)}:${('0' + this.time.to['second']).slice(-2)}`; 64 | } 65 | } else { 66 | if (!this.date.from) { 67 | const currentDate = this.datePipe.transform(new Date(), 'yyyy-MM-dd'); 68 | this.filter.timestampFrom = currentDate + ` ${('0' + this.time.from['hour']).slice(-2)}:` + 69 | `${('0' + this.time.from['minute']).slice(-2)}:${('0' + this.time.from['second']).slice(-2)}`; 70 | } else { 71 | const date = this.filter.timestampFrom.substring(0, 10); 72 | this.filter.timestampFrom = date + ` ${('0' + this.time.from['hour']).slice(-2)}:` + 73 | `${('0' + this.time.from['minute']).slice(-2)}:${('0' + this.time.from['second']).slice(-2)}`; 74 | } 75 | } 76 | } 77 | 78 | checkTimestampFilters(): void { 79 | if (this.filter.timestampFrom) { 80 | this.date.from = this.filter.timestampFrom.substring(0, 10); 81 | this.time.from.hour = +this.filter.timestampFrom.substring(11, 13); 82 | this.time.from.minute = +this.filter.timestampFrom.substring(14, 16); 83 | this.time.from.second = +this.filter.timestampFrom.substring(17, 19); 84 | } 85 | if (this.filter.timestampTo) { 86 | this.date.to = this.filter.timestampTo.substring(0, 10); 87 | this.time.to.hour = +this.filter.timestampTo.substring(11, 13); 88 | this.time.to.minute = +this.filter.timestampTo.substring(14, 16); 89 | this.time.to.second = +this.filter.timestampTo.substring(17, 19); 90 | } 91 | } 92 | 93 | cleanAll(): void { 94 | this.cleanTimestamp(); 95 | this.cleanLevel(); 96 | this.cleanImposter(); 97 | } 98 | 99 | cleanTimestamp(): void { 100 | this.filter.timestampFrom = ''; 101 | this.filter.timestampTo = ''; 102 | this.date.from = ''; 103 | this.date.to = ''; 104 | this.time.from = { hour: +'00', minute: +'00', second: +'00' }; 105 | this.time.to = { hour: +'00', minute: +'00', second: +'00' }; 106 | } 107 | 108 | cleanLevel(): void { 109 | this.filter.level = 'none'; 110 | } 111 | 112 | cleanImposter(): void { 113 | this.filter.imposter = 'Choose Imposter'; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-add.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Imposters / 5 | New Imposter 6 | 7 |

New Imposter

8 |
9 |
10 |
11 | 12 | 14 |
15 | Please insert a valid port 16 |
17 |
18 |
19 | 20 | 22 |
23 | Please insert a valid protocol 24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 |
34 | Record Requests 35 |
36 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 63 |
64 |
65 |
66 |
67 |
68 | 69 | 71 |
72 | Please insert a valid object 73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 82 |
83 | Please insert a valid stubs object 84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 | 93 |
94 | Please insert a valid object 95 |
96 |
97 |
98 |
99 | 102 | 105 |
106 |
107 |
108 |
-------------------------------------------------------------------------------- /src/app/imposters/imposter.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { ImposterService } from './imposter.service'; 3 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 4 | import { HttpRequest } from '@angular/common/http'; 5 | import { Constants } from '../shared/constants'; 6 | 7 | describe('ImposterService', () => { 8 | let imposterService: ImposterService; 9 | let httpMock: HttpTestingController; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [HttpClientTestingModule], 14 | providers: [ImposterService] 15 | }); 16 | imposterService = TestBed.inject(ImposterService); 17 | httpMock = TestBed.inject(HttpTestingController); 18 | }); 19 | 20 | afterEach(() => { 21 | httpMock.verify(); 22 | }); 23 | 24 | it('should create the imposter service', async(() => { 25 | expect(ImposterService).toBeTruthy(); 26 | })); 27 | 28 | describe('getImposters()', () => { 29 | it('should return the list of imposters', async(() => { 30 | const imposters = { 31 | imposters: [ 32 | { port: 1234, protocol: 'http' } 33 | ]}; 34 | 35 | imposterService.getImposters().subscribe( 36 | response => { 37 | expect(response['imposters'].length).toBe(1); 38 | expect(response['imposters']).toEqual([{ port: 1234, protocol: 'http' }]); 39 | }, 40 | () => fail('should have succeded') 41 | ); 42 | const req = httpMock.expectOne(`${Constants.mb}/imposters`); 43 | expect(req.request.method).toBe('GET'); 44 | req.flush(imposters, { status: 200, statusText: 'OK' }); 45 | })); 46 | }); 47 | 48 | describe('getImposter()', () => { 49 | it('should return the given imposter', async(() => { 50 | const imposter = { port: 1234, protocol: 'http', stubs: [] }; 51 | imposterService.getImposter(1234).subscribe( 52 | response => { 53 | expect(response['port']).toEqual(1234); 54 | expect(response['protocol']).toEqual('http'); 55 | expect(response['stubs']).toEqual([]); 56 | }, 57 | () => fail('should have succeded') 58 | ); 59 | const req = httpMock.expectOne(`${Constants.mb}/imposters/1234`); 60 | expect(req.request.method).toBe('GET'); 61 | req.flush(imposter, { status: 200, statusText: 'OK' }); 62 | })); 63 | }); 64 | 65 | describe('postImposters()', () => { 66 | it('should post the imposter', async(() => { 67 | const imposter = { port: 1234, protocol: 'http' }; 68 | imposterService.postImposters(imposter).subscribe( 69 | response => expect(response).toEqual(imposter), 70 | () => fail('should have succeded') 71 | ); 72 | const req = httpMock.expectOne((request: HttpRequest) => { 73 | return request.method === 'POST' 74 | && request.url === `${Constants.mb}/imposters` 75 | && JSON.stringify(request.body) === JSON.stringify(imposter); 76 | }); 77 | req.flush(imposter, { status: 201, statusText: 'Created' }); 78 | })); 79 | }); 80 | 81 | describe('addStub()', () => { 82 | it('should post the stub', async(() => { 83 | const stub = { predicates: [], responses: [] }; 84 | imposterService.addStub(1234, stub).subscribe( 85 | response => expect(response).toEqual(stub), 86 | () => fail('should have succeded') 87 | ); 88 | const req = httpMock.expectOne((request: HttpRequest) => { 89 | return request.method === 'POST' 90 | && request.url === `${Constants.mb}/imposters/1234/stubs` 91 | && JSON.stringify(request.body) === JSON.stringify(stub); 92 | }); 93 | req.flush(stub, { status: 201, statusText: 'Created' }); 94 | })); 95 | }); 96 | 97 | describe('deleteImposters()', () => { 98 | it('should delete the imposter', async(() => { 99 | imposterService.deleteImposters(1234).subscribe( 100 | response => expect(response).toEqual({}), 101 | () => fail('should have succeded') 102 | ); 103 | const req = httpMock.expectOne((request: HttpRequest) => { 104 | return request.method === 'DELETE' 105 | && request.url === `${Constants.mb}/imposters/1234`; 106 | }); 107 | req.flush({}, { status: 200, statusText: 'OK' }); 108 | })); 109 | }); 110 | 111 | describe('putStub()', () => { 112 | it('should put the stub', async(() => { 113 | const stub = { predicates: [], responses: [] }; 114 | imposterService.putStub(1234, 0, stub).subscribe( 115 | response => expect(response).toEqual(stub), 116 | () => fail('should have succeded') 117 | ); 118 | const req = httpMock.expectOne((request: HttpRequest) => { 119 | return request.method === 'PUT' 120 | && request.url === `${Constants.mb}/imposters/1234/stubs/0` 121 | && JSON.stringify(request.body) === JSON.stringify(stub); 122 | }); 123 | req.flush(stub, { status: 200, statusText: 'OK'}); 124 | })); 125 | }); 126 | 127 | describe('deleteStub()', () => { 128 | it('should delete the stub', async(() => { 129 | imposterService.deleteStub(1234, 0).subscribe( 130 | response => expect(response).toEqual({}), 131 | () => fail('should have succeded') 132 | ); 133 | const req = httpMock.expectOne((request: HttpRequest) => { 134 | return request.method === 'DELETE' 135 | && request.url === `${Constants.mb}/imposters/1234/stubs/0`; 136 | }); 137 | req.flush({}, { status: 200, statusText: 'OK' }); 138 | })); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/app/logs/log.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { LogComponent } from './log.component'; 3 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 4 | import { LogService } from './log.service'; 5 | import { of } from 'rxjs'; 6 | import { ImposterService } from '../imposters/imposter.service'; 7 | import { DatePipe } from '@angular/common'; 8 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 9 | import { RouterTestingModule } from '@angular/router/testing'; 10 | import { DialogService } from '../shared/dialog.service'; 11 | import { TimestampPipe } from '../shared/timestamp.pipe'; 12 | 13 | describe('LogComponent', () => { 14 | let mockLogService; 15 | let mockImposterService; 16 | beforeEach(async(() => { 17 | mockLogService = jasmine.createSpyObj('mockLogService', ['getLog']); 18 | mockImposterService = jasmine.createSpyObj('mockImposterService', ['getImposters']); 19 | TestBed.configureTestingModule({ 20 | declarations: [LogComponent, TimestampPipe], 21 | providers: [ 22 | { provide: LogService, useValue: mockLogService }, 23 | { provide: ImposterService, useValue: mockImposterService }, 24 | { provide: DialogService }, 25 | DatePipe, 26 | ], 27 | imports: [NgbModule, RouterTestingModule.withRoutes([])], 28 | schemas: [NO_ERRORS_SCHEMA] 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | function setup() { 34 | const fixture = TestBed.createComponent(LogComponent); 35 | const log = fixture.debugElement.componentInstance; 36 | return { fixture, log }; 37 | } 38 | 39 | it('should create the log component', async(() => { 40 | const { log } = setup(); 41 | expect(log).toBeTruthy(); 42 | })); 43 | 44 | it('should get and set the logs and imposters ports', async(() => { 45 | const { fixture, log } = setup(); 46 | mockLogService.getLog.and.returnValue(of({ logs: [{ message: '[tcp:123] test', level: 'info', 47 | timestamp: '2020-01-01T10:00:00.123Z' }] })); 48 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 123, protocol: 'tcp' }] })); 49 | fixture.detectChanges(); 50 | const ts = log.parseTimestamp('2020-01-01T10:00:00.123Z'); 51 | expect(log.filteredLog).toEqual([{ message: 'test', imposter: 'tcp:123', level: 'info', 52 | timestamp: ts }]); 53 | expect(log.logArray).toEqual(log.filteredLog); 54 | expect(log.imposters).toEqual([123]); 55 | })); 56 | 57 | it('should apply the level filter', async(() => { 58 | const { fixture, log } = setup(); 59 | mockLogService.getLog.and.returnValue(of({ logs: [{ message: '[tcp:123] test', level: 'info', 60 | timestamp: '2020-01-01T10:00:00.123Z' }, { message: '[http:222] error', level: 'error', 61 | timestamp: '2020-01-01T10:00:00.123Z' }] })); 62 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 123, protocol: 'tcp' }, 63 | { port: 222, protocol: 'http' }] })); 64 | fixture.detectChanges(); 65 | const ts = log.parseTimestamp('2020-01-01T10:00:00.123Z'); 66 | log.filters.level = 'error'; 67 | log.applyLevelFilter(); 68 | fixture.detectChanges(); 69 | expect(log.filteredLog).toEqual([{ message: 'error', imposter: 'http:222', level: 'error', timestamp: ts }]); 70 | })); 71 | 72 | it('should apply the imposter filter', async(() => { 73 | const { fixture, log } = setup(); 74 | mockLogService.getLog.and.returnValue(of({ logs: [{ message: '[tcp:123] test', level: 'info', 75 | timestamp: '2020-01-01T10:00:00.123Z' }, { message: '[http:222] error', level: 'error', 76 | timestamp: '2020-01-01T10:00:00.123Z' }] })); 77 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 123, protocol: 'tcp' }, 78 | { port: 222, protocol: 'http' }] })); 79 | fixture.detectChanges(); 80 | const ts = log.parseTimestamp('2020-01-01T10:00:00.123Z'); 81 | log.filters.imposter = '222'; 82 | log.applyImposterFilter(); 83 | fixture.detectChanges(); 84 | expect(log.filteredLog).toEqual([{ message: 'error', imposter: 'http:222', level: 'error', timestamp: ts }]); 85 | })); 86 | 87 | it('should apply the timestamp flter', async(() => { 88 | const { fixture, log } = setup(); 89 | mockLogService.getLog.and.returnValue(of({ logs: [{ message: '[tcp:123] test', level: 'info', 90 | timestamp: '2020-10-10T10:00:00.123Z' }, { message: '[http:222] error', level: 'error', 91 | timestamp: '2020-01-01T10:00:00.123Z' }] })); 92 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 123, protocol: 'tcp' }, 93 | { port: 222, protocol: 'http' }] })); 94 | fixture.detectChanges(); 95 | const ts = log.parseTimestamp('2020-01-01T10:00:00.123Z'); 96 | const ts2 = log.parseTimestamp('2020-10-10T10:00:00.123Z'); 97 | log.filters.timestampFrom = '2019-10-10 00:00:00'; 98 | log.filters.timestampTo = '2020-02-02 00:00:00'; 99 | log.applyTimestampFilter(); 100 | expect(log.filteredLog).toEqual([{ message: 'error', imposter: 'http:222', level: 'error', timestamp: ts }]); 101 | })); 102 | 103 | it('should apply more than one filter', async(() => { 104 | const { fixture, log } = setup(); 105 | mockLogService.getLog.and.returnValue(of({ logs: [{ message: '[tcp:123] test', level: 'info', 106 | timestamp: '2020-10-10T10:00:00.123Z' }, { message: '[http:222] error', level: 'error', 107 | timestamp: '2020-01-01T10:00:00.123Z' }, { message: '[tcp:123] test2', level: 'error', 108 | timestamp: '2020-01-01T10:00:00.123Z' }] })); 109 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 123, protocol: 'tcp' }, 110 | { port: 222, protocol: 'http' }] })); 111 | fixture.detectChanges(); 112 | const ts = log.parseTimestamp('2020-01-01T10:00:00.123Z'); 113 | log.filters.level = 'error'; 114 | log.filters.imposter = '123'; 115 | log.applyLevelFilter(); 116 | log.applyImposterFilter(); 117 | expect(log.filteredLog).toEqual([{ message: 'test2', imposter: 'tcp:123', level: 'error', timestamp: ts }]); 118 | })); 119 | 120 | it('should apply all filters', async(() => { 121 | const { fixture, log } = setup(); 122 | mockLogService.getLog.and.returnValue(of({ logs: [{ message: '[tcp:123] test', level: 'info', 123 | timestamp: '2020-10-10T10:00:00.123Z' }, { message: '[http:222] error', level: 'error', 124 | timestamp: '2020-01-01T10:00:00.123Z' }, { message: '[tcp:123] test2', level: 'error', 125 | timestamp: '2020-01-01T10:00:00.123Z' }] })); 126 | mockImposterService.getImposters.and.returnValue(of({ imposters: [{ port: 123, protocol: 'tcp' }, 127 | { port: 222, protocol: 'http' }] })); 128 | fixture.detectChanges(); 129 | const ts = log.parseTimestamp('2020-01-01T10:00:00.123Z'); 130 | log.filters.level = 'error'; 131 | log.filters.imposter = '123'; 132 | log.applyAllFilters(); 133 | expect(log.filteredLog).toEqual([{ message: 'test2', imposter: 'tcp:123', level: 'error', timestamp: ts }]); 134 | })); 135 | }); 136 | -------------------------------------------------------------------------------- /src/app/logs/log.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { LogService } from './log.service'; 3 | import { faFilter, faSync, faSearch } from '@fortawesome/free-solid-svg-icons'; 4 | import { ImposterService } from '../imposters/imposter.service'; 5 | import { DatePipe } from '@angular/common'; 6 | import { Router } from '@angular/router'; 7 | import { DialogService } from '../shared/dialog.service'; 8 | @Component({ 9 | templateUrl: './log.component.html', 10 | styleUrls: ['./log.component.sass'] 11 | }) 12 | 13 | export class LogComponent implements OnInit { 14 | page = 1; 15 | pageSize = 10; 16 | maxSize = 10; 17 | logArray: object[] = []; 18 | isChecked = false; 19 | faFilter = faFilter; 20 | faSearch = faSearch; 21 | faSync = faSync; 22 | filteredLog: object[] = []; 23 | imposters: string[] = []; 24 | redirectedPort: string; 25 | filters = { 26 | level: 'none', 27 | timestampFrom: '', 28 | timestampTo: '', 29 | imposter: 'Choose Imposter' 30 | }; 31 | redirect = 0; 32 | searchFilter = ''; 33 | 34 | constructor(private logService: LogService, 35 | private imposterService: ImposterService, 36 | private datePipe: DatePipe, 37 | private router: Router, 38 | private dialogService: DialogService) { } 39 | 40 | ngOnInit(): void { 41 | this.getLog(); 42 | this.getAllImposters(); 43 | } 44 | 45 | getLog() { 46 | this.logService.getLog().subscribe( 47 | data => { 48 | this.logArray = data['logs'].sort((n1, n2) => { 49 | if (n1 < n2) { 50 | return 1; 51 | } else { 52 | return -1; 53 | } 54 | }); 55 | this.logArray.forEach(log => { 56 | log['timestamp'] = this.parseTimestamp(log['timestamp']); 57 | const match = log['message'].match(/\[(.*?)\]/); 58 | log['imposter'] = match[1]; 59 | log['message'] = log['message'].substring(match[0].length + 1); 60 | }); 61 | this.filteredLog = this.logArray; 62 | this.checkImposterFilter(); 63 | this.applySearchFilter(); 64 | this.applyAllFilters(); 65 | const switchState = localStorage.getItem('isChecked'); 66 | if (switchState) { 67 | this.isChecked = JSON.parse(switchState); 68 | this.page = +localStorage.getItem('page'); 69 | localStorage.removeItem('page'); 70 | this.auto_refresh(); 71 | } 72 | } 73 | ); 74 | } 75 | 76 | parseTimestamp(log: string): string { 77 | return this.datePipe.transform(new Date(log), 'yyyy-MM-dd HH:mm:ss'); 78 | } 79 | 80 | getAllImposters(): void { 81 | this.imposters = []; 82 | this.imposterService.getImposters().subscribe( 83 | imposters => { 84 | imposters['imposters'].forEach(imposter => { 85 | this.imposters.push(imposter['port']); 86 | }); 87 | } 88 | ); 89 | } 90 | 91 | refresh() { 92 | this.getLog(); 93 | this.getAllImposters(); 94 | } 95 | 96 | onFilters(): void { 97 | this.dialogService.openFilter(this.filters, this.imposters).then( 98 | appliedFilters => { 99 | if (JSON.stringify(appliedFilters) !== JSON.stringify(this.filters)) { 100 | this.filters.level = appliedFilters['level']; 101 | this.filters.timestampFrom = appliedFilters['timestampFrom']; 102 | this.filters.timestampTo = appliedFilters['timestampTo']; 103 | this.filters.imposter = appliedFilters['imposter']; 104 | this.refresh(); 105 | } 106 | }, 107 | fail => { } 108 | ); 109 | } 110 | 111 | checkValue() { 112 | if (!this.isChecked) { 113 | this.auto_refresh(); 114 | } else { 115 | localStorage.removeItem('isChecked'); 116 | clearTimeout(JSON.parse(localStorage.getItem('timeout'))); 117 | localStorage.removeItem('timeout'); 118 | localStorage.removeItem('page'); 119 | } 120 | } 121 | 122 | auto_refresh() { 123 | localStorage.setItem('isChecked', 'true'); 124 | const timeout = setTimeout(() => { 125 | localStorage.setItem('page', `${this.page}`); 126 | this.refresh(); 127 | }, 5000); // Activate after 5 seconds. 128 | localStorage.setItem('timeout', JSON.stringify(timeout)); 129 | } 130 | 131 | applyLevelFilter(): void { 132 | if (this.filters.level !== 'none') { 133 | this.filteredLog = this.filteredLog.filter(log => { 134 | return log['level'] === this.filters.level; 135 | }); 136 | } 137 | } 138 | 139 | applyTimestampFilter(): void { 140 | this.filteredLog = this.filteredLog.filter(log => { 141 | const from = this.filters.timestampFrom ? this.filters.timestampFrom : '0000-01-01 00:00:00'; 142 | const to = this.filters.timestampTo ? this.filters.timestampTo : '9999-12-31 23:59:59'; 143 | const logTime = new Date(log['timestamp']); 144 | return (logTime >= new Date(from) && logTime <= new Date(to)); 145 | }); 146 | } 147 | 148 | applyImposterFilter(): void { 149 | if (this.filters.imposter !== 'Choose Imposter') { 150 | this.filteredLog = this.filteredLog.filter(log => { 151 | return log['imposter'].includes(this.filters.imposter); 152 | }); 153 | } 154 | } 155 | 156 | checkImposterFilter(): void { 157 | const imposter = localStorage.getItem('imposterFilter'); 158 | if (imposter) { 159 | localStorage.removeItem('imposterFilter'); 160 | this.filters['imposter'] = imposter; 161 | this.redirectedPort = imposter; 162 | this.applyImposterFilter(); 163 | } 164 | const redirectMode = localStorage.getItem('redirect'); 165 | localStorage.removeItem('redirect'); 166 | if (redirectMode === '1') { 167 | this.redirect = 1; 168 | } else if (redirectMode === '2') { 169 | this.redirect = 2; 170 | } else { 171 | this.redirect = 0; 172 | } 173 | } 174 | 175 | applyAllFilters(): void { 176 | this.applyImposterFilter(); 177 | this.applyTimestampFilter(); 178 | this.applyLevelFilter(); 179 | } 180 | 181 | returnImpostersList(): void { 182 | this.router.navigate(['imposters']); 183 | } 184 | 185 | returnImposterDetails(): void { 186 | this.router.navigate(['imposters', this.redirectedPort]); 187 | } 188 | 189 | applySearchFilter(): void { 190 | if (this.searchFilter.trim()) { 191 | this.filteredLog = this.filteredLog.filter(log => { 192 | const fit = ( 193 | log['timestamp'].toLowerCase().includes(this.searchFilter.toLowerCase()) || 194 | log['level'].toLowerCase().includes(this.searchFilter.toLowerCase()) || 195 | log['imposter'].toLowerCase().includes(this.searchFilter.toLowerCase()) || 196 | log['message'].toLowerCase().includes(this.searchFilter.toLowerCase()) 197 | ); 198 | return fit; 199 | }); 200 | } else { 201 | this.filteredLog = this.logArray; 202 | this.applyAllFilters(); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ImposterService } from './imposter.service'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { faPlusSquare, faPen, faTrash, faCaretDown, faCaretUp, faSearch } from '@fortawesome/free-solid-svg-icons'; 5 | import { DialogService } from '../shared/dialog.service'; 6 | import { NgxXml2jsonService } from 'ngx-xml2json'; 7 | 8 | @Component({ 9 | templateUrl: './imposter-detail.component.html', 10 | styleUrls: ['./imposter-detail.component.sass'] 11 | }) 12 | 13 | export class ImposterDetailComponent implements OnInit { 14 | imposter: object = { stubs: [] }; 15 | minimizedImposter: object[] = []; 16 | faPlusSquare = faPlusSquare; 17 | faSearch = faSearch; 18 | faPen = faPen; 19 | faTrash = faTrash; 20 | faCaretDown = faCaretDown; 21 | faCaretUp = faCaretUp; 22 | port: number; 23 | stub: object = { predicates: [], responses: [] }; 24 | validPredicate = true; 25 | validResponse = true; 26 | editResponse; 27 | editPredicate; 28 | predicateLines: string[] = []; 29 | responseLines: string[] = []; 30 | editPredicateLines: string[] = []; 31 | editResponseLines: string[] = []; 32 | requests: object[] = []; 33 | protocol: string; 34 | keys = []; 35 | 36 | constructor(private imposterService: ImposterService, 37 | private route: ActivatedRoute, 38 | private router: Router, 39 | private dialogService: DialogService, 40 | private ngxXml2jsonService: NgxXml2jsonService) { } 41 | 42 | ngOnInit(): void { 43 | this.port = +this.route.snapshot.paramMap.get('port'); 44 | if (this.port) { 45 | this.getImposter(this.port); 46 | } 47 | } 48 | 49 | treatStubs(): void { 50 | this.imposter['stubs'].forEach(stub => { 51 | this.minimizedImposter.push({ minPredicate: this.minimizePredicate(stub['predicates']) }); 52 | }); 53 | } 54 | 55 | minimizePredicate(stub: object[]): string[] { 56 | if (!stub) { 57 | return []; 58 | } 59 | const stubs = []; 60 | stub.forEach(object => { 61 | const minObj = this.minimizeObject(object, ''); 62 | stubs.push(minObj.substring(0, minObj.length - 2)); 63 | }); 64 | return stubs; 65 | } 66 | 67 | minimizeObject(obj: any, prefix: string): string { 68 | let finalString = ''; 69 | switch (typeof obj) { 70 | case 'object': { 71 | const keys = Object.keys(obj); 72 | keys.forEach(prop => { 73 | if (prefix === '') { 74 | finalString += `${this.minimizeObject(obj[prop], prop)}`; 75 | } else { 76 | finalString += `${this.minimizeObject(obj[prop], prefix + '.' + prop)}`; 77 | } 78 | }); 79 | break; 80 | } 81 | default: { 82 | return `${prefix}:${obj}, `; 83 | } 84 | } 85 | return finalString; 86 | } 87 | 88 | 89 | getImposter(port: number): void { 90 | this.imposterService.getImposter(port).subscribe({ 91 | next: imposter => { 92 | this.imposter = imposter; 93 | this.treatStubs(); 94 | this.getLineNumbers(); 95 | } 96 | }); 97 | } 98 | 99 | reset(i: number): void { 100 | this.editResponse = JSON.stringify(this.imposter['stubs'][i]['responses'], null, 2); 101 | this.validPredicate = true; 102 | this.validResponse = true; 103 | this.editPredicate = JSON.stringify(this.imposter['stubs'][i]['predicates'], null, 2); 104 | this.editPredicateLines[i] = this.predicateLines[i]; 105 | this.editResponseLines[i] = this.responseLines[i]; 106 | } 107 | 108 | return(): void { 109 | this.router.navigate(['imposters']); 110 | } 111 | 112 | deleteStub(port: number, index: number): void { 113 | this.dialogService.confirm('Delete Stub', `Do you really want to delete the stub at index ${index}?`).then( 114 | ok => { 115 | if (ok) { 116 | this.imposterService.deleteStub(port, index).subscribe({ 117 | next: () => this.windowReload() 118 | }); 119 | } 120 | }, 121 | fail => { } 122 | ); 123 | } 124 | 125 | onAddStub(port: number): void { 126 | this.dialogService.addStub(port).then( 127 | ok => { 128 | this.windowReload(); 129 | }, 130 | fail => { } 131 | ); 132 | } 133 | 134 | viewLogs(port: string): void { 135 | localStorage.setItem('imposterFilter', port); 136 | localStorage.setItem('redirect', '2'); 137 | this.router.navigate(['/logs']); 138 | } 139 | 140 | windowReload(): void { 141 | window.location.reload(); 142 | } 143 | 144 | getStub(port: number, index: number): void { 145 | this.imposterService.getImposter(port).subscribe({ 146 | next: imposter => { 147 | this.stub = imposter['stubs'][index]; 148 | } 149 | }); 150 | } 151 | 152 | putStub(i: number): void { 153 | try { 154 | JSON.parse(this.editPredicate); 155 | this.validPredicate = true; 156 | } catch { 157 | this.validPredicate = false; 158 | } 159 | try { 160 | this.validResponse = true; 161 | JSON.parse(this.editResponse); 162 | } catch { 163 | this.validResponse = false; 164 | } 165 | if (this.validResponse && this.validPredicate) { 166 | const body = { predicates: JSON.parse(this.editPredicate), responses: JSON.parse(this.editResponse) }; 167 | this.imposterService.putStub(this.port, i, body).subscribe({ 168 | next: () => this.windowReload() 169 | }); 170 | } 171 | } 172 | 173 | returnImpostersList(): void { 174 | this.router.navigate(['imposters']); 175 | } 176 | 177 | getLineNumbers(): void { 178 | this.imposter['stubs'].forEach(stub => { 179 | let lines = ''; 180 | let linesCount = 0; 181 | if (stub['predicates']) { 182 | linesCount = JSON.stringify(stub['predicates'], null, 2).split(/\r\n|\r|\n/).length; 183 | for (let i = 1; i <= linesCount; i++) { 184 | lines = lines + `${i}.\n`; 185 | } 186 | } 187 | this.predicateLines.push(lines); 188 | this.editPredicateLines.push(lines); 189 | linesCount = JSON.stringify(stub['responses'], null, 2).split(/\r\n|\r|\n/).length; 190 | lines = ''; 191 | for (let i = 1; i <= linesCount; i++) { 192 | lines = lines + `${i}.\n`; 193 | } 194 | this.responseLines.push(lines); 195 | this.editResponseLines.push(lines); 196 | }); 197 | } 198 | 199 | onKey(target: string, index: number): void { 200 | if (target === 'pred') { 201 | this.editPredicateLines[index] = ''; 202 | const lines = this.editPredicate.split(/\r\n|\r|\n/).length; 203 | for (let i = 1; i <= lines; i++) { 204 | this.editPredicateLines[index] = this.editPredicateLines[index] + `${i}.\n`; 205 | } 206 | } else { 207 | this.editResponseLines[index] = ''; 208 | const lines = this.editResponse.split(/\r\n|\r|\n/).length; 209 | for (let i = 1; i <= lines; i++) { 210 | this.editResponseLines[index] = this.editResponseLines[index] + `${i}.\n`; 211 | } 212 | } 213 | } 214 | 215 | getRequests(): void { 216 | this.imposterService.getImposter(this.port).subscribe({ 217 | next: (imposter) => { 218 | this.requests = imposter['requests']; 219 | this.requests.forEach((req) => { 220 | this.keys.push(Object.keys(req['headers'])); 221 | if (req['headers']['Content-Type'] === 'application/json') { 222 | if (req['body']) { 223 | req['body'] = JSON.parse(req['body']); 224 | } 225 | } else if (req['headers']['Content-Type'] === 'application/xml') { 226 | req['body'] = this.parseXml(req['body']); 227 | } 228 | }); 229 | this.protocol = imposter['protocol']; 230 | }, 231 | }); 232 | } 233 | 234 | isEmpty(obj: object): boolean { 235 | return Object.keys(obj).length <= 0; 236 | } 237 | 238 | parseXml(str: string) { 239 | str = str.replace(/ {4}| {2}|[\t\n\r]/g, ''); 240 | const parser = new DOMParser(); 241 | const xml = parser.parseFromString(str, 'text/xml'); 242 | const obj = this.ngxXml2jsonService.xmlToJson(xml); 243 | return obj; 244 | } 245 | 246 | } 247 | -------------------------------------------------------------------------------- /src/app/imposters/imposter-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Imposters / 4 | Imposter Details 5 | 6 |

Imposter Details

7 |
8 |
9 |

Imposter Data

10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Port:Protocol:Name:
{{ imposter["port"] }}{{ imposter["protocol"] }}{{ imposter["name"] }}
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 |
Record Requests:Number of Requests:
37 | {{ imposter["recordRequests"] }} 38 | 40 | {{ imposter["numberOfRequests"] }}
45 |
46 | 47 | 48 |
49 | 114 |
115 |
116 |

Stubs

117 | 121 |
122 |
123 |
124 |
125 |
126 |
{{ min }}
127 |
128 |
129 |
130 | 136 |
137 |
138 | 139 | 142 | 145 |
146 | 189 | 232 |
233 |
234 |
235 | 236 |
237 | 238 | 240 |
241 |
242 |
243 | 244 |
245 | 246 | 248 |
249 |
250 |
251 | 252 |
253 |
254 |
255 |
256 |
-------------------------------------------------------------------------------- /src/app/shared/add-stub-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 3 | import { ImposterService } from '../imposters/imposter.service'; 4 | import { faPlusSquare, faTrash, faCaretUp, faCaretDown, faPen, faTimes } from '@fortawesome/free-solid-svg-icons'; 5 | import { Response, Predicate } from './interfaces'; 6 | 7 | @Component({ 8 | selector: 'app-add-stub-dialog', 9 | templateUrl: './add-stub-dialog.component.html', 10 | styleUrls: ['./add-stub-dialog.component.sass'] 11 | }) 12 | export class AddStubDialogComponent implements OnInit { 13 | 14 | @Input() btnOkText: string; 15 | @Input() btnCancelText: string; 16 | @Input() imposterPort: number; 17 | showObject = true; 18 | showField = false; 19 | description = ''; 20 | faPlusSquare = faPlusSquare; 21 | faTrash = faTrash; 22 | faCaretUp = faCaretUp; 23 | faCaretDown = faCaretDown; 24 | faPen = faPen; 25 | faTimes = faTimes; 26 | stubIndex = ''; 27 | stubObject = ''; 28 | responses: Response[] = [{statusCode: '', body: '', wait: '', decorate: '', repeat: '', headers: [], 29 | shellTransform: '', copy: '', lookup: '', bodyType: 'object'}]; 30 | predicates: Predicate[] = []; 31 | predicateOptions: object[] = []; 32 | validationFields: object = {}; 33 | doneResponses: object[] = [{}]; 34 | donePredicates: object[] = []; 35 | responseViewMode: boolean[] = [false]; 36 | predicateViewMode: boolean[] = []; 37 | validResponse: boolean[] = [false]; 38 | validPredicate: boolean[] = []; 39 | hasSomeError = false; 40 | stubObjectLines = ''; 41 | predicateOptionsLines: object[] = []; 42 | responseBodyLines: string[] = ['1.']; 43 | donePredicateLines: string[] = []; 44 | doneResponseLines: string[] = ['1.']; 45 | 46 | @ViewChild('stubObjLines') stubObjLines: ElementRef; 47 | @ViewChild('stubObjArea') stubObjArea: ElementRef; 48 | 49 | constructor(private activeModal: NgbActiveModal, 50 | private imposterService: ImposterService) { } 51 | 52 | ngOnInit(): void { 53 | this.resetValidation(); 54 | this.stubObjectLines = '1.'; 55 | } 56 | 57 | resetValidation(): void { 58 | this.validationFields['stubObjSynt'] = true; 59 | this.validationFields['index'] = true; 60 | this.validationFields['status'] = [true]; 61 | this.validationFields['bodyObj'] = [true]; 62 | this.validationFields['repeat'] = [true]; 63 | this.validationFields['lookup'] = [true]; 64 | this.validationFields['copy'] = [true]; 65 | this.validationFields['equals'] = []; 66 | this.validationFields['deepEquals'] = []; 67 | this.validationFields['contains'] = []; 68 | this.validationFields['startsWith'] = []; 69 | this.validationFields['endsWith'] = []; 70 | this.validationFields['matches'] = []; 71 | this.validationFields['exists'] = []; 72 | this.validationFields['not'] = []; 73 | this.validationFields['or'] = []; 74 | this.validationFields['and'] = []; 75 | this.validationFields['xpath'] = []; 76 | this.validationFields['jsonpath'] = []; 77 | } 78 | 79 | initPredicateValField(): void { 80 | this.validationFields['equals'].push(true); 81 | this.validationFields['deepEquals'].push(true); 82 | this.validationFields['contains'].push(true); 83 | this.validationFields['startsWith'].push(true); 84 | this.validationFields['endsWith'].push(true); 85 | this.validationFields['matches'].push(true); 86 | this.validationFields['exists'].push(true); 87 | this.validationFields['not'].push(true); 88 | this.validationFields['or'].push(true); 89 | this.validationFields['and'].push(true); 90 | this.validationFields['xpath'].push(true); 91 | this.validationFields['jsonpath'].push(true); 92 | } 93 | 94 | initResponseValField(): void { 95 | this.validationFields['status'].push(true); 96 | this.validationFields['bodyObj'].push(true); 97 | this.validationFields['repeat'].push(true); 98 | this.validationFields['lookup'].push(true); 99 | this.validationFields['copy'].push(true); 100 | } 101 | 102 | removePredicateValField(index: number): void { 103 | this.validationFields['equals'].splice(index, 1); 104 | this.validationFields['deepEquals'].splice(index, 1); 105 | this.validationFields['contains'].splice(index, 1); 106 | this.validationFields['startsWith'].splice(index, 1); 107 | this.validationFields['endsWith'].splice(index, 1); 108 | this.validationFields['matches'].splice(index, 1); 109 | this.validationFields['exists'].splice(index, 1); 110 | this.validationFields['not'].splice(index, 1); 111 | this.validationFields['or'].splice(index, 1); 112 | this.validationFields['and'].splice(index, 1); 113 | this.validationFields['xpath'].splice(index, 1); 114 | this.validationFields['jsonpath'].splice(index, 1); 115 | } 116 | 117 | removeResponseValField(index: number): void { 118 | this.validationFields['status'].splice(index, 1); 119 | this.validationFields['bodyObj'].splice(index, 1); 120 | this.validationFields['repeat'].splice(index, 1); 121 | this.validationFields['lookup'].splice(index, 1); 122 | this.validationFields['copy'].splice(index, 1); 123 | } 124 | 125 | checkPredicateValidation(index: number): boolean { 126 | const keys = ['equals', 'deepEquals', 'contains', 'startsWith', 'endsWith', 'matches', 127 | 'exists', 'not', 'or', 'and', 'xpath', 'jsonpath']; 128 | let valid = true; 129 | keys.forEach(k => { 130 | if (!this.validationFields[k][index]) { 131 | valid = false; 132 | } 133 | }); 134 | return valid; 135 | } 136 | 137 | checkResponseValidation(index: number): boolean { 138 | const keys = ['status', 'bodyObj', 'repeat', 'lookup', 'copy']; 139 | let valid = true; 140 | keys.forEach(k => { 141 | if (!this.validationFields[k][index]) { 142 | valid = false; 143 | } 144 | }); 145 | return valid; 146 | } 147 | 148 | public decline() { 149 | const discard = confirm('Do you really want to discard your changes and return to the imposter details page?'); 150 | if (discard) { 151 | this.activeModal.dismiss(); 152 | } 153 | } 154 | 155 | public accept() { 156 | if (this.showObject) { 157 | if (this.validateObject()) { 158 | let body = JSON.parse(this.stubObject); 159 | if (!body['stub']) { 160 | body = {stub: body}; 161 | } 162 | if (this.stubIndex) { 163 | body['index'] = +this.stubIndex; 164 | } 165 | this.imposterService.addStub(this.imposterPort, body).subscribe({ 166 | next: () => this.activeModal.close(true) 167 | }); 168 | } 169 | } else { 170 | const body = this.mergeFields(); 171 | if (!body) { 172 | alert('Add at least one response object in the stub.'); 173 | } else { 174 | let conf = true; 175 | if (this.hasSomeError) { 176 | conf = confirm('Responses and predicates with unsolved errors or unsaved ' + 177 | 'changes will not be added. Do you want to continue?'); 178 | } 179 | if (conf) { 180 | const stubBody = body['predicates'].length > 0 ? body : { responses: body['responses'] }; 181 | if (this.description) { 182 | stubBody['description'] = this.description; 183 | } 184 | const stub = { stub: stubBody }; 185 | if (this.stubIndex) { 186 | stub['index'] = +this.stubIndex; 187 | } 188 | this.imposterService.addStub(this.imposterPort, stub).subscribe({ 189 | next: () => this.activeModal.close(true) 190 | }); 191 | } 192 | } 193 | } 194 | } 195 | 196 | validateObject(): boolean { 197 | this.validationFields['stubObjSynt'] = true; 198 | try { 199 | JSON.parse(this.stubObject); 200 | } catch { 201 | this.validationFields['stubObjSynt'] = false; 202 | } 203 | if (this.stubIndex) { 204 | this.validationFields['index'] = isNaN(+this.stubIndex) ? false : true; 205 | } 206 | if (this.validationFields['index'] && this.validationFields['stubObjSynt']) { 207 | return true; 208 | } else { 209 | return false; 210 | } 211 | } 212 | 213 | mergeFields(): object { 214 | this.hasSomeError = false; 215 | const resp = []; 216 | const pred = []; 217 | this.validResponse.forEach((r, i) => { 218 | if (r) { 219 | resp.push(this.doneResponses[i]); 220 | } else { 221 | this.hasSomeError = true; 222 | } 223 | }); 224 | this.validPredicate.forEach((p, i) => { 225 | if (p) { 226 | pred.push(this.donePredicates[i]); 227 | } else { 228 | this.hasSomeError = true; 229 | } 230 | }); 231 | if (resp.length === 0) { 232 | return undefined; 233 | } 234 | return { responses: resp, predicates: pred }; 235 | } 236 | 237 | showObjects(): void { 238 | this.showObject = true; 239 | this.showField = false; 240 | } 241 | 242 | showFields(): void { 243 | this.showField = true; 244 | this.showObject = false; 245 | } 246 | 247 | newResponse(): void { 248 | this.responses.push({statusCode: '', headers: [], body: '', wait: '', 249 | decorate: '', repeat: '', shellTransform: '', copy: '', lookup: '', bodyType: 'object'}); 250 | this.doneResponses.push({}); 251 | this.responseViewMode.push(false); 252 | this.initResponseValField(); 253 | this.validResponse.push(false); 254 | this.responseBodyLines.push('1.'); 255 | this.doneResponseLines.push('1.'); 256 | } 257 | 258 | removeResponse(index: number): void { 259 | this.responses.splice(index, 1); 260 | this.doneResponses.splice(index, 1); 261 | this.responseViewMode.splice(index, 1); 262 | this.removeResponseValField(index); 263 | this.validResponse.splice(index, 1); 264 | this.responseBodyLines.splice(index, 1); 265 | this.doneResponseLines.splice(index, 1); 266 | } 267 | 268 | newPredicate(): void { 269 | this.predicates.push({equals: '', deepEquals: '', contains: '', startsWith: '', endsWith: '', 270 | matches: '', exists: '', not: '', or: '', and: '', xpath: '', jsonpath: '', inject: '', caseSensitive: false}); 271 | this.donePredicates.push({}); 272 | this.predicateViewMode.push(false); 273 | this.predicateOptions.push({}); 274 | this.initPredicateValField(); 275 | this.validPredicate.push(false); 276 | this.predicateOptionsLines.push({equals: '1.', deepEquals: '1.', contains: '1.', startsWith: '1.', endsWith: '1.', 277 | matches: '1.', exists: '1.', not: '1.', or: '1.', and: '1.', xpath: '1.', jsonpath: '1.', inject: '1.'}); 278 | this.donePredicateLines.push('1.'); 279 | } 280 | 281 | removePredicate(index: number): void { 282 | this.predicates.splice(index, 1); 283 | this.donePredicates.splice(index, 1); 284 | this.predicateViewMode.splice(index, 1); 285 | this.predicateOptions.splice(index, 1); 286 | this.removePredicateValField(index); 287 | this.validPredicate.splice(index, 1); 288 | this.predicateOptionsLines.splice(index, 1); 289 | this.donePredicateLines.splice(index, 1); 290 | } 291 | 292 | addHeader(index: number): void { 293 | this.responses[index].headers.push([]); 294 | } 295 | 296 | removeHeader(indexR: number, indexH: number): void { 297 | this.responses[indexR].headers.splice(indexH, 1); 298 | } 299 | 300 | onEditDonePredicate(index: number): void { 301 | this.predicateViewMode[index] = false; 302 | } 303 | 304 | onEditDoneResponse(index: number): void { 305 | this.responseViewMode[index] = false; 306 | } 307 | 308 | onAcceptPredicate(index: number): void { 309 | this.donePredicates[index] = this.buildPredicate(this.predicates[index], index); 310 | if (this.checkPredicateValidation(index)) { 311 | this.onKey('donePred', index); 312 | this.validPredicate[index] = true; 313 | this.predicateViewMode[index] = true; 314 | } 315 | } 316 | 317 | onAcceptResponse(index: number): void { 318 | this.doneResponses[index] = this.buildResponse(this.responses[index], index); 319 | if (this.checkResponseValidation(index)) { 320 | this.onKey('doneResp', index); 321 | this.validResponse[index] = true; 322 | this.responseViewMode[index] = true; 323 | } 324 | } 325 | 326 | checkEmpty(object: string, target: string, index: number): void { 327 | if (object === '') { 328 | this.validationFields[target][index] = true; 329 | } 330 | } 331 | 332 | buildPredicate(pred: Predicate, index: number): object { 333 | const returnObj = {}; 334 | const keys = Object.keys(pred); 335 | keys.pop(); 336 | keys.pop(); 337 | returnObj['caseSensitive'] = pred.caseSensitive; 338 | keys.forEach(atr => { 339 | if (pred[atr]) { 340 | this.validationFields[atr][index] = true; 341 | try { 342 | returnObj[atr] = JSON.parse(pred[atr]); 343 | } catch { 344 | this.validationFields[atr][index] = false; 345 | } 346 | } 347 | this.checkEmpty(pred[atr], atr, index); 348 | }); 349 | if (pred.inject) { 350 | returnObj['inject'] = pred.inject; 351 | } 352 | return returnObj; 353 | } 354 | 355 | buildResponse(res: Response, index: number): object { 356 | const returnObj = { }; 357 | if (res.statusCode) { 358 | this.validationFields['status'][index] = true; 359 | returnObj['is'] = returnObj['is'] ? returnObj['is'] : {}; 360 | this.validationFields['status'][index] = isNaN(+res.statusCode) ? false : true; 361 | returnObj['is']['statusCode'] = +res.statusCode; 362 | } 363 | this.checkEmpty(res.statusCode, 'status', index); 364 | if (res.headers) { 365 | res.headers.forEach(header => { 366 | if (header[0] && header[1]) { 367 | returnObj['is'] = returnObj['is'] ? returnObj['is'] : {}; 368 | returnObj['is']['headers'] = returnObj['is']['headers'] ? returnObj['is']['headers'] : {}; 369 | returnObj['is']['headers'][header[0]] = header[1]; 370 | } 371 | }); 372 | } 373 | if (res.body) { 374 | this.validationFields['bodyObj'][index] = true; 375 | returnObj['is'] = returnObj['is'] ? returnObj['is'] : {}; 376 | try { 377 | returnObj['is']['body'] = res.bodyType === 'string' ? res.body : JSON.parse(res.body); 378 | } catch { 379 | this.validationFields['bodyObj'][index] = false; 380 | } 381 | } 382 | this.checkEmpty(res.body, 'bodyObj', index); 383 | if (res.wait) { 384 | returnObj['_behaviors'] = returnObj['_behaviors'] ? returnObj['_behaviors'] : {}; 385 | returnObj['_behaviors']['wait'] = isNaN(+res.wait) ? res.wait : +res.wait; 386 | } 387 | if (res.repeat) { 388 | this.validationFields['repeat'][index] = true; 389 | returnObj['_behaviors'] = returnObj['_behaviors'] ? returnObj['_behaviors'] : {}; 390 | this.validationFields['repeat'][index] = (isNaN(+res.repeat) || +res.repeat <= 0) ? false : true; 391 | returnObj['_behaviors']['repeat'] = +res.repeat; 392 | } 393 | this.checkEmpty(res.repeat, 'repeat', index); 394 | if (res.copy) { 395 | this.validationFields['copy'][index] = true; 396 | returnObj['_behaviors'] = returnObj['_behaviors'] ? returnObj['_behaviors'] : {}; 397 | try { 398 | returnObj['_behaviors']['copy'] = JSON.parse(res.copy); 399 | } catch { 400 | this.validationFields['copy'][index] = false; 401 | } 402 | } 403 | this.checkEmpty(res.copy, 'copy', index); 404 | if (res.lookup) { 405 | this.validationFields['lookup'][index] = true; 406 | returnObj['_behaviors'] = returnObj['_behaviors'] ? returnObj['_behaviors'] : {}; 407 | try { 408 | returnObj['_behaviors']['lookup'] = JSON.parse(res.lookup); 409 | } catch { 410 | this.validationFields['lookup'][index] = false; 411 | } 412 | } 413 | this.checkEmpty(res.lookup, 'lookup', index); 414 | if (res.decorate) { 415 | returnObj['_behaviors'] = returnObj['_behaviors'] ? returnObj['_behaviors'] : {}; 416 | returnObj['_behaviors']['decorate'] = res.decorate; 417 | } 418 | if (res.shellTransform) { 419 | returnObj['_behaviors'] = returnObj['_behaviors'] ? returnObj['_behaviors'] : {}; 420 | const tmp = res.shellTransform.replace(/[\[\]]+/g, ''); 421 | returnObj['_behaviors']['shellTransform'] = tmp.split(','); 422 | } 423 | return returnObj; 424 | } 425 | 426 | togglePredicateOption(index: number, target: string): void { 427 | if (this.predicateOptions[index][target] === undefined) { 428 | this.predicateOptions[index][target] = true; 429 | } else { 430 | this.predicateOptions[index][target] = !this.predicateOptions[index][target]; 431 | } 432 | } 433 | 434 | deletePredicateOption(index: number, target: string): void { 435 | this.togglePredicateOption(index, target); 436 | this.predicates[index][target] = ''; 437 | } 438 | 439 | onKey(target: string, index?: number, predOpt?: string): void { 440 | let lines = 0; 441 | switch (target) { 442 | case 'stubObj': 443 | this.stubObjectLines = ''; 444 | lines = this.stubObject.split(/\r\n|\r|\n/).length; 445 | for (let i = 1; i <= lines; i++) { 446 | this.stubObjectLines = this.stubObjectLines + `${i}.\n`; 447 | } 448 | break; 449 | case 'respBody': 450 | this.responseBodyLines[index] = ''; 451 | lines = this.responses[index]['body'].split(/\r\n|\r|\n/).length; 452 | for (let i = 1; i <= lines; i++) { 453 | this.responseBodyLines[index] = this.responseBodyLines[index] + `${i}.\n`; 454 | } 455 | break; 456 | case 'doneResp': 457 | this.doneResponseLines[index] = ''; 458 | lines = JSON.stringify(this.doneResponses[index], null, 2).split(/\r\n|\r|\n/).length; 459 | for (let i = 1; i <= lines; i++) { 460 | this.doneResponseLines[index] = this.doneResponseLines[index] + `${i}.\n`; 461 | } 462 | break; 463 | case 'donePred': 464 | this.donePredicateLines[index] = ''; 465 | lines = JSON.stringify(this.donePredicates[index], null, 2).split(/\r\n|\r|\n/).length; 466 | for (let i = 1; i <= lines; i++) { 467 | this.donePredicateLines[index] = this.donePredicateLines[index] + `${i}.\n`; 468 | } 469 | break; 470 | case 'predBody': 471 | this.predicateOptionsLines[index][predOpt] = ''; 472 | lines = this.predicates[index][predOpt].split(/\r\n|\r|\n/).length; 473 | for (let i = 1; i <= lines; i++) { 474 | this.predicateOptionsLines[index][predOpt] = this.predicateOptionsLines[index][predOpt] + `${i}.\n`; 475 | } 476 | break; 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /docs/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | @angular-devkit/build-angular 2 | MIT 3 | The MIT License 4 | 5 | Copyright (c) 2017 Google, Inc. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | 26 | @angular/common 27 | MIT 28 | 29 | @angular/core 30 | MIT 31 | 32 | @angular/forms 33 | MIT 34 | 35 | @angular/localize 36 | MIT 37 | 38 | @angular/platform-browser 39 | MIT 40 | 41 | @angular/router 42 | MIT 43 | 44 | @fortawesome/angular-fontawesome 45 | MIT 46 | MIT License 47 | 48 | Copyright (c) 2018 Fonticons, Inc. and contributors 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to deal 52 | in the Software without restriction, including without limitation the rights 53 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in 58 | all copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 66 | THE SOFTWARE. 67 | 68 | 69 | @fortawesome/fontawesome-svg-core 70 | MIT 71 | Font Awesome Free License 72 | ------------------------- 73 | 74 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 75 | commercial projects, open source projects, or really almost whatever you want. 76 | Full Font Awesome Free license: https://fontawesome.com/license/free. 77 | 78 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 79 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 80 | packaged as SVG and JS file types. 81 | 82 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 83 | In the Font Awesome Free download, the SIL OFL license applies to all icons 84 | packaged as web and desktop font files. 85 | 86 | # Code: MIT License (https://opensource.org/licenses/MIT) 87 | In the Font Awesome Free download, the MIT license applies to all non-font and 88 | non-icon files. 89 | 90 | # Attribution 91 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 92 | Awesome Free files already contain embedded comments with sufficient 93 | attribution, so you shouldn't need to do anything additional when using these 94 | files normally. 95 | 96 | We've kept attribution comments terse, so we ask that you do not actively work 97 | to remove them from files, especially code. They're a great way for folks to 98 | learn about Font Awesome. 99 | 100 | # Brand Icons 101 | All brand icons are trademarks of their respective owners. The use of these 102 | trademarks does not indicate endorsement of the trademark holder by Font 103 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 104 | to represent the company, product, or service to which they refer.** 105 | 106 | 107 | @fortawesome/free-solid-svg-icons 108 | (CC-BY-4.0 AND MIT) 109 | Font Awesome Free License 110 | ------------------------- 111 | 112 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 113 | commercial projects, open source projects, or really almost whatever you want. 114 | Full Font Awesome Free license: https://fontawesome.com/license/free. 115 | 116 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 117 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 118 | packaged as SVG and JS file types. 119 | 120 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 121 | In the Font Awesome Free download, the SIL OFL license applies to all icons 122 | packaged as web and desktop font files. 123 | 124 | # Code: MIT License (https://opensource.org/licenses/MIT) 125 | In the Font Awesome Free download, the MIT license applies to all non-font and 126 | non-icon files. 127 | 128 | # Attribution 129 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 130 | Awesome Free files already contain embedded comments with sufficient 131 | attribution, so you shouldn't need to do anything additional when using these 132 | files normally. 133 | 134 | We've kept attribution comments terse, so we ask that you do not actively work 135 | to remove them from files, especially code. They're a great way for folks to 136 | learn about Font Awesome. 137 | 138 | # Brand Icons 139 | All brand icons are trademarks of their respective owners. The use of these 140 | trademarks does not indicate endorsement of the trademark holder by Font 141 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 142 | to represent the company, product, or service to which they refer.** 143 | 144 | 145 | @ng-bootstrap/ng-bootstrap 146 | MIT 147 | The MIT License (MIT) 148 | 149 | Copyright (c) 2015-2018 Angular ng-bootstrap team 150 | 151 | Permission is hereby granted, free of charge, to any person obtaining a copy 152 | of this software and associated documentation files (the "Software"), to deal 153 | in the Software without restriction, including without limitation the rights 154 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 155 | copies of the Software, and to permit persons to whom the Software is 156 | furnished to do so, subject to the following conditions: 157 | 158 | The above copyright notice and this permission notice shall be included in 159 | all copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 162 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 163 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 164 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 165 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 166 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 167 | THE SOFTWARE. 168 | 169 | 170 | bootstrap 171 | MIT 172 | The MIT License (MIT) 173 | 174 | Copyright (c) 2011-2019 Twitter, Inc. 175 | Copyright (c) 2011-2019 The Bootstrap Authors 176 | 177 | Permission is hereby granted, free of charge, to any person obtaining a copy 178 | of this software and associated documentation files (the "Software"), to deal 179 | in the Software without restriction, including without limitation the rights 180 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 181 | copies of the Software, and to permit persons to whom the Software is 182 | furnished to do so, subject to the following conditions: 183 | 184 | The above copyright notice and this permission notice shall be included in 185 | all copies or substantial portions of the Software. 186 | 187 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 188 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 189 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 190 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 191 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 192 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 193 | THE SOFTWARE. 194 | 195 | 196 | core-js 197 | MIT 198 | Copyright (c) 2014-2020 Denis Pushkarev 199 | 200 | Permission is hereby granted, free of charge, to any person obtaining a copy 201 | of this software and associated documentation files (the "Software"), to deal 202 | in the Software without restriction, including without limitation the rights 203 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 204 | copies of the Software, and to permit persons to whom the Software is 205 | furnished to do so, subject to the following conditions: 206 | 207 | The above copyright notice and this permission notice shall be included in 208 | all copies or substantial portions of the Software. 209 | 210 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 211 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 212 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 213 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 214 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 215 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 216 | THE SOFTWARE. 217 | 218 | 219 | ngx-xml2json 220 | 221 | regenerator-runtime 222 | MIT 223 | MIT License 224 | 225 | Copyright (c) 2014-present, Facebook, Inc. 226 | 227 | Permission is hereby granted, free of charge, to any person obtaining a copy 228 | of this software and associated documentation files (the "Software"), to deal 229 | in the Software without restriction, including without limitation the rights 230 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 231 | copies of the Software, and to permit persons to whom the Software is 232 | furnished to do so, subject to the following conditions: 233 | 234 | The above copyright notice and this permission notice shall be included in all 235 | copies or substantial portions of the Software. 236 | 237 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 238 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 239 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 240 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 241 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 242 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 243 | SOFTWARE. 244 | 245 | 246 | rxjs 247 | Apache-2.0 248 | Apache License 249 | Version 2.0, January 2004 250 | http://www.apache.org/licenses/ 251 | 252 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 253 | 254 | 1. Definitions. 255 | 256 | "License" shall mean the terms and conditions for use, reproduction, 257 | and distribution as defined by Sections 1 through 9 of this document. 258 | 259 | "Licensor" shall mean the copyright owner or entity authorized by 260 | the copyright owner that is granting the License. 261 | 262 | "Legal Entity" shall mean the union of the acting entity and all 263 | other entities that control, are controlled by, or are under common 264 | control with that entity. For the purposes of this definition, 265 | "control" means (i) the power, direct or indirect, to cause the 266 | direction or management of such entity, whether by contract or 267 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 268 | outstanding shares, or (iii) beneficial ownership of such entity. 269 | 270 | "You" (or "Your") shall mean an individual or Legal Entity 271 | exercising permissions granted by this License. 272 | 273 | "Source" form shall mean the preferred form for making modifications, 274 | including but not limited to software source code, documentation 275 | source, and configuration files. 276 | 277 | "Object" form shall mean any form resulting from mechanical 278 | transformation or translation of a Source form, including but 279 | not limited to compiled object code, generated documentation, 280 | and conversions to other media types. 281 | 282 | "Work" shall mean the work of authorship, whether in Source or 283 | Object form, made available under the License, as indicated by a 284 | copyright notice that is included in or attached to the work 285 | (an example is provided in the Appendix below). 286 | 287 | "Derivative Works" shall mean any work, whether in Source or Object 288 | form, that is based on (or derived from) the Work and for which the 289 | editorial revisions, annotations, elaborations, or other modifications 290 | represent, as a whole, an original work of authorship. For the purposes 291 | of this License, Derivative Works shall not include works that remain 292 | separable from, or merely link (or bind by name) to the interfaces of, 293 | the Work and Derivative Works thereof. 294 | 295 | "Contribution" shall mean any work of authorship, including 296 | the original version of the Work and any modifications or additions 297 | to that Work or Derivative Works thereof, that is intentionally 298 | submitted to Licensor for inclusion in the Work by the copyright owner 299 | or by an individual or Legal Entity authorized to submit on behalf of 300 | the copyright owner. For the purposes of this definition, "submitted" 301 | means any form of electronic, verbal, or written communication sent 302 | to the Licensor or its representatives, including but not limited to 303 | communication on electronic mailing lists, source code control systems, 304 | and issue tracking systems that are managed by, or on behalf of, the 305 | Licensor for the purpose of discussing and improving the Work, but 306 | excluding communication that is conspicuously marked or otherwise 307 | designated in writing by the copyright owner as "Not a Contribution." 308 | 309 | "Contributor" shall mean Licensor and any individual or Legal Entity 310 | on behalf of whom a Contribution has been received by Licensor and 311 | subsequently incorporated within the Work. 312 | 313 | 2. Grant of Copyright License. Subject to the terms and conditions of 314 | this License, each Contributor hereby grants to You a perpetual, 315 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 316 | copyright license to reproduce, prepare Derivative Works of, 317 | publicly display, publicly perform, sublicense, and distribute the 318 | Work and such Derivative Works in Source or Object form. 319 | 320 | 3. Grant of Patent License. Subject to the terms and conditions of 321 | this License, each Contributor hereby grants to You a perpetual, 322 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 323 | (except as stated in this section) patent license to make, have made, 324 | use, offer to sell, sell, import, and otherwise transfer the Work, 325 | where such license applies only to those patent claims licensable 326 | by such Contributor that are necessarily infringed by their 327 | Contribution(s) alone or by combination of their Contribution(s) 328 | with the Work to which such Contribution(s) was submitted. If You 329 | institute patent litigation against any entity (including a 330 | cross-claim or counterclaim in a lawsuit) alleging that the Work 331 | or a Contribution incorporated within the Work constitutes direct 332 | or contributory patent infringement, then any patent licenses 333 | granted to You under this License for that Work shall terminate 334 | as of the date such litigation is filed. 335 | 336 | 4. Redistribution. You may reproduce and distribute copies of the 337 | Work or Derivative Works thereof in any medium, with or without 338 | modifications, and in Source or Object form, provided that You 339 | meet the following conditions: 340 | 341 | (a) You must give any other recipients of the Work or 342 | Derivative Works a copy of this License; and 343 | 344 | (b) You must cause any modified files to carry prominent notices 345 | stating that You changed the files; and 346 | 347 | (c) You must retain, in the Source form of any Derivative Works 348 | that You distribute, all copyright, patent, trademark, and 349 | attribution notices from the Source form of the Work, 350 | excluding those notices that do not pertain to any part of 351 | the Derivative Works; and 352 | 353 | (d) If the Work includes a "NOTICE" text file as part of its 354 | distribution, then any Derivative Works that You distribute must 355 | include a readable copy of the attribution notices contained 356 | within such NOTICE file, excluding those notices that do not 357 | pertain to any part of the Derivative Works, in at least one 358 | of the following places: within a NOTICE text file distributed 359 | as part of the Derivative Works; within the Source form or 360 | documentation, if provided along with the Derivative Works; or, 361 | within a display generated by the Derivative Works, if and 362 | wherever such third-party notices normally appear. The contents 363 | of the NOTICE file are for informational purposes only and 364 | do not modify the License. You may add Your own attribution 365 | notices within Derivative Works that You distribute, alongside 366 | or as an addendum to the NOTICE text from the Work, provided 367 | that such additional attribution notices cannot be construed 368 | as modifying the License. 369 | 370 | You may add Your own copyright statement to Your modifications and 371 | may provide additional or different license terms and conditions 372 | for use, reproduction, or distribution of Your modifications, or 373 | for any such Derivative Works as a whole, provided Your use, 374 | reproduction, and distribution of the Work otherwise complies with 375 | the conditions stated in this License. 376 | 377 | 5. Submission of Contributions. Unless You explicitly state otherwise, 378 | any Contribution intentionally submitted for inclusion in the Work 379 | by You to the Licensor shall be under the terms and conditions of 380 | this License, without any additional terms or conditions. 381 | Notwithstanding the above, nothing herein shall supersede or modify 382 | the terms of any separate license agreement you may have executed 383 | with Licensor regarding such Contributions. 384 | 385 | 6. Trademarks. This License does not grant permission to use the trade 386 | names, trademarks, service marks, or product names of the Licensor, 387 | except as required for reasonable and customary use in describing the 388 | origin of the Work and reproducing the content of the NOTICE file. 389 | 390 | 7. Disclaimer of Warranty. Unless required by applicable law or 391 | agreed to in writing, Licensor provides the Work (and each 392 | Contributor provides its Contributions) on an "AS IS" BASIS, 393 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 394 | implied, including, without limitation, any warranties or conditions 395 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 396 | PARTICULAR PURPOSE. You are solely responsible for determining the 397 | appropriateness of using or redistributing the Work and assume any 398 | risks associated with Your exercise of permissions under this License. 399 | 400 | 8. Limitation of Liability. In no event and under no legal theory, 401 | whether in tort (including negligence), contract, or otherwise, 402 | unless required by applicable law (such as deliberate and grossly 403 | negligent acts) or agreed to in writing, shall any Contributor be 404 | liable to You for damages, including any direct, indirect, special, 405 | incidental, or consequential damages of any character arising as a 406 | result of this License or out of the use or inability to use the 407 | Work (including but not limited to damages for loss of goodwill, 408 | work stoppage, computer failure or malfunction, or any and all 409 | other commercial damages or losses), even if such Contributor 410 | has been advised of the possibility of such damages. 411 | 412 | 9. Accepting Warranty or Additional Liability. While redistributing 413 | the Work or Derivative Works thereof, You may choose to offer, 414 | and charge a fee for, acceptance of support, warranty, indemnity, 415 | or other liability obligations and/or rights consistent with this 416 | License. However, in accepting such obligations, You may act only 417 | on Your own behalf and on Your sole responsibility, not on behalf 418 | of any other Contributor, and only if You agree to indemnify, 419 | defend, and hold each Contributor harmless for any liability 420 | incurred by, or claims asserted against, such Contributor by reason 421 | of your accepting any such warranty or additional liability. 422 | 423 | END OF TERMS AND CONDITIONS 424 | 425 | APPENDIX: How to apply the Apache License to your work. 426 | 427 | To apply the Apache License to your work, attach the following 428 | boilerplate notice, with the fields enclosed by brackets "[]" 429 | replaced with your own identifying information. (Don't include 430 | the brackets!) The text should be enclosed in the appropriate 431 | comment syntax for the file format. We also recommend that a 432 | file or class name and description of purpose be included on the 433 | same "printed page" as the copyright notice for easier 434 | identification within third-party archives. 435 | 436 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 437 | 438 | Licensed under the Apache License, Version 2.0 (the "License"); 439 | you may not use this file except in compliance with the License. 440 | You may obtain a copy of the License at 441 | 442 | http://www.apache.org/licenses/LICENSE-2.0 443 | 444 | Unless required by applicable law or agreed to in writing, software 445 | distributed under the License is distributed on an "AS IS" BASIS, 446 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 447 | See the License for the specific language governing permissions and 448 | limitations under the License. 449 | 450 | 451 | 452 | zone.js 453 | MIT 454 | The MIT License 455 | 456 | Copyright (c) 2010-2020 Google LLC. http://angular.io/license 457 | 458 | Permission is hereby granted, free of charge, to any person obtaining a copy 459 | of this software and associated documentation files (the "Software"), to deal 460 | in the Software without restriction, including without limitation the rights 461 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 462 | copies of the Software, and to permit persons to whom the Software is 463 | furnished to do so, subject to the following conditions: 464 | 465 | The above copyright notice and this permission notice shall be included in 466 | all copies or substantial portions of the Software. 467 | 468 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 469 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 470 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 471 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 472 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 473 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 474 | THE SOFTWARE. 475 | --------------------------------------------------------------------------------