import { MockProxy, mock } from "jest-mock-extended"; import { ReplaySubject, skip, take } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; import { StateService } from "../../abstractions/state.service"; import { ServerConfigData } from "../../models/data/server-config.data"; import { EnvironmentServerConfigResponse, ServerConfigResponse, ThirdPartyServerConfigResponse, } from "../../models/response/server-config.response"; import { StateProvider } from "../../state"; import { ConfigService } from "./config.service"; describe("ConfigService", () => { let stateService: MockProxy; let configApiService: MockProxy; let authService: MockProxy; let environmentService: MockProxy; let logService: MockProxy; let replaySubject: ReplaySubject; let stateProvider: StateProvider; let serverResponseCount: number; // increments to track distinct responses received from server // Observables will start emitting as soon as this is created, so only create it // after everything is mocked const configServiceFactory = () => { const configService = new ConfigService( stateService, configApiService, authService, environmentService, logService, stateProvider, ); configService.init(); return configService; }; beforeEach(() => { stateService = mock(); configApiService = mock(); authService = mock(); environmentService = mock(); logService = mock(); replaySubject = new ReplaySubject(1); const accountService = mockAccountServiceWith("0" as UserId); stateProvider = new FakeStateProvider(accountService); environmentService.environment$ = replaySubject.asObservable(); serverResponseCount = 1; configApiService.get.mockImplementation(() => Promise.resolve(serverConfigResponseFactory("server" + serverResponseCount++)), ); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it("Uses storage as fallback", (done) => { const storedConfigData = serverConfigDataFactory("storedConfig"); stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch")); const configService = configServiceFactory(); configService.serverConfig$.pipe(take(1)).subscribe((config) => { expect(config).toEqual(new ServerConfig(storedConfigData)); expect(stateService.getServerConfig).toHaveBeenCalledTimes(1); expect(stateService.setServerConfig).not.toHaveBeenCalled(); done(); }); configService.triggerServerConfigFetch(); }); it("Stream does not error out if fetch fails", (done) => { const storedConfigData = serverConfigDataFactory("storedConfig"); stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); const configService = configServiceFactory(); configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => { try { expect(config.gitHash).toEqual("server1"); done(); } catch (e) { done(e); } }); configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch")); configService.triggerServerConfigFetch(); configApiService.get.mockResolvedValueOnce(serverConfigResponseFactory("server1")); configService.triggerServerConfigFetch(); }); describe("Fetches config from server", () => { beforeEach(() => { stateService.getServerConfig.mockResolvedValueOnce(null); }); it.each([1, 2, 3])( "after %p hour/s", (hours: number, done: jest.DoneCallback) => { const configService = configServiceFactory(); // skip previous hours (if any) configService.serverConfig$.pipe(skip(hours - 1), take(1)).subscribe((config) => { try { expect(config.gitHash).toEqual("server" + hours); expect(configApiService.get).toHaveBeenCalledTimes(hours); done(); } catch (e) { done(e); } }); const oneHourInMs = 1000 * 3600; jest.advanceTimersByTime(oneHourInMs * hours + 1); }, ); it("when environment URLs change", (done) => { const configService = configServiceFactory(); configService.serverConfig$.pipe(take(1)).subscribe((config) => { try { expect(config.gitHash).toEqual("server1"); done(); } catch (e) { done(e); } }); replaySubject.next(null); }); it("when triggerServerConfigFetch() is called", (done) => { const configService = configServiceFactory(); configService.serverConfig$.pipe(take(1)).subscribe((config) => { try { expect(config.gitHash).toEqual("server1"); done(); } catch (e) { done(e); } }); configService.triggerServerConfigFetch(); }); }); it("Saves server config to storage when the user is logged in", (done) => { stateService.getServerConfig.mockResolvedValueOnce(null); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked); const configService = configServiceFactory(); configService.serverConfig$.pipe(take(1)).subscribe(() => { try { expect(stateService.setServerConfig).toHaveBeenCalledWith( expect.objectContaining({ gitHash: "server1" }), ); done(); } catch (e) { done(e); } }); configService.triggerServerConfigFetch(); }); }); function serverConfigDataFactory(gitHash: string) { return new ServerConfigData(serverConfigResponseFactory(gitHash)); } function serverConfigResponseFactory(gitHash: string) { return new ServerConfigResponse({ version: "myConfigVersion", gitHash: gitHash, server: new ThirdPartyServerConfigResponse({ name: "myThirdPartyServer", url: "www.example.com", }), environment: new EnvironmentServerConfigResponse({ vault: "vault.example.com", }), featureStates: { feat1: "off", feat2: "on", feat3: "off", }, }); }