Commit 845be4e2 authored by Matt Teeter's avatar Matt Teeter

feat(browser, rx, security): incorporate browser-utils, rx-utils, and a part of angular-security

parent c5e07045
Pipeline #73981 failed with stages
in 7 minutes and 22 seconds
......@@ -73,10 +73,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/demo/tsconfig.app.json",
"apps/demo/tsconfig.spec.json"
],
"tsConfig": ["apps/demo/tsconfig.app.json", "apps/demo/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
},
......@@ -132,10 +129,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"libs/utils/tsconfig.lib.json",
"libs/utils/tsconfig.spec.json"
],
"tsConfig": ["libs/utils/tsconfig.lib.json", "libs/utils/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
},
......@@ -153,6 +147,90 @@
"styleext": "scss"
}
}
},
"utils-security": {
"projectType": "library",
"root": "libs/utils/security",
"sourceRoot": "libs/utils/security/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/security/tsconfig.lib.json", "libs/utils/security/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/security/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/security/jest.config.js",
"tsConfig": "libs/utils/security/tsconfig.spec.json",
"setupFile": "libs/utils/security/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
},
"utils-rx": {
"projectType": "library",
"root": "libs/utils/rx",
"sourceRoot": "libs/utils/rx/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/rx/tsconfig.lib.json", "libs/utils/rx/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/rx/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/rx/jest.config.js",
"tsConfig": "libs/utils/rx/tsconfig.spec.json",
"setupFile": "libs/utils/rx/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
},
"utils-browser": {
"projectType": "library",
"root": "libs/utils/browser",
"sourceRoot": "libs/utils/browser/src",
"prefix": "psu",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["libs/utils/browser/tsconfig.lib.json", "libs/utils/browser/tsconfig.spec.json"],
"exclude": ["**/node_modules/**", "!libs/utils/browser/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/utils/browser/jest.config.js",
"tsConfig": "libs/utils/browser/tsconfig.spec.json",
"setupFile": "libs/utils/browser/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"styleext": "scss"
}
}
}
},
"cli": {
......@@ -169,7 +247,10 @@
"e2eTestRunner": "cypress"
},
"@nrwl/angular:library": {
"unitTestRunner": "jest"
"unitTestRunner": "jest",
"directory": "utils",
"simpleModuleName": true,
"style": "scss"
},
"@psu/schematics:ng-new": {
"workspaceType": "library"
......
# utils
# Utils Library
This library was generated with [Nx](https://nx.dev).
Monorepo for building-block type utilities that don't have dependencies on any other `@psu` libraries.
## Running unit tests
- [Browser](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/browser)
- Cache Interceptor
- Autofocus Directive
- Error Service
- Loading Events Service
- Local Storage Service
- Request Tracing Interceptor
- UrlBuilder
- Window Service
- [Rx](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/rx)
- Http Retry Backoff Operator
- [Security](https://git.psu.edu/ais-swe/ux/utils/tree/develop/libs/utils/security)
- `REQUIRE_AUTH_HEADER` string constant
Run `ng test utils` to execute the unit tests.
# Contributing
Write small, portable utils, and try to keep the docs up to date as you add things.
See the README here for how to create a new entry point https://git.psu.edu/ais-swe/ux/components/blob/develop/README.md
# Browser Utils
## Cache Interceptor
Adds an always different `_d` param to any request which has the `NO_CACHE_HEADER` present. This header is only used by the client as a marker, and is deleted within this interceptor
Must be registered at the same level as `HttpClientModule`
### Exports
- `CacheInterceptor`
- `const NO_CACHE_HEADER = 'X-No-Cache'`
## Focus Directive
Autofocuses the host element
```html
<input type="text" utAutoFocus />
```
## Error Service
Provides a facade for handling http errors
```typescript
import { HttpErrorService } from '@psu/utils/browser';
http.get('some-url').pipe(
map(() => {}),
catchError(e => this.httpErrorService.handleError(e))
);
```
## Loading Events Service
Provides method to trigger a DOM event outside of angular that can be picked up by a loading screen defined in index.html.
```typescript
import { LoadingEvents } from '@psu/utils/browser';
```
## Local Storage Service
Provides an abstraction layer over various local storage areas
```typescript
import { LocalStorageService } from '@psu/utils/browser';
```
## Request Tracing Interceptor
Provides a client-only header, `TRACE_HEADER` that will enable server side request tracing on any request with said header attached.
Must be registered at the same level as `HttpClientModule`
## UrlBuilder
A class which normalizes alot of the pain around consistently building URLs
```typescript
import { UrlBuilder } from '@psu/utils/browser';
```
## Window Service
An abstraction over the browser's native window object.
```typescript
import { WindowService } from '@psu/utils/browser';
```
module.exports = {
name: 'utils-browser',
preset: '../../../jest.config.js',
coverageDirectory: '../../../coverage/libs/utils/browser',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};
{
"lib": {
"entryFile": "src/index.ts"
}
}
export { CacheInterceptor } from './lib/cache/cache.interceptor';
export { FocusDirective } from './lib/focus/focus.directive';
export { FocusModule } from './lib/focus/focus.module';
export { HttpErrorModule } from './lib/http-error/http-error.module';
export { HttpErrorService } from './lib/http-error/http-error.service';
export { LoadingEvents } from './lib/loading-screen/loading-events';
export { LoadingEventsModule } from './lib/loading-screen/loading-events.module';
export { LocalStorageModule } from './lib/local-storage/local-storage.module';
export { LocalStorageService } from './lib/local-storage/local-storage.service';
export { RequestTracingConfig } from './lib/tracing/request-tracing.config';
export { RequestTracingInterceptor } from './lib/tracing/request-tracing.interceptor';
export { UrlBuilder } from './lib/url-builder/url-builder';
export { WindowService } from './lib/window/window.service';
import { CacheInterceptor, NO_CACHE_HEADER } from './cache.interceptor';
import * as MockDate from 'mockdate';
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient, HttpRequest, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable()
class TestService {
private headers: HttpHeaders;
constructor(private httpClient: HttpClient) {
this.headers = new HttpHeaders().append(NO_CACHE_HEADER, 'true');
}
public getNoCache(): Observable<boolean> {
return this.httpClient.get<boolean>('busted', {
headers: this.headers.append('Another', 'abc')
});
}
public getNoCache2(): Observable<boolean> {
return this.httpClient.get<boolean>('busted2', {
headers: this.headers.append('Content-Type', 'junk'),
params: {
a: 'a',
b: 'b'
}
});
}
public get(): Observable<boolean> {
return this.httpClient.get<boolean>('normal');
}
}
describe('CacheInterceptor', () => {
let backend: HttpTestingController;
let service: TestService;
let now: Date;
beforeEach(() => {
now = new Date();
MockDate.set(now);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }, TestService]
});
service = TestBed.get(TestService);
backend = TestBed.get(HttpTestingController);
});
describe('intercept', () => {
describe('when request has cache bust header', () => {
beforeEach(() => {
service.getNoCache().subscribe(r => {
expect(r).toBeTruthy();
});
});
it('should add cache busting query param', () => {
const req = backend.expectOne((r: HttpRequest<any>) => {
return r.url === 'busted';
});
expect(req.request.params.get('_d')).toEqual(`${now.getTime()}`);
expect(req.request.headers.has('X-No-Cache')).toBe(false);
expect(req.request.headers.get('Another')).toBe('abc');
});
});
describe('when request has cache bust header and other query params', () => {
beforeEach(() => {
service.getNoCache2().subscribe(r => {
expect(r).toBeTruthy();
});
});
it('should add cache busting query param', () => {
const req = backend.expectOne((r: HttpRequest<any>) => {
return r.url === 'busted2';
});
expect(req.request.params.get('_d')).toEqual(`${now.getTime()}`);
expect(req.request.params.get('a')).toEqual('a');
expect(req.request.params.get('b')).toEqual('b');
expect(req.request.headers.has('X-No-Cache')).toBe(false);
expect(req.request.headers.get('Content-Type')).toBe('junk');
});
});
describe('when request does not have cache bust header', () => {
beforeEach(() => {
service.get().subscribe(r => {
expect(r).toBeTruthy();
});
});
it('should not add cache busting query param', () => {
const req = backend.expectOne('normal');
expect(req.request.params.has('_d')).toBe(false);
expect(req.request.headers.has('X-No-Cache')).toBe(false);
});
});
});
});
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export const NO_CACHE_HEADER = 'X-No-Cache';
@Injectable({ providedIn: 'root' })
export class CacheInterceptor implements HttpInterceptor {
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.has(NO_CACHE_HEADER)) {
const busted = req.clone({
setParams: {
_d: `${new Date().getTime()}`
},
headers: req.headers.delete(NO_CACHE_HEADER)
});
return next.handle(busted);
}
return next.handle(req);
}
}
import { CommonModule } from '@angular/common';
import { Component, PLATFORM_ID } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FocusModule } from './focus.module';
@Component({
selector: 'ut-test-comp',
template: `
<input id="inputId" utAutoFocus />
`
})
class TestComponent {}
describe('FocusDirective', () => {
let fixture: ComponentFixture<TestComponent>;
describe('when platform is browser', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FocusModule],
declarations: [TestComponent],
providers: [{ provide: PLATFORM_ID, useValue: 'browser' }]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should set focus', () => {
expect(document.activeElement.id).toEqual('inputId');
});
});
describe('when platform is server', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FocusModule],
declarations: [TestComponent],
providers: [{ provide: PLATFORM_ID, useValue: 'server' }]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should not set focus', () => {
expect(document.activeElement.id).not.toBe('inputId');
});
});
});
import { isPlatformBrowser } from '@angular/common';
import { AfterViewInit, Directive, ElementRef, Inject, PLATFORM_ID } from '@angular/core';
@Directive({
selector: '[utAutoFocus]'
})
export class FocusDirective implements AfterViewInit {
constructor(private element: ElementRef, @Inject(PLATFORM_ID) private platform: Object) {}
public ngAfterViewInit(): void {
if (isPlatformBrowser(this.platform)) {
this.element.nativeElement.focus();
}
}
}
import { NgModule } from '@angular/core';
import { FocusDirective } from './focus.directive';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule],
declarations: [FocusDirective],
exports: [FocusDirective]
})
export class FocusModule {}
import { HttpErrorResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { HttpErrorService } from './http-error.service';
describe('HttpErrorService', () => {
let service: HttpErrorService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [HttpErrorService]
});
service = TestBed.get(HttpErrorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('handleError', () => {
let result: string;
let emitted: boolean;
beforeEach(() => {
emitted = false;
});
describe('when error message is a string', () => {
beforeEach(() => {
service.handleError('some error').subscribe(() => (emitted = true), e => (result = e));
});
it('should not emit', () => {
expect(emitted).toBe(false);
});
it('should error with some error', () => {
expect(result).toBe('some error');
});
});
describe('when error message is specified', () => {
beforeEach(() => {
service
.handleError(
new HttpErrorResponse({
status: 404,
statusText: 'Not Found',
error: {
errorMessage: 'oh no'
}
})
)
.subscribe(() => (emitted = true), e => (result = e));
});
it('should not emit', () => {
expect(emitted).toBe(false);
});
it('should error with oh no', () => {
expect(result).toBe('oh no');
});
});
describe('when error message is not specified', () => {
beforeEach(() => {
service
.handleError(
new HttpErrorResponse({
status: 404,
statusText: 'Not Found',
error: {}
})
)
.subscribe(() => (emitted = true), e => (result = e));
});
it('should not emit', () => {
expect(emitted).toBe(false);
});
it('should error with oops', () => {
expect(result).toBe('Oops, something went wrong!');
});
});
describe('when error message is not specified with default', () => {
beforeEach(() => {
service
.handleError(
new HttpErrorResponse({
status: 404,
statusText: 'Not Found',
error: {}
}),
'my message'
)
.subscribe(() => (emitted = true), e => (result = e));
});
it('should not emit', () => {
expect(emitted).toBe(false);
});
it('should error with custom message', () => {
expect(result).toBe('my message');
});
});