Added unit tests to increase coverage

This commit is contained in:
Shiran Pasternak 2024-08-15 10:53:56 -04:00
Родитель 57c05bfdf0
Коммит 9631a47219
8 изменённых файлов: 674 добавлений и 11 удалений

Просмотреть файл

@ -0,0 +1,65 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormsModule } from "@angular/forms";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { By } from "@angular/platform-browser";
import { SlideToggleComponent } from "./slide-toggle.component";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
describe("SlideToggleComponent", () => {
let component: SlideToggleComponent;
let fixture: ComponentFixture<SlideToggleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SlideToggleComponent],
imports: [FormsModule, MatSlideToggleModule, NoopAnimationsModule]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SlideToggleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
it("should toggle checked state", () => {
const slideToggle = fixture.debugElement.query(By.css("mat-slide-toggle"));
slideToggle.triggerEventHandler("change", { checked: true });
fixture.detectChanges();
expect(component.checked).toBe(true);
slideToggle.triggerEventHandler("change", { checked: false });
fixture.detectChanges();
expect(component.checked).toBe(false);
});
it("should emit toggleChange event", () => {
spyOn(component.toggleChange, "emit");
const slideToggle = fixture.debugElement.query(By.css("mat-slide-toggle"));
slideToggle.triggerEventHandler("change", { checked: true });
fixture.detectChanges();
expect(component.toggleChange.emit).toHaveBeenCalledWith(true);
slideToggle.triggerEventHandler("change", { checked: false });
fixture.detectChanges();
expect(component.toggleChange.emit).toHaveBeenCalledWith(false);
});
it("should disable the slide toggle", () => {
component.isDisabled = true;
fixture.detectChanges();
const slideToggle = fixture.debugElement.query(By.css("mat-slide-toggle"));
expect(slideToggle.attributes["ng-reflect-disabled"]).toBe("true");
});
it("should call registerOnChange", () => {
const fn = jasmine.createSpy("onChangeCallback");
component.registerOnChange(fn);
component.valueChange(true);
expect(fn).toHaveBeenCalledWith(true);
});
});

Просмотреть файл

