src/app/core/session/session-service/session-manager.service.spec.ts
/*
* This file is part of ndb-core.
*
* ndb-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ndb-core is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/
import { SessionManagerService } from "./session-manager.service";
import { LoginState } from "../session-states/login-state.enum";
import {
LoginStateSubject,
SessionType,
SyncStateSubject,
} from "../session-type";
import { TestBed, waitForAsync } from "@angular/core/testing";
import { PouchDatabase } from "../../database/pouch-database";
import { environment } from "../../../../environments/environment";
import { SessionInfo, SessionSubject } from "../auth/session-info";
import { LocalAuthService } from "../auth/local/local-auth.service";
import { SyncService } from "../../database/sync.service";
import { KeycloakAuthService } from "../auth/keycloak/keycloak-auth.service";
import { Database } from "../../database/database";
import { Router } from "@angular/router";
import { NAVIGATOR_TOKEN } from "../../../utils/di-tokens";
import { CurrentUserSubject } from "../current-user-subject";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { mockEntityMapper } from "../../entity/entity-mapper/mock-entity-mapper-service";
import { TEST_USER } from "../../user/demo-user-generator.service";
import { Config } from "../../config/config";
import { TestEntity } from "../../../utils/test-utils/TestEntity";
describe("SessionManagerService", () => {
let service: SessionManagerService;
let loginStateSubject: LoginStateSubject;
let sessionInfo: SessionSubject;
let mockKeycloak: jasmine.SpyObj<KeycloakAuthService>;
let mockNavigator: { onLine: boolean };
let dbUser: SessionInfo;
const userDBName = `${TEST_USER}-${environment.DB_NAME}`;
const deprecatedDBName = environment.DB_NAME;
let initInMemorySpy: jasmine.Spy;
let initIndexedSpy: jasmine.Spy;
beforeEach(waitForAsync(() => {
dbUser = { name: TEST_USER, id: "99", roles: ["user_app"] };
mockKeycloak = jasmine.createSpyObj(["login", "logout", "addAuthHeader"]);
mockKeycloak.login.and.resolveTo(dbUser);
mockNavigator = { onLine: true };
TestBed.configureTestingModule({
providers: [
SessionManagerService,
SyncStateSubject,
LoginStateSubject,
SessionSubject,
CurrentUserSubject,
{
provide: EntityMapperService,
useValue: mockEntityMapper(),
},
{ provide: Database, useClass: PouchDatabase },
{ provide: KeycloakAuthService, useValue: mockKeycloak },
{ provide: NAVIGATOR_TOKEN, useValue: mockNavigator },
{
provide: Router,
useValue: {
navigate: () => Promise.resolve(),
routerState: { snapshot: {} },
},
},
],
});
service = TestBed.inject(SessionManagerService);
loginStateSubject = TestBed.inject(LoginStateSubject);
sessionInfo = TestBed.inject(SessionSubject);
const db = TestBed.inject(Database) as PouchDatabase;
initInMemorySpy = spyOn(db, "initInMemoryDB").and.callThrough();
initIndexedSpy = spyOn(db, "initIndexedDB").and.callThrough();
spyOn(TestBed.inject(SyncService), "startSync").and.callFake(() =>
TestBed.inject(EntityMapperService).save(
new Config(Config.CONFIG_KEY, {}),
),
);
TestBed.inject(LocalAuthService).saveUser(dbUser);
environment.session_type = SessionType.mock;
spyOn(service, "remoteLoginAvailable").and.returnValue(true);
}));
afterEach(async () => {
localStorage.clear();
const tmpDB = new PouchDatabase();
await tmpDB.initInMemoryDB(userDBName).destroy();
await tmpDB.initInMemoryDB(deprecatedDBName).destroy();
});
it("should update the session info once authenticated", async () => {
const updatedUser: SessionInfo = {
name: TEST_USER,
id: "101",
roles: dbUser.roles.concat("admin"),
};
mockKeycloak.login.and.resolveTo(updatedUser);
const saveUserSpy = spyOn(TestBed.inject(LocalAuthService), "saveUser");
await service.remoteLogin();
expect(saveUserSpy).toHaveBeenCalledWith(updatedUser);
expect(sessionInfo.value).toEqual(updatedUser);
expect(TestBed.inject(SyncService).startSync).toHaveBeenCalled();
expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
});
it("should initialize current user as the entity to which a login is connected", async () => {
const entityMapper = TestBed.inject(EntityMapperService);
const loggedInUser = new TestEntity(TEST_USER);
const otherUser = new TestEntity("other_user");
await entityMapper.saveAll([loggedInUser, otherUser]);
const currentUser = TestBed.inject(CurrentUserSubject);
// first login with existing user entity
mockKeycloak.login.and.resolveTo({
name: TEST_USER,
id: "101",
roles: [],
entityId: loggedInUser.getId(),
});
await service.remoteLogin();
expect(currentUser.value).toEqual(loggedInUser);
// logout -> user should reset
await service.logout();
expect(currentUser.value).toBeUndefined();
const adminUser = new TestEntity("admin-user");
// login, user entity not available yet
mockKeycloak.login.and.resolveTo({
name: "admin-user",
id: "101",
roles: ["admin"],
entityId: adminUser.getId(),
});
await service.remoteLogin();
expect(currentUser.value).toBeUndefined();
// user entity available -> user should be set
await entityMapper.save(adminUser);
expect(currentUser.value).toEqual(adminUser);
});
it("should not initialize the user entity if no entityId is set", async () => {
const loadSpy = spyOn(TestBed.inject(EntityMapperService), "load");
mockKeycloak.login.and.resolveTo({
name: "some-user",
id: "101",
roles: [],
});
await service.remoteLogin();
expect(loadSpy).not.toHaveBeenCalled();
expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
expect(TestBed.inject(CurrentUserSubject).value).toBeUndefined();
});
it("should allow other entities to log in", async () => {
const loggedInChild = new TestEntity("123");
const childSession: SessionInfo = {
name: loggedInChild.getId(),
id: "101",
roles: [],
entityId: loggedInChild.getId(),
};
mockKeycloak.login.and.resolveTo(childSession);
const otherChild = new TestEntity("456");
await TestBed.inject(EntityMapperService).saveAll([
loggedInChild,
otherChild,
]);
await service.remoteLogin();
expect(sessionInfo.value).toBe(childSession);
expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
expect(TestBed.inject(CurrentUserSubject).value).toEqual(loggedInChild);
});
it("should automatically login, if the session is still valid", async () => {
await service.remoteLogin();
expect(loginStateSubject.value).toEqual(LoginState.LOGGED_IN);
expect(sessionInfo.value).toEqual(dbUser);
});
it("should trigger remote logout if remote login succeeded before", async () => {
await service.remoteLogin();
service.logout();
expect(mockKeycloak.logout).toHaveBeenCalled();
});
it("should only reset local state if remote login did not happen", async () => {
const navigateSpy = spyOn(TestBed.inject(Router), "navigate");
await service.offlineLogin(dbUser);
expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
expect(sessionInfo.value).toEqual(dbUser);
service.logout();
expect(mockKeycloak.logout).not.toHaveBeenCalled();
expect(loginStateSubject.value).toBe(LoginState.LOGGED_OUT);
expect(sessionInfo.value).toBeUndefined();
expect(navigateSpy).toHaveBeenCalled();
});
it("should store information if remote session needs to be reset", async () => {
await service.remoteLogin();
mockNavigator.onLine = false;
await service.logout();
expect(
localStorage.getItem(service.RESET_REMOTE_SESSION_KEY),
).toBeDefined();
});
it("should trigger a remote logout if reset flag has been set", async () => {
localStorage.setItem(service.RESET_REMOTE_SESSION_KEY, "true");
service.clearRemoteSessionIfNecessary();
expect(mockKeycloak.logout).toHaveBeenCalled();
});
it("should create a pouchdb with the username of the logged in user", async () => {
await service.remoteLogin();
expect(initInMemorySpy).toHaveBeenCalledWith(
TEST_USER + "-" + environment.DB_NAME,
);
});
it("should create the database according to the session type in the environment", async () => {
async function testDatabaseCreation(
sessionType: SessionType,
expectedDB: "inMemory" | "indexed",
) {
initInMemorySpy.calls.reset();
initIndexedSpy.calls.reset();
environment.session_type = sessionType;
await service.remoteLogin();
if (expectedDB === "inMemory") {
expect(initInMemorySpy).toHaveBeenCalled();
expect(initIndexedSpy).not.toHaveBeenCalled();
} else {
expect(initInMemorySpy).not.toHaveBeenCalled();
expect(initIndexedSpy).toHaveBeenCalled();
}
}
await testDatabaseCreation(SessionType.mock, "inMemory");
await testDatabaseCreation(SessionType.local, "indexed");
await testDatabaseCreation(SessionType.synced, "indexed");
});
it("should use current user db if database has content", async () => {
await defineExistingDatabases(true, false);
await service.remoteLogin();
expect(initInMemorySpy).toHaveBeenCalledOnceWith(userDBName);
});
it("should use and reserve a deprecated db if it exists and current db has no content", async () => {
await defineExistingDatabases(false, true);
await service.remoteLogin();
expect(initInMemorySpy).toHaveBeenCalledOnceWith(deprecatedDBName);
const dbReservation = window.localStorage.getItem(
service.DEPRECATED_DB_KEY,
);
expect(dbReservation).toBe(TEST_USER);
});
it("should open a new database if deprecated db is already in use", async () => {
await defineExistingDatabases(false, true, "other-user");
await service.remoteLogin();
expect(initInMemorySpy).toHaveBeenCalledOnceWith(userDBName);
});
it("should use the deprecated database if it is reserved by the current user", async () => {
await defineExistingDatabases(false, true, TEST_USER);
await service.remoteLogin();
expect(initInMemorySpy).toHaveBeenCalledOnceWith(deprecatedDBName);
});
async function defineExistingDatabases(
initUserDB: boolean,
initDeprecatedDB: boolean,
reserved?: string,
) {
if (reserved) {
window.localStorage.setItem(service.DEPRECATED_DB_KEY, reserved);
}
const tmpDB = new PouchDatabase();
if (initUserDB) {
await tmpDB.initInMemoryDB(userDBName).put({ _id: "someDoc" });
}
if (initDeprecatedDB) {
await tmpDB.initInMemoryDB(deprecatedDBName).put({ _id: "someDoc" });
}
}
});