src/app/core/database/pouch-database.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 { PouchDatabase } from "./pouch-database";
import { fakeAsync, tick } from "@angular/core/testing";
import PouchDB from "pouchdb-browser";
import { HttpStatusCode } from "@angular/common/http";
describe("PouchDatabase tests", () => {
let database: PouchDatabase;
beforeEach(() => {
database = PouchDatabase.create();
});
afterEach(() => database.destroy());
it("get object by _id after put into database", async () => {
const id = "test_id";
const name = "test";
const count = 42;
const testData = { _id: id, name: name, count: count };
await database.put(testData);
const resultData = await database.get(id);
expect(resultData).toEqual(
jasmine.objectContaining({
_id: id,
name: name,
count: count,
}),
);
});
it("put two objects with different _id", async () => {
const data1 = { _id: "test_id", name: "test" };
const data2 = { _id: "other_id", name: "test2" };
await database.put(data1);
await database.put(data2);
const result = await database.get(data1._id);
expect(result._id).toBe(data1._id);
const result2 = await database.get(data2._id);
expect(result2._id).toBe(data2._id);
});
it("fails to get by not existing _id", () => {
return expectAsync(database.get("some_id")).toBeRejected();
});
it("rejects putting new object with existing _id and no _rev with forceOverwrite being false", async () => {
const testData = { _id: "test_id", name: "test", count: 42 };
const duplicateData = { _id: "test_id", name: "duplicate", count: 43 };
await database.put(testData);
const result = await database.get(testData._id);
expect(result._id).toBe(testData._id);
await expectAsync(database.put(duplicateData)).toBeRejected();
const result2 = await database.get(testData._id);
expect(result2.name).toBe(testData.name);
});
it("allows overwriting an existing object with existing _id and no _rev with forceOverwrite being true", async () => {
const testData = { _id: "test_id", name: "test", count: 42 };
const duplicateData = { _id: "test_id", name: "duplicate", count: 43 };
await database.put(testData);
const result = await database.get(testData._id);
expect(result._id).toBe(testData._id);
await expectAsync(database.put(duplicateData, true)).toBeResolved();
const result2 = await database.get(testData._id);
expect(result2.name).toBe(duplicateData.name);
});
it("allows overwriting object with existing _id and a wrong _rev with forceOverwrite being true", async () => {
const testData = { _id: "test_id", name: "test" };
const duplicateData = {
_id: "test_id",
name: "duplicate",
_rev: "1234blabla",
};
await database.put(testData);
const result = await database.get(testData._id);
expect(result._id).toBe(testData._id);
await expectAsync(database.put(duplicateData, true)).toBeResolved();
const result2 = await database.get(testData._id);
expect(result2.name).toBe(duplicateData.name);
});
it("allows saving a new object even when the _rev field is set with forceOverwrite being true", async () => {
const testData = { _id: "test_id", name: "test", _rev: "1234blabla" };
await expectAsync(database.put(testData, true)).toBeResolved();
const result2 = await database.get(testData._id);
expect(result2.name).toBe(testData.name);
});
it("removes an existing object", async () => {
const id = "test_id";
const name = "test";
const count = 42;
const testData = { _id: id, name: name, count: count };
await database.put(testData);
await expectAsync(database.get(testData._id)).toBeResolved();
const savedDocument = await database.get(testData._id);
await database.remove(savedDocument);
await expectAsync(database.get(testData._id)).toBeRejected();
});
it("returns undefined on get by not existing entityId with returnUndefined parameter", async () => {
const result = await database.get("some_id", {}, true);
expect(result).toBeUndefined();
});
it("getAll returns all objects", async () => {
const testData1 = { _id: "x:test_id", name: "test", count: 42 };
const testData2 = { _id: "y:two", name: "two" };
await database.put(testData1);
await database.put(testData2);
const result = await database.getAll();
expect(result.map((el) => el._id)).toContain(testData1._id);
expect(result.map((el) => el._id)).toContain(testData2._id);
expect(result).toHaveSize(2);
});
it("getAll returns prefixed objects", async () => {
const testData1 = { _id: "x:test_id", name: "test", count: 42 };
const testData2 = { _id: "y:two", name: "two" };
const prefix = "x";
await database.put(testData1);
await database.put(testData2);
const result = await database.getAll(prefix);
expect(result).toHaveSize(1);
expect(result.map((el) => el._id)).toContain(testData1._id);
expect(result.map((el) => el._id)).not.toContain(testData2._id);
});
it("saveDatabaseIndex creates new index", async () => {
const testIndex = { _id: "_design/test_index", views: { a: {}, b: {} } };
spyOn(database, "put").and.resolveTo();
const spyOnQuery = spyOn(database, "query").and.resolveTo();
await database.saveDatabaseIndex(testIndex);
expect(database.put).toHaveBeenCalledWith(testIndex, true);
// expect all indices to be queried
expect(spyOnQuery).toHaveBeenCalledTimes(2);
expect(spyOnQuery.calls.allArgs()).toEqual([
["test_index/a", { key: "1" }],
["test_index/b", { key: "1" }],
]);
});
it("saveDatabaseIndex updates existing index", async () => {
const testIndex = { _id: "_design/test_index", views: { a: {}, b: {} } };
await database.put({
_id: testIndex._id,
views: { a: {} },
});
const existingIndex = await database.get(testIndex._id);
spyOn(database, "put").and.resolveTo();
const spyOnQuery = spyOn(database, "query").and.resolveTo();
await database.saveDatabaseIndex(testIndex);
expect(database.put).toHaveBeenCalledWith(
{
_id: testIndex._id,
_rev: existingIndex._rev,
views: testIndex.views,
},
true,
);
// expect all indices to be queried
expect(spyOnQuery).toHaveBeenCalledTimes(2);
expect(spyOnQuery.calls.allArgs()).toEqual([
["test_index/a", { key: "1" }],
["test_index/b", { key: "1" }],
]);
});
it("saveDatabaseIndex does not update unchanged index", async () => {
const testIndex = { _id: "_design/test_index", views: { a: {}, b: {} } };
const existingIndex = {
_id: "_design/test_index",
views: testIndex.views,
};
await database.put(existingIndex);
spyOn(database, "put").and.resolveTo();
await database.saveDatabaseIndex(testIndex);
expect(database.put).not.toHaveBeenCalled();
});
it("query simply calls through to pouchDB query", async () => {
const testQuery = "testquery";
const testQueryResults = { rows: [] } as any;
const pouchDB = database.getPouchDB();
spyOn(pouchDB, "query").and.resolveTo(testQueryResults);
const result = await database.query(testQuery, {});
expect(result).toEqual(testQueryResults);
expect(pouchDB.query).toHaveBeenCalledWith(testQuery, {});
});
it("writes all the docs to the database with putAll", async () => {
await database.putAll([
{
_id: "5",
name: "The Grinch",
},
{
_id: "8",
name: "Santa Claus",
},
]);
await expectAsync(database.get("5")).toBeResolvedTo(
jasmine.objectContaining({
_id: "5",
name: "The Grinch",
}),
);
await expectAsync(database.get("8")).toBeResolvedTo(
jasmine.objectContaining({
_id: "8",
name: "Santa Claus",
}),
);
});
it("Throws errors for each conflict individually", async () => {
const resolveConflictSpy = spyOn<any>(database, "resolveConflict");
const conflictError = new Error();
resolveConflictSpy.and.rejectWith(conflictError);
await database.put({
_id: "3",
name: "Rudolph, the Red-Nosed Reindeer",
});
const dataWithConflicts = [
{
_id: "3",
name: "Rudolph, the Pink-Nosed Reindeer",
_rev: "1-invalid_rev",
},
{
_id: "4",
name: "Dasher",
},
{
_id: "5",
name: "Dancer",
},
];
await expectAsync(database.putAll(dataWithConflicts)).toBeRejectedWith([
conflictError,
jasmine.objectContaining({ id: "4", ok: true }),
jasmine.objectContaining({ id: "5", ok: true }),
]);
expect(resolveConflictSpy.calls.allArgs()).toEqual([
[
{
_id: "3",
name: "Rudolph, the Pink-Nosed Reindeer",
_rev: "1-invalid_rev",
},
false,
jasmine.objectContaining({ status: 409 }),
],
]);
});
it("should correctly determine if database is empty", async () => {
await expectAsync(database.isEmpty()).toBeResolvedTo(true);
await database.put({ _id: "User:test" });
await expectAsync(database.isEmpty()).toBeResolvedTo(false);
});
it("should try auto-login if fetch fails and fetch again", fakeAsync(() => {
const mockAuthService = jasmine.createSpyObj(["login", "addAuthHeader"]);
database = new PouchDatabase(mockAuthService);
database.initRemoteDB("");
mockAuthService.login.and.resolveTo();
// providing "valid" token on second call
let calls = 0;
mockAuthService.addAuthHeader.and.callFake((headers) => {
headers.Authorization = calls % 2 === 1 ? "valid" : "invalid";
});
spyOn(PouchDB, "fetch").and.callFake(async (url, opts) => {
calls++;
if (opts.headers["Authorization"] === "valid") {
return new Response('{ "_id": "foo" }', { status: HttpStatusCode.Ok });
} else {
return {
status: HttpStatusCode.Unauthorized,
ok: false,
} as Response;
}
});
database.get("Entity:ABC");
tick();
tick();
expect(PouchDB.fetch).toHaveBeenCalledTimes(2);
expect(mockAuthService.login).toHaveBeenCalled();
expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);
}));
});