@ -45,6 +45,8 @@ describe("ProfileButtonComponent", () => {
notificationServiceSpy = new NotificationServiceMock();
authServiceSpy = {
currentUser: new BehaviorSubject(null),
login: jasmine.createSpy("login"),
logout: jasmine.createSpy("logout"),
};
autoUpdateServiceSpy = {
@ -133,6 +135,33 @@ describe("ProfileButtonComponent", () => {
expect(items.length).toBe(14);
});
describe("login/logout", () => {
it("shows sign-in when not signed in", () => {
click(clickableEl);
const items = contextMenuServiceSpy.lastMenu.items;
const signInItem = items[items.length - 1] as ContextMenuItem;
expect(signInItem.label).toBe("profile-button.sign-in");
// Perform the sign in
signInItem.click();
expect(authServiceSpy.login).toHaveBeenCalledOnce();
});
it("shows sign-out when signed in", () => {
authServiceSpy.currentUser.next({
name: "Some Name",
username: "somename"
});
click(clickableEl);
const items = contextMenuServiceSpy.lastMenu.items;
const signOutItem = items[items.length - 1] as ContextMenuItem;
expect(signOutItem.label).toBe("profile-button.sign-out");
// Perform the sign out
signOutItem.click();
expect(authServiceSpy.logout).toHaveBeenCalledOnce();
});
});
describe("Clicking on the profile", () => {
it("It shows a context menu", () => {
click(clickableEl);

Просмотреть файл

@ -5,7 +5,7 @@ import { By } from "@angular/platform-browser";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { UserConfigurationService } from "@batch-flask/core";
import { I18nTestingModule, MockUserConfigurationService } from "@batch-flask/core/testing";
import { FormModule, SelectComponent, SelectModule, ToolbarModule } from "@batch-flask/ui";
import { FormModule, SelectComponent, SelectModule, SlideToggleComponent, ToolbarModule } from "@batch-flask/ui";
import { ButtonComponent, ButtonsModule } from "@batch-flask/ui/buttons";
import { PermissionService } from "@batch-flask/ui/permission";
import { SettingsComponent } from "app/components/settings";
@ -25,6 +25,9 @@ describe("SettingsComponent", () => {
let resetButtonEl: DebugElement;
let resetButton: ButtonComponent;
let externalBrowserAuthToggleEl: DebugElement;
let externalBrowserAuthToggle: SlideToggleComponent;
let settingsServiceSpy: MockUserConfigurationService<BEUserDesktopConfiguration>;
let themeSelect: SelectComponent;
@ -54,6 +57,11 @@ describe("SettingsComponent", () => {
resetButtonEl = de.query(By.css("bl-button.reset"));
resetButton = resetButtonEl.componentInstance;
externalBrowserAuthToggleEl = de.query(
By.css("be-slide-toggle[formControlName=externalBrowserAuth]"));
externalBrowserAuthToggle =
externalBrowserAuthToggleEl.componentInstance;
themeSelect = de.query(By.css("bl-select[formControlName=theme]")).componentInstance;
});
@ -68,6 +76,17 @@ describe("SettingsComponent", () => {
expect(resetButton.disabled).toBe(false);
});
it("updates the externalBrowserAuth setting", fakeAsync(() => {
tick(400);
expect(settingsServiceSpy.current.externalBrowserAuth).toBe(true);
externalBrowserAuthToggleEl.triggerEventHandler("toggleChange", false);
tick(1000);
fixture.detectChanges();
expect(settingsServiceSpy.current.externalBrowserAuth).toBe(false);
tick(1000);
expect(settingsServiceSpy.current.externalBrowserAuth).toBe(false);
}));
it("updates the theme", fakeAsync(() => {
tick(400);
expect(settingsServiceSpy.current.theme).toEqual("classic");

Просмотреть файл

@ -0,0 +1,99 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Component, DebugElement, NO_ERRORS_SCHEMA } from "@angular/core";
import { of } from "rxjs";
import { AuthService } from "app/services";
import { TenantPickerComponent } from "./tenant-picker.component";
import { I18nTestingModule } from "@batch-flask/core/testing";
import { TenantCardComponent } from ".";
import { By } from "@angular/platform-browser";
@Component({
template: `<be-tenant-picker></be-tenant-picker>`,
})
class TestComponent {
}
describe("TenantPickerComponent", () => {
let fixture: ComponentFixture<TestComponent>;
let de: DebugElement;
let component: TenantPickerComponent;
let authServiceMock: any;
beforeEach(async () => {
authServiceMock = {
isLoggedIn: jasmine.createSpy("isLoggedIn").and.returnValue(of(true)),
getTenantAuthorizations:
jasmine.createSpy("getTenantAuthorizations")
.and.returnValue(of([]))
};
await TestBed.configureTestingModule({
imports: [
I18nTestingModule,
],
declarations: [
TenantPickerComponent,
TenantCardComponent,
TestComponent,
],
providers: [
{ provide: AuthService, useValue: authServiceMock }
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
de = fixture.debugElement.query(By.css("be-tenant-picker"));
component = de.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
it("should initialize tenantSettings form control", () => {
expect(component.tenantSettings.value).toEqual([]);
});
it("should call fetchTenantAuthorizations on initialization if logged in", () => {
expect(authServiceMock.isLoggedIn).toHaveBeenCalled();
expect(authServiceMock.getTenantAuthorizations).toHaveBeenCalled();
});
it("should propagate changes when tenantSettings value changes", () => {
const propagateChangeSpy = jasmine.createSpy("propagateChange");
component.registerOnChange(propagateChangeSpy);
const newValue = [{ tenantId: "1", authorization: "auth1" }];
component.tenantSettings.setValue(newValue);
expect(propagateChangeSpy).toHaveBeenCalledWith(newValue);
});
it("should call fetchTenantAuthorizations with reauthenticate on refresh", () => {
const fetchTenantAuthorizationsSpy =
spyOn<any>(component, "fetchTenantAuthorizations")
.and.callThrough();
component.refresh();
expect(fetchTenantAuthorizationsSpy).toHaveBeenCalledWith({ reauthenticate: true });
});
it("should call fetchTenantAuthorizations with specific tenant on refreshTenant", () => {
const fetchTenantAuthorizationsSpy =
spyOn<any>(component, "fetchTenantAuthorizations").and.callThrough();
const refreshData = { tenantId: "1", reauthenticate: true };
component.refreshTenant(refreshData);
expect(fetchTenantAuthorizationsSpy).toHaveBeenCalledWith(refreshData);
});
it("should clean up on destroy", () => {
const destroySpy = spyOn(component["_destroy"], "next");
const completeSpy = spyOn(component["_destroy"], "complete");
component.ngOnDestroy();
expect(destroySpy).toHaveBeenCalled();
expect(completeSpy).toHaveBeenCalled();
});
});

Просмотреть файл

@ -24,8 +24,14 @@ const token2 = new AccessToken({
tokenType: "Bearer"
});
describe("AuthService spec", () => {
let service: AuthService;
class AuthServiceStub extends AuthService {
public getTokenCache() {
return (this as any).tokenCache;
}
}
describe("AuthService", () => {
let service: AuthServiceStub;
let aadServiceSpy;
let remoteSpy;
let batchExplorerSpy;
@ -83,7 +89,7 @@ describe("AuthService spec", () => {
]
});
service = new AuthService(
service = new AuthServiceStub(
zoneSpy,
batchExplorerSpy,
remoteSpy,
@ -215,7 +221,58 @@ describe("AuthService spec", () => {
});
});
expect(remoteSpy.send).toHaveBeenCalledTimes(2);
});
it("returns cached token if available and not expired", (done) => {
const cachedToken = new AccessToken({
accessToken: "cachedToken",
expiresOn: DateTime.local().plus({ hours: 1 }).toJSDate(),
tokenType: "Bearer"
});
service.getTokenCache().storeToken(FakeTenants.One, resource1, cachedToken);
service.accessTokenData(FakeTenants.One, resource1).subscribe((token) => {
expect(token).toEqual(cachedToken);
done();
});
});
it("fetches a new token if no cached token is available", (done) => {
service.getTokenCache().removeToken(FakeTenants.One, resource1);
service.accessTokenData(FakeTenants.One, resource1).subscribe((token) => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(token).toEqual(token1);
done();
});
});
it("fetches a new token if cached token is expired", (done) => {
const expiredToken = new AccessToken({
accessToken: "expiredToken",
expiresOn: DateTime.local().minus({ hours: 1 }).toJSDate(),
tokenType: "Bearer"
});
service.getTokenCache().storeToken(FakeTenants.One, resource1, expiredToken);
service.accessTokenData(FakeTenants.One, resource1).subscribe((token) => {
expect(remoteSpy.send).toHaveBeenCalledOnce();
expect(token).toEqual(token1);
done();
});
});
it("handles errors correctly", (done) => {
remoteSpy.send.and.returnValue(Promise.reject("some-error"));
service.accessTokenData(FakeTenants.One, resource1).subscribe({
next: () => fail("Should not have a next() call"),
error: (error) => {
expect(error).toEqual("some-error");
done();
}
});
});
});
it("updates the tenants when updated by the auth service", () => {
@ -338,5 +395,137 @@ describe("AuthService spec", () => {
forceRefresh: true
});
});
it("doesn't notify on error if notifyOnError is false", async () => {
remoteSpy.send.and.callFake(async (_, { tenantId }) => {
if (tenantId === FakeTenants.One) {
throw new Error("Fake error for tenant-1");
} else {
return token2;
}
});
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active"
});
const authorizations = await auth({ notifyOnError: false });
expect(tenantErrorServiceSpy.showError).not.toHaveBeenCalled();
});
it("caches authorization state for failed tenants", async () => {
remoteSpy.send.and.callFake(async (_, { tenantId }) => {
if (tenantId === FakeTenants.One) {
throw new Error("Fake error for tenant-1");
} else {
return token2;
}
});
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active",
[FakeTenants.Two]: "active"
});
await auth();
remoteSpy.send.calls.reset();
const authorizations = await auth();
expect(remoteSpy.send).not.toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.One,
resource: null,
forceRefresh: false
});
expect(authorizations[0].status).toEqual("failed");
});
it("refreshes token if forceRefresh is true", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active"
});
await auth({ reauthenticate: FakeTenants.One });
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.One,
resource: null,
forceRefresh: true
});
});
it("does not refresh token if forceRefresh is false", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active"
});
await auth();
expect(remoteSpy.send).toHaveBeenCalledWith(
IpcEvent.AAD.accessTokenData, {
tenantId: FakeTenants.One,
resource: null,
forceRefresh: false
});
});
it("emits AuthComplete event after fetching token", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active"
});
const authCompleteSpy = jasmine.createSpy("authComplete");
service.on("AuthComplete", authCompleteSpy);
await auth();
expect(authCompleteSpy).toHaveBeenCalled();
});
it("does not emit AuthComplete event if fetching token fails", async () => {
tenantSettingsServiceSpy.current.next({
[FakeTenants.One]: "active"
});
const authCompleteSpy = jasmine.createSpy("authComplete");
service.on("AuthComplete", authCompleteSpy);
remoteSpy.send.and.callFake(async (_, { tenantId }) => {
if (tenantId === FakeTenants.One) {
throw new Error("Fake error for tenant-1");
} else {
return token2;
}
});
await auth();
expect(authCompleteSpy).not.toHaveBeenCalled();
});
});
describe("#logout", () => {
it("clears the token cache and emits Logout event", async () => {
const logoutSpy = jasmine.createSpy("logout");
service.on("Logout", logoutSpy);
await service.logout();
expect(remoteSpy.send).toHaveBeenCalledWith(IpcEvent.logout);
expect(service.getTokenCache().hasToken(FakeTenants.One, resource1)).toBeFalse();
expect(logoutSpy).toHaveBeenCalledOnce();
});
});
describe("#login", () => {
it("sends the login IPC event", async () => {
await service.login();
expect(remoteSpy.send).toHaveBeenCalledWith(IpcEvent.login);
});
});
describe("#isLoggedIn", () => {
it("returns true when user is logged in", (done) => {
aadServiceSpy.currentUser.next({ name: "test-user" });
service.isLoggedIn().subscribe((isLoggedIn) => {
expect(isLoggedIn).toBeTrue();
done();
});
});
it("returns false when user is not logged in", (done) => {
aadServiceSpy.currentUser.next(null);
service.isLoggedIn().subscribe((isLoggedIn) => {
expect(isLoggedIn).toBeFalse();
done();
});
});
});
});

Просмотреть файл

@ -0,0 +1,97 @@
import { ElectronTestingModule } from '@batch-flask/electron/testing';
import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { IpcService } from "@batch-flask/electron";
import { BatchAccountService } from "./batch-account";
import { AuthService } from "app/services";
import { NavigatorService } from "./navigator.service";
import { Constants, BatchExplorerLink, BatchExplorerLinkAction } from "common";
import { IpcEvent } from "common/constants";
import { RouterTestingModule } from "@angular/router/testing";
import { of } from 'rxjs';
describe("NavigatorService", () => {
let service: NavigatorService;
let accountService: jasmine.SpyObj<BatchAccountService>;
let router: jasmine.SpyObj<Router>;
let ipc: jasmine.SpyObj<IpcService>;
let authService: jasmine.SpyObj<AuthService>;
beforeEach(() => {
const accountServiceSpy = jasmine.createSpyObj("BatchAccountService", ["selectAccount"]);
const authServiceSpy = jasmine.createSpyObj("AuthService", ["showAuthSelect"]);
const ipcSpy = jasmine.createSpyObj("IpcService", ["on"]);
const routerSpy = jasmine.createSpyObj("Router", ["navigateByUrl"]);
ipcSpy.on.and.returnValue(of([null, null]));
TestBed.configureTestingModule({
imports: [ElectronTestingModule, RouterTestingModule],
providers: [
{ provide: BatchAccountService, useValue: accountServiceSpy },
{ provide: AuthService, useValue: authServiceSpy },
{ provide: IpcService, useValue: ipcSpy },
{ provide: Router, useValue: routerSpy },
]
});
accountService = TestBed.inject(BatchAccountService) as jasmine.SpyObj<BatchAccountService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
ipc = TestBed.inject(IpcService) as jasmine.SpyObj<IpcService>;
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
service = new NavigatorService(accountService, router, ipc, authService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should unsubscribe from _destroy on ngOnDestroy", () => {
const destroySpy = spyOn(service["_destroy"], "next");
const unsubscribeSpy = spyOn(service["_destroy"], "unsubscribe");
service.ngOnDestroy();
expect(destroySpy).toHaveBeenCalled();
expect(unsubscribeSpy).toHaveBeenCalled();
});
it("should initialize and subscribe to IPC events", () => {
service.init();
expect(ipc.on).toHaveBeenCalledWith(Constants.rendererEvents.batchExplorerLink);
expect(ipc.on).toHaveBeenCalledWith(Constants.rendererEvents.navigateTo);
expect(ipc.on).toHaveBeenCalledWith(IpcEvent.userAuthSelectRequest);
});
it("should handle openBatchExplorerLink with route action", () => {
const params = new URLSearchParams("param1=value1&param2=value2");
const link = new BatchExplorerLink({
action: BatchExplorerLinkAction.route,
path: "/some/path",
queryParams: params,
session: null,
accountId: "account-id"
});
spyOn(service, "goto");
service.openBatchExplorerLink(link);
expect(service.goto).toHaveBeenCalledWith("/some/path?param1=value1&param2=value2", { accountId: "account-id" });
});
it("should navigate to a route with goto method", async () => {
const route = "/some/path";
const options = { accountId: "account-id" };
accountService.selectAccount.and.returnValue(undefined);
router.navigateByUrl.and.returnValue(Promise.resolve(true));
const result = await service.goto(route, options);
expect(accountService.selectAccount).toHaveBeenCalledWith("account-id");
expect(router.navigateByUrl).toHaveBeenCalledWith(route);
expect(result).toBe(true);
});
});

Просмотреть файл

@ -0,0 +1,166 @@
import * as http from "http";
import { AuthLoopbackClient } from "./auth-loopback-client";
import { ServerAuthorizationCodeResponse } from "@azure/msal-node";
import { IncomingMessage, ServerResponse } from "http";
describe("AuthLoopbackClient", () => {
let authLoopbackClient: AuthLoopbackClient;
let serverSpy;
beforeEach(async () => {
authLoopbackClient = await AuthLoopbackClient.initialize(0);
});
afterEach(() => {
authLoopbackClient.closeServer();
if (serverSpy) {
serverSpy.close();
}
});
function createServerSpy(opts?: { listening?: boolean }) {
opts = opts || {};
if (opts.listening === undefined) {
opts.listening = true;
}
serverSpy = jasmine.createSpyObj("server",
["listen", "close", "emit", "address"]);
serverSpy.listen.and.returnValue({ on: () => jasmine.createSpy() });
spyOn(http, "createServer").and.callFake((callback) => {
serverSpy.serverCallback = callback;
return serverSpy;
});
serverSpy.listening = opts.listening;
}
it("should initialize with preferredPort as undefined", async () => {
const client = await AuthLoopbackClient.initialize(undefined);
expect(client.port).toBe(0);
});
it("should initialize with a valid preferredPort", async () => {
spyOn(authLoopbackClient, "isPortAvailable").and.returnValue(Promise.resolve(true));
const client = await AuthLoopbackClient.initialize(3000);
expect(client.port).toBe(3000);
});
it("should throw error if server is already initialized", () => {
createServerSpy();
authLoopbackClient.listenForAuthCode();
expectAsync(authLoopbackClient.listenForAuthCode())
.toBeRejectedWithError("Auth code listener already exists. Cannot create another.");
});
it("should initialize and listen for auth code", () => {
createServerSpy();
authLoopbackClient.listenForAuthCode();
expect(serverSpy.listen).toHaveBeenCalled();
});
it("should timeout if server does not start listening", () => {
createServerSpy({ listening: false });
expectAsync(authLoopbackClient.listenForAuthCode())
.toBeRejectedWithError("Timed out waiting for auth code listener to be registered.");
});
it("should throw error if server is not initialized in getRedirectUri", () => {
authLoopbackClient.closeServer();
expect(() => authLoopbackClient.getRedirectUri()).toThrowError("No auth code listener exists yet.");
});
it("should throw error if server address is invalid in getRedirectUri", () => {
const serverSpy = jasmine.createSpyObj("server", ["address", "close"]);
serverSpy.address.and.returnValue(null);
(authLoopbackClient as any).server = serverSpy;
expect(() => authLoopbackClient.getRedirectUri()).toThrowError("Failed to read auth code listener port");
});
it("should close the server", () => {
const serverSpy = jasmine.createSpyObj("server", ["close"]);
(authLoopbackClient as any).server = serverSpy;
authLoopbackClient.closeServer();
expect(serverSpy.close).toHaveBeenCalled();
});
it("should return true if port is available", async () => {
const result = await authLoopbackClient.isPortAvailable(3000);
expect(result).toBe(true);
});
it("should return false if port is unavailable", async () => {
createServerSpy();
serverSpy.listen.and.callFake(() => ({
on: (_, callback) => callback("Uhoh")
}));
const result = await authLoopbackClient.isPortAvailable(3000);
expect(result).toBe(false);
});
it("should return empty object for empty query in getDeserializedQueryString", () => {
const result = AuthLoopbackClient.getDeserializedQueryString("");
expect(result).toEqual({});
});
it("should return a valid redirect URI", () => {
createServerSpy();
serverSpy.address.and.returnValue({ port: 3000 });
authLoopbackClient.listenForAuthCode();
const redirectUri = authLoopbackClient.getRedirectUri();
expect(redirectUri).toMatch(/http:\/\/localhost:\d+/);
});
it("should parse query string correctly", () => {
const queryString = "code=authcode&state=state";
const parsedObject = AuthLoopbackClient.queryStringToObject(queryString);
expect(parsedObject).toEqual({
code: "authcode",
state: "state"
});
});
it("should deserialize query string correctly", () => {
const queryString = "code=authcode&state=state";
const deserializedObject = AuthLoopbackClient.getDeserializedQueryString(queryString);
expect(deserializedObject).toEqual({
code: "authcode",
state: "state"
});
});
it("should check if port is available", async () => {
const isAvailable = await authLoopbackClient.isPortAvailable(0);
expect(isAvailable).toBe(true);
});
it("should listen for auth code and return the response", async () => {
const successTemplate = "Success";
const errorTemplate = "Error";
createServerSpy();
spyOn(authLoopbackClient, "getRedirectUri").and.returnValue("http://localhost:3000");
const authCodeResponse: ServerAuthorizationCodeResponse = {
code: "authcode",
state: "state"
};
spyOn(AuthLoopbackClient, "getDeserializedQueryString").and.returnValue(authCodeResponse);
const listenPromise = authLoopbackClient.listenForAuthCode(successTemplate, errorTemplate);
// Simulate server behavior
const req = { url: "/?code=authcode&state=state" } as IncomingMessage;
const res = {
end: jasmine.createSpy("end"),
writeHead: jasmine.createSpy("writeHead")
} as unknown as ServerResponse;
// Invoke the captured callback with the mock request and response
serverSpy.serverCallback(req, res);
const result = await listenPromise;
expect(result).toEqual(authCodeResponse);
expect(res.writeHead).toHaveBeenCalledWith(302, { location: "http://localhost:3000" });
});
});

Просмотреть файл

@ -1,7 +1,6 @@
import { log } from "@batch-flask/utils";
import { IncomingMessage, Server, ServerResponse, createServer } from "http";
import { ILoopbackClient, ServerAuthorizationCodeResponse } from "@azure/msal-node";
import type { AddressInfo } from "net";
/**
* Listen for an auth code response on the loopback address. Will use a preferred
@ -44,7 +43,7 @@ export class AuthLoopbackClient implements ILoopbackClient {
*/
async listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise<ServerAuthorizationCodeResponse> {
if (!!this.server) {
throw new Error('Auth code listener already exists. Cannot create another.');
throw new Error("Auth code listener already exists. Cannot create another.");
}
const authCodeListener = new Promise<ServerAuthorizationCodeResponse>((resolve, reject) => {
@ -52,7 +51,7 @@ export class AuthLoopbackClient implements ILoopbackClient {
const url = req.url;
if (!url) {
res.end(errorTemplate || "Login failed: Error occurred loading redirectUrl");
reject(new Error('Auth code listener callback was invoked without a url.'));
reject(new Error("Auth code listener callback was invoked without a url."));
return;
} else if (url === "/") {
res.end(successTemplate || "Successfully logged in to Batch Explorer. You may close this window.");
@ -61,7 +60,7 @@ export class AuthLoopbackClient implements ILoopbackClient {
const authCodeResponse = AuthLoopbackClient.getDeserializedQueryString(url);
if (authCodeResponse.code) {
const redirectUri = await this.getRedirectUri();
const redirectUri = this.getRedirectUri();
res.writeHead(302, { location: redirectUri }); // Prevent auth code from being saved in the browser history
res.end();
}
@ -76,7 +75,7 @@ export class AuthLoopbackClient implements ILoopbackClient {
let ticks = 0;
const id = setInterval(() => {
if ((5000 / 100) < ticks) {
throw new Error('Timed out waiting for auth code listener to be registered.');
throw new Error("Timed out waiting for auth code listener to be registered.");
}
if (this.server.listening) {
@ -96,7 +95,7 @@ export class AuthLoopbackClient implements ILoopbackClient {
*/
getRedirectUri(): string {
if (!this.server) {
throw new Error('No auth code listener exists yet.');
throw new Error("No auth code listener exists yet.");
}
const addressInfo = this.server.address();
@ -185,7 +184,7 @@ export class AuthLoopbackClient implements ILoopbackClient {
const decode = (s: string) => decodeURIComponent(s.replace(/\+/g, " "));
params.forEach((pair) => {
if (pair.trim()) {
const [key, value] = pair.split(/=(.+)/g, 2); // Split on the first occurence of the '=' character
const [key, value] = pair.split(/=(.+)/g, 2); // Split on the first occurence of the "=" character
if (key && value) {
obj[decode(key)] = decode(value);
}