From 57bb83497f174d405ba695a3719f9e39d89170ed Mon Sep 17 00:00:00 2001 From: Perry Mitchell Date: Tue, 6 Feb 2024 21:41:11 +0200 Subject: [PATCH 1/3] Add vault source entry search --- source/index.common.ts | 1 + source/search/BaseSearch.ts | 14 +++++++-- source/search/VaultSourceEntrySearch.ts | 39 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 source/search/VaultSourceEntrySearch.ts diff --git a/source/index.common.ts b/source/index.common.ts index ee1c121a..6594a6f8 100644 --- a/source/index.common.ts +++ b/source/index.common.ts @@ -80,6 +80,7 @@ export { SearchResult } from "./search/BaseSearch.js"; export { VaultEntrySearch as Search } from "./search/VaultEntrySearch.js"; // compat @todo remove export { VaultEntrySearch } from "./search/VaultEntrySearch.js"; export { VaultFacadeEntrySearch } from "./search/VaultFacadeEntrySearch.js"; +export { VaultSourceEntrySearch } from "./search/VaultSourceEntrySearch.js"; export { SearchKey, buildSearcher } from "./search/searcher.js"; export { AppEnv, AppEnvGetPropertyOptions } from "./env/core/appEnv.js"; diff --git a/source/search/BaseSearch.ts b/source/search/BaseSearch.ts index bf06f6a6..6079af0d 100644 --- a/source/search/BaseSearch.ts +++ b/source/search/BaseSearch.ts @@ -2,8 +2,8 @@ import levenshtein from "fast-levenshtein"; import { StorageInterface } from "../storage/StorageInterface.js"; import { buildSearcher } from "./searcher.js"; import { Vault } from "../core/Vault.js"; -import { EntryID, EntryType, GroupID, VaultFacade, VaultID } from "../types.js"; import { extractTagsFromSearchTerm, tagsMatchSearch } from "./tags.js"; +import { EntryID, EntryType, GroupID, VaultFacade, VaultID, VaultSourceID } from "../types.js"; interface DomainScores { [domain: string]: number; @@ -28,6 +28,7 @@ export interface SearchResult { id: EntryID; properties: { [property: string]: string }; tags: Array; + sourceID?: VaultSourceID; urls: Array; vaultID: VaultID; } @@ -78,11 +79,20 @@ export class BaseSearch { /** * Last search results + * @deprecated Use `getResults` instead */ get results(): Array { return this._results; } + /** + * Get last search results + * @returns An array of results + */ + getResults(): Array { + return this._results; + } + /** * Increment the score of a URL in an entry * @param vaultID The vault ID @@ -110,7 +120,7 @@ export class BaseSearch { * Prepare the search instance by processing * entries */ - async prepare() { + async prepare(): Promise { this._entries = []; this._scores = {}; for (const target of this._targets) { diff --git a/source/search/VaultSourceEntrySearch.ts b/source/search/VaultSourceEntrySearch.ts new file mode 100644 index 00000000..928b6cdb --- /dev/null +++ b/source/search/VaultSourceEntrySearch.ts @@ -0,0 +1,39 @@ +import { SearchResult, SearcherFactory } from "./BaseSearch.js"; +import { VaultSource } from "../core/VaultSource.js"; +import { Vault } from "../core/Vault.js"; +import { StorageInterface } from "../storage/StorageInterface.js"; +import { VaultEntrySearch } from "./VaultEntrySearch.js"; +import { VaultSourceStatus } from "../types.js"; + +export class VaultSourceEntrySearch extends VaultEntrySearch { + _sources: Array; + + constructor( + sources: Array, + memory?: StorageInterface, + searcherFactory?: SearcherFactory + ) { + const vaults: Array = sources.reduce((output, source) => { + if (source.status === VaultSourceStatus.Unlocked) { + return [...output, source.vault]; + } + return output; + }, []); + super(vaults, memory, searcherFactory); + this._sources = [...sources]; + } + + /** + * Last search results + */ + get results(): Array { + return this._results.map((res) => { + const output = res; + const source = this._sources.find((src) => src?.vault?.id === output.vaultID); + if (source) { + output.sourceID = source.id; + } + return output; + }); + } +} From 62e449e52b9604e85c71180a7ac03e76b200ef92 Mon Sep 17 00:00:00 2001 From: Perry Mitchell Date: Tue, 6 Feb 2024 22:15:28 +0200 Subject: [PATCH 2/3] Attach sourceID to all results for source entry s. --- source/search/VaultSourceEntrySearch.ts | 34 +++++ .../search/VaultEntrySearch.spec.js | 62 +-------- .../search/VaultSourceEntrySearch.spec.js | 119 ++++++++++++++++++ test/integration/search/helpers.js | 85 +++++++++++++ 4 files changed, 241 insertions(+), 59 deletions(-) create mode 100644 test/integration/search/VaultSourceEntrySearch.spec.js create mode 100644 test/integration/search/helpers.js diff --git a/source/search/VaultSourceEntrySearch.ts b/source/search/VaultSourceEntrySearch.ts index 928b6cdb..7c7ce1a8 100644 --- a/source/search/VaultSourceEntrySearch.ts +++ b/source/search/VaultSourceEntrySearch.ts @@ -36,4 +36,38 @@ export class VaultSourceEntrySearch extends VaultEntrySearch { return output; }); } + + /** + * Search for entries by term + * @param term The term to search for + * @returns An array of search results + */ + searchByTerm(term: string): Array { + const results = super.searchByTerm(term); + return results.map((res) => { + const output = res; + const source = this._sources.find((src) => src?.vault?.id === output.vaultID); + if (source) { + output.sourceID = source.id; + } + return output; + }); + } + + /** + * Search for entries by URL + * @param url The URL to search with + * @returns An array of search results + */ + searchByURL(url: string): Array { + const results = super.searchByURL(url); + return results.map((res) => { + const output = res; + const source = this._sources.find((src) => src?.vault?.id === output.vaultID); + if (source) { + output.sourceID = source.id; + } + return output; + }); + } } diff --git a/test/integration/search/VaultEntrySearch.spec.js b/test/integration/search/VaultEntrySearch.spec.js index 92e7414f..4f31daf7 100644 --- a/test/integration/search/VaultEntrySearch.spec.js +++ b/test/integration/search/VaultEntrySearch.spec.js @@ -1,66 +1,10 @@ import { expect } from "chai"; -import { - Entry, - EntryType, - Group, - MemoryStorageInterface, - Vault, - VaultEntrySearch -} from "../../../dist/node/index.js"; +import { EntryType, MemoryStorageInterface, VaultEntrySearch } from "../../../dist/node/index.js"; +import { createSampleVault } from "./helpers.js"; describe("VaultEntrySearch", function () { beforeEach(function () { - const vault = (this.vault = new Vault()); - const groupA = vault.createGroup("Email"); - groupA - .createEntry("Personal Mail") - .setProperty("username", "green.monkey@fastmail.com") - .setProperty("password", "df98Sm2.109x{91") - .setProperty("url", "https://fastmail.com") - .setAttribute(Entry.Attributes.FacadeType, EntryType.Website); - groupA - .createEntry("Work") - .setProperty("username", "j.crowley@gmov.edu.au") - .setProperty("password", "#f05c.*skU3") - .setProperty("URL", "gmov.edu.au/portal/auth") - .addTags("job"); - groupA - .createEntry("Work logs") - .setProperty("username", "j.crowley@gmov.edu.au") - .setProperty("password", "#f05c.*skU3") - .setProperty("URL", "https://logs.gmov.edu.au/sys30/atc.php") - .addTags("job"); - const groupB = vault.createGroup("Bank"); - groupB - .createEntry("MyBank") - .setProperty("username", "324654356346") - .setProperty("PIN", "1234") - .setAttribute(Entry.Attributes.FacadeType, EntryType.Login) - .addTags("finance", "banking"); - groupB - .createEntry("Insurance") - .setProperty("username", "testing-user") - .setProperty("URL", "http://test.org/portal-int/login.aspx") - .addTags("finance"); - const groupC = vault.createGroup("General"); - groupC - .createEntry("Clipart") - .setProperty("username", "gmonkey123") - .setProperty("password", "test93045") - .setProperty("Url", "clipart.com"); - groupC - .createEntry("Wordpress") - .setProperty("username", "gmonkey1234") - .setProperty("password", "passw0rd") - .setProperty("Url", "https://wordpress.com/") - .setProperty("Login URL", "https://wordpress.com/account/login.php"); - const trashGroup = vault.createGroup("Trash"); - trashGroup - .createEntry("Ebay") - .setProperty("username", "gmk123@hotmail.com") - .setProperty("password", "passw0rd") - .setProperty("Url", "https://ebay.com/"); - trashGroup.setAttribute(Group.Attribute.Role, "trash"); + this.vault = createSampleVault(); }); it("can be instantiated", function () { diff --git a/test/integration/search/VaultSourceEntrySearch.spec.js b/test/integration/search/VaultSourceEntrySearch.spec.js new file mode 100644 index 00000000..c6666197 --- /dev/null +++ b/test/integration/search/VaultSourceEntrySearch.spec.js @@ -0,0 +1,119 @@ +import { expect } from "chai"; +import { + EntryType, + MemoryStorageInterface, + VaultSourceEntrySearch +} from "../../../dist/node/index.js"; +import { createSampleManager } from "./helpers.js"; + +describe("VaultSourceEntrySearch", function () { + beforeEach(async function () { + const [, source] = await createSampleManager(); + this.source = source; + this.vault = source.vault; + }); + + it("can be instantiated", function () { + expect(() => { + new VaultSourceEntrySearch([this.source]); + }).to.not.throw(); + }); + + describe("instance", function () { + beforeEach(function () { + this.storage = new MemoryStorageInterface(); + this.search = new VaultSourceEntrySearch([this.source], this.storage); + return this.search.prepare(); + }); + + describe("searchByTerm", function () { + it("finds results by term", function () { + const results = this.search.searchByTerm("work").map((res) => res.properties.title); + expect(results[0]).to.equal("Work"); + expect(results[1]).to.equal("Work logs"); + expect(results[2]).to.equal("Wordpress"); + }); + + it("excludes trash entries", function () { + const results = this.search.searchByTerm("ebay"); + expect(results).to.have.lengthOf(0); + }); + + it("returns resulting entry type", function () { + const [res] = this.search.searchByTerm("Personal Mail"); + expect(res).to.have.property("entryType", EntryType.Website); + }); + + it("returns results including entry group IDs", function () { + const [res] = this.search.searchByTerm("Personal Mail"); + expect(res).to.have.property("groupID").that.is.a("string"); + }); + + it("returns results using a single tag, no search", function () { + const results = this.search.searchByTerm("#job").map((res) => res.properties.title); + expect(results).to.deep.equal(["Work", "Work logs"]); + }); + + it("returns results using multiple tags, no search", function () { + const results = this.search + .searchByTerm("#finance #banking") + .map((res) => res.properties.title); + expect(results).to.deep.equal(["MyBank"]); + }); + + it("returns results using tags and search", function () { + const results = this.search + .searchByTerm("#job logs") + .map((res) => res.properties.title); + expect(results).to.deep.equal(["Work logs"]); + }); + + describe("results", function () { + it("contain source ID", function () { + const [res] = this.search.searchByTerm("Personal Mail"); + expect(res).to.have.property("sourceID", this.source.id); + }); + }); + }); + + describe("searchByURL", function () { + it("finds results by URL", function () { + const results = this.search.searchByURL("https://wordpress.com/homepage/test/org"); + expect(results).to.have.length.above(0); + expect(results[0]).to.have.nested.property("properties.title", "Wordpress"); + }); + + it("excludes trash entries", function () { + const results = this.search.searchByURL("ebay.com"); + expect(results).to.have.lengthOf(0); + }); + + it("finds multiple similar results", function () { + const results = this.search.searchByURL("https://gmov.edu.au/portal/"); + expect(results).to.have.lengthOf(2); + expect(results[0].properties.title).to.equal("Work"); + expect(results[1].properties.title).to.equal("Work logs"); + }); + + it("supports ordering", function () { + const [entry] = this.vault.findEntriesByProperty("title", "Work logs"); + return this.storage + .setValue( + `bcup_search_${this.vault.id}`, + JSON.stringify({ + [entry.id]: { + "gmov.edu.au": 1 + } + }) + ) + .then(() => this.search.prepare()) + .then(() => { + const results = this.search.searchByURL("https://gmov.edu.au/portal/"); + expect(results).to.have.lengthOf(2); + expect(results[0].properties.title).to.equal("Work logs"); + expect(results[1].properties.title).to.equal("Work"); + }); + }); + }); + }); +}); diff --git a/test/integration/search/helpers.js b/test/integration/search/helpers.js new file mode 100644 index 00000000..9159609a --- /dev/null +++ b/test/integration/search/helpers.js @@ -0,0 +1,85 @@ +import { + Credentials, + Entry, + EntryType, + Group, + Vault, + VaultManager, + VaultSource +} from "../../../dist/node/index.js"; + +export async function createSampleManager() { + const manager = new VaultManager({ + autoUpdate: false + }); + const creds = Credentials.fromDatasource( + { + type: "memory", + property: `test:${Math.floor(Math.random() * 1000000)}` + }, + "test" + ); + const credsStr = await creds.toSecureString(); + const source = new VaultSource("Refs test", "memory", credsStr); + await manager.addSource(source); + await source.unlock(Credentials.fromPassword("test"), { + initialiseRemote: true + }); + createSampleVault(source.vault); + await source.save(); + return [manager, source]; +} + +export function createSampleVault(vault = new Vault()) { + const groupA = vault.createGroup("Email"); + groupA + .createEntry("Personal Mail") + .setProperty("username", "green.monkey@fastmail.com") + .setProperty("password", "df98Sm2.109x{91") + .setProperty("url", "https://fastmail.com") + .setAttribute(Entry.Attributes.FacadeType, EntryType.Website); + groupA + .createEntry("Work") + .setProperty("username", "j.crowley@gmov.edu.au") + .setProperty("password", "#f05c.*skU3") + .setProperty("URL", "gmov.edu.au/portal/auth") + .addTags("job"); + groupA + .createEntry("Work logs") + .setProperty("username", "j.crowley@gmov.edu.au") + .setProperty("password", "#f05c.*skU3") + .setProperty("URL", "https://logs.gmov.edu.au/sys30/atc.php") + .addTags("job"); + const groupB = vault.createGroup("Bank"); + groupB + .createEntry("MyBank") + .setProperty("username", "324654356346") + .setProperty("PIN", "1234") + .setAttribute(Entry.Attributes.FacadeType, EntryType.Login) + .addTags("finance", "banking"); + groupB + .createEntry("Insurance") + .setProperty("username", "testing-user") + .setProperty("URL", "http://test.org/portal-int/login.aspx") + .addTags("finance"); + const groupC = vault.createGroup("General"); + groupC + .createEntry("Clipart") + .setProperty("username", "gmonkey123") + .setProperty("password", "test93045") + .setProperty("Url", "clipart.com"); + groupC + .createEntry("Wordpress") + .setProperty("username", "gmonkey1234") + .setProperty("password", "passw0rd") + .setProperty("Url", "https://wordpress.com/") + .setProperty("Login URL", "https://wordpress.com/account/login.php"); + const trashGroup = vault.createGroup("Trash"); + trashGroup + .createEntry("Ebay") + .setProperty("username", "gmk123@hotmail.com") + .setProperty("password", "passw0rd") + .setProperty("Url", "https://ebay.com/"); + trashGroup.setAttribute(Group.Attribute.Role, "trash"); + return vault; +} From 01bbdf5a041d4bc1391549d4dda19d9a4689ddfb Mon Sep 17 00:00:00 2001 From: Perry Mitchell Date: Tue, 6 Feb 2024 22:31:54 +0200 Subject: [PATCH 3/3] Add getResults for VaultSourceEntrySearch --- source/search/VaultSourceEntrySearch.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/source/search/VaultSourceEntrySearch.ts b/source/search/VaultSourceEntrySearch.ts index 7c7ce1a8..9e3096ec 100644 --- a/source/search/VaultSourceEntrySearch.ts +++ b/source/search/VaultSourceEntrySearch.ts @@ -25,9 +25,18 @@ export class VaultSourceEntrySearch extends VaultEntrySearch { /** * Last search results + * @deprecated Use `getResults` instead */ get results(): Array { - return this._results.map((res) => { + return this.getResults(); + } + + /** + * Get last search results + * @returns An array of results + */ + getResults(): Array { + return super.getResults().map((res) => { const output = res; const source = this._sources.find((src) => src?.vault?.id === output.vaultID); if (source) {