Skip to content

Commit

Permalink
Use jest to test API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
irby committed Jun 26, 2023
1 parent 719f283 commit a6f6667
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/spa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"axios": "^1.4.0",
"ionicons": "^4.6.3",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
Expand Down
15 changes: 14 additions & 1 deletion src/spa/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';

describe('AppComponent', () => {
let fixture: ComponentFixture<AppComponent>;
let component: AppComponent;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
Expand All @@ -16,9 +19,19 @@ describe('AppComponent', () => {
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});

it('ngOnInit', () => {
component.ngOnInit();
});
});
5 changes: 0 additions & 5 deletions src/spa/src/app/create/create.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@
{{this.errorMessage}}
</p>
</div>
<div *ngIf="this.isSystemError">
<p style="color: red">
An unexpected error has occurred. Please try your request again later.
</p>
</div>
</div>

<div style="padding-top: 1.5em" *ngIf="this.secretCreationResponse !== null">
Expand Down
183 changes: 178 additions & 5 deletions src/spa/src/app/create/create.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, getTestBed, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { CreateComponent } from './create.component';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AngularMaterialModule } from '../angular-material';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SecretSubmissionResponse } from '../models/SecretSubmissionResponse';
import axios from 'axios';

describe('CreateComponent', () => {
let component: CreateComponent;
let fixture: ComponentFixture<CreateComponent>;
let compiled: any;
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CreateComponent ]
, imports: [ RouterTestingModule, HttpClientModule, AngularMaterialModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule ]
, imports: [ RouterTestingModule, HttpClientTestingModule, AngularMaterialModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule ]
})
.compileComponents();
}));
Expand All @@ -27,13 +30,42 @@ describe('CreateComponent', () => {
fixture = TestBed.createComponent(CreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
compiled = fixture.debugElement.nativeElement;
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('ngOnInit', () => {
it('should set timeExpiryOptions and expiryTimeInMinutes', () => {
component.timeExpiryOptions = [];
component.expiryTimeInMinutes = 0;

component.ngOnInit();

expect(component.timeExpiryOptions.length).toBe(5);

expect(component.timeExpiryOptions[0].displayText).toBe("30 minutes");
expect(component.timeExpiryOptions[0].timeInMinutes).toBe(30);

expect(component.timeExpiryOptions[1].displayText).toBe("1 hour");
expect(component.timeExpiryOptions[1].timeInMinutes).toBe(60);

expect(component.timeExpiryOptions[2].displayText).toBe("8 hours");
expect(component.timeExpiryOptions[2].timeInMinutes).toBe(480);

expect(component.timeExpiryOptions[3].displayText).toBe("24 hours");
expect(component.timeExpiryOptions[3].timeInMinutes).toBe(1440);

expect(component.timeExpiryOptions[4].displayText).toBe("7 days");
expect(component.timeExpiryOptions[4].timeInMinutes).toBe(10080);

expect(component.expiryTimeInMinutes).toBe(30);
});
});

describe('copyText', () => {
it('should call copy command and set isCopy to true', () => {
document.execCommand = jest.fn();
Expand Down Expand Up @@ -143,6 +175,147 @@ describe('CreateComponent', () => {
}
});

describe('submit', () => {
beforeEach(() => {
jest.mock('axios');
axios.post = jest.fn();
});

it('should clear properties', async() => {
const mockResponse: SecretSubmissionResponse = new SecretSubmissionResponse('TestId', 'TestKey', '2023-06-26T01:29:24.6827179+00:00', 1687742964);
axios.post = jest.fn().mockImplementationOnce(() => ({
type: 'data',
data: mockResponse
}));

component.errorMessage = 'Test error message';
component.isSystemError = true;
component.isCopied = true;

await component.submit();

expect(component.errorMessage).toBe('');
expect(component.isSystemError).toBe(false);
expect(component.isCopied).toBe(false);
});

it('when request success, populates with results from API call', async() => {

expect(component.secretCreationResponse).toBeFalsy();

const mockResponse: SecretSubmissionResponse = new SecretSubmissionResponse('TestId', 'TestKey', '2023-06-26T01:29:24.6827179+00:00', 1687742964);
axios.post = jest.fn().mockImplementationOnce(() => ({
type: 'data',
data: mockResponse
}));

await component.submit();

expect(component.secretCreationResponse).toBeTruthy();
expect(component.secretCreationResponse!.secretId).toBe(mockResponse.secretId);
expect(component.secretCreationResponse!.key).toBe(mockResponse.key);
expect(component.secretCreationResponse!.expireDateTime).toBe(mockResponse.expireDateTime);
expect(component.secretCreationResponse!.expireDateTimeEpoch).toBe(mockResponse.expireDateTimeEpoch);
});

it('when request fails with 400, shows error message defined in API call', async() => {
expect(component.secretCreationResponse).toBeFalsy();

axios.post = jest.fn().mockRejectedValueOnce({
response: {
data: {
message: 'A bad request!!'
},
status: 400,
statusText: 'Bad Request'
}
});

await component.submit();

expect(component.secretCreationResponse).toBeFalsy();
expect(component.errorMessage).toBe('A bad request!!');
});

it('when request fails with other status type, shows unexpected error occurred', async() => {
expect(component.secretCreationResponse).toBeFalsy();

axios.post = jest.fn().mockRejectedValueOnce({
response: {
data: {
message: 'Not found!!!'
},
status: 500
}
});

await component.submit();

expect(component.secretCreationResponse).toBeFalsy();
expect(component.errorMessage).toBe('An unexpected error has occured. Please try again.');
expect(component.isSystemError).toBe(true);
});
});

describe('reset', () => {
it('should reset properties to expected values', () => {
component.errorMessage = 'Error!';
component.secretCreationResponse = new SecretSubmissionResponse('', '', '', 0);
component.isSystemError = true;
component.isCopied = true;
component.secretText.setValue('');
component.isLoading = true;
component.charactersRemaining = 0;
component.expiryTimeInMinutes = 0;

component.reset();

expect(component.errorMessage).toBe('');
expect(component.secretCreationResponse).toBeNull();
expect(component.isSystemError).toBe(false);
expect(component.isCopied).toBe(false);
expect(component.secretText.value).toBe('');
expect(component.charactersRemaining).toBe(5000);
expect(component.expiryTimeInMinutes).toBe(30);
});
});

describe('onKeydownHandler', () => {
const tab: string = " ";

it('should insert tab when called from start', () => {
let secretInput: HTMLTextAreaElement = fixture.debugElement.query(By.css('#secretText')).nativeElement;
secretInput.value = "";
secretInput.selectionStart = 0;
fixture.detectChanges();
fixture.whenStable();

const event = new KeyboardEvent('keydown');
component.onKeydownHandler(event);
fixture.detectChanges();
fixture.whenStable();

secretInput = fixture.debugElement.query(By.css('#secretText')).nativeElement;
expect(secretInput.value).toBe(`${tab}`);
});

it('should insert tab when called between two elements', () => {
let secretInput: HTMLTextAreaElement = fixture.debugElement.query(By.css('#secretText')).nativeElement;
secretInput.value = "ab";
secretInput.selectionStart = 1;
fixture.detectChanges();
fixture.whenStable();

const event = new KeyboardEvent('keydown');
component.onKeydownHandler(event);
fixture.detectChanges();
fixture.whenStable();

secretInput = fixture.debugElement.query(By.css('#secretText')).nativeElement;
expect(secretInput.value).toBe(`a${tab}b`);
});
});

describe('changeExpiryTime', () => {
it('calling with value sets expiry time to value', () => {
component.expiryTimeInMinutes = 30;
Expand Down
47 changes: 25 additions & 22 deletions src/spa/src/app/create/create.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { environment } from '../../environments/environment';
import { SecretSubmissionRequest } from '../models/SecretSubmissionRequest';
import { SecretSubmissionResponse } from '../models/SecretSubmissionResponse';
import { TimeOption } from '../models/TimeOption';
import { HostListener } from '@angular/core';
import axios, { AxiosError } from 'axios';

@Component({
selector: 'app-create',
Expand Down Expand Up @@ -52,8 +53,8 @@ export class CreateComponent implements OnInit {

selectText() {
const textElement = document.getElementById("secretUrl") as HTMLInputElement;
textElement?.focus();
textElement?.select();
textElement!.focus();
textElement!.select();
}

valueChange() {
Expand All @@ -78,21 +79,23 @@ export class CreateComponent implements OnInit {
this.isSystemError = false;
this.isCopied = false;

await this.http.post<SecretSubmissionResponse>(environment.apiUrl + '/api/secrets',
new SecretSubmissionRequest(this.secretText.value, this.expiryTimeInMinutes)
).subscribe(data => {
this.secretCreationResponse = data;
const expiry = new Date(this.secretCreationResponse.expireDateTime);
const hours =
this.expireDateTime = `${expiry.getUTCMonth()+1}/${expiry.getUTCDate()}/${expiry.getUTCFullYear()} ${expiry.getUTCHours() < 10 ? '0':''}${expiry.getUTCHours()}:${expiry.getUTCMinutes() < 10 ? '0' : ''}${expiry.getUTCMinutes()}:${expiry.getUTCSeconds() < 10 ? '0' : ''}${expiry.getUTCSeconds()}`;
}, err => {
if(err.status === 400) {
this.errorMessage = err.error.message;
} else {
this.isSystemError = true;
}
this.errorMessage = err.error.message;
});
try {
const response = await axios.post<SecretSubmissionResponse>(environment.apiUrl + '/api/secrets',
new SecretSubmissionRequest(this.secretText.value, this.expiryTimeInMinutes));
this.secretCreationResponse = response.data;
}
catch (error: any) {
const err = error as AxiosError<HttpErrorResponse>;
const errorResponse = err.response!;

if (errorResponse.status === 400) {
this.errorMessage = errorResponse.data.message as string;
}
else {
this.errorMessage = "An unexpected error has occured. Please try again.";
this.isSystemError = true;
}
}

this.isLoading = false;
}
Expand All @@ -114,14 +117,14 @@ export class CreateComponent implements OnInit {
@HostListener('document:keydown.tab', ['$event'])
onKeydownHandler(event: KeyboardEvent) {
event.preventDefault();
const secretText = (document.getElementById("secretText") as HTMLInputElement);
const secretText = document.getElementById("secretText") as HTMLInputElement;

const cursorPosition = secretText.selectionStart;
const cursorPosition = secretText.selectionStart!;

const secretValue = secretText.value;

const firstHalf = secretValue.substring(0, cursorPosition!);
const secondHalf = secretValue.substring(cursorPosition!);
const firstHalf = secretValue.substring(0, cursorPosition);
const secondHalf = secretValue.substring(cursorPosition);

// Insert tab padding between the first half and second half
secretText.value = firstHalf + this.tabPadding + secondHalf;
Expand Down

0 comments on commit a6f6667

Please sign in to comment.