зеркало из https://github.com/Azure/BatchExplorer.git
Added unit tests to increase coverage
This commit is contained in:
Родитель
57c05bfdf0
Коммит
9631a47219
|
@ -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,
|
||||
|
@ -216,6 +222,57 @@ 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¶m2=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¶m2=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);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче