diff --git a/package-lock.json b/package-lock.json index 925411123..ae4078655 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", "eslint": "^8.31.0", + "fake-indexeddb": "^5.0.1", "file-loader": "^6.2.0", "gh-pages": "^5.0.0", "happy-dom": "^12.10.3", @@ -5335,6 +5336,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fake-indexeddb": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-5.0.1.tgz", + "integrity": "sha512-vxybH29Owtc6khV/Usy47B1g+eKwyhFiX8nwpCC4td320jvwrKQDH6vNtcJZgUzVxmfsSIlHzLKQzT76JMCO7A==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 0462f849f..61e31bc78 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", "eslint": "^8.31.0", + "fake-indexeddb": "^5.0.1", "file-loader": "^6.2.0", "gh-pages": "^5.0.0", "happy-dom": "^12.10.3", diff --git a/scripts/database.js b/scripts/database.js index dbdb7d7be..b6106e583 100644 --- a/scripts/database.js +++ b/scripts/database.js @@ -100,13 +100,13 @@ export class Database { } /** * Removes a Promo Code from the Promo management system - * @param {string} promo - the promo code to remove + * @param {string} promoCode - the promo code to remove */ - async removePromo(promo) { + async removePromo(promoCode) { const store = this.#db .transaction('promos', 'readwrite') .objectStore('promos'); - await store.delete(promo); + await store.delete(promoCode); } /** @@ -127,7 +127,9 @@ export class Database { 'warning', 'Account Creation Error
Logs were dumped in your Browser Console
Please submit these privately to PIVX Labs Developers!' ); - return false; + throw new Error( + 'addAccount was called with with an invalid account' + ); } // Create an empty DB Account @@ -157,7 +159,7 @@ export class Database { // Check this account isn't already added (by pubkey once multi-account) if (await store.get('account')) - return console.error( + throw new Error( 'DB: Ran addAccount() when account already exists!' ); @@ -190,7 +192,9 @@ export class Database { 'warning', 'DB Update Error
Your wallet is safe, logs were dumped in your Browser Console
Please submit these privately to PIVX Labs Developers!' ); - return false; + throw new Error( + 'addAccount was called with with an invalid account' + ); } // Fetch the DB account @@ -208,7 +212,9 @@ export class Database { 'warning', 'DB Update Error
Logs were dumped in your Browser Console
Please submit these privately to PIVX Labs Developers!' ); - return false; + throw new Error( + "updateAccount was called, but the account doesn't exist" + ); } // We'll overlay the `account` keys atop the `DB Account` keys: @@ -483,7 +489,7 @@ export class Database { }); database.#db = db; if (migrate) { - database.#migrateLocalStorage(); + await database.#migrateLocalStorage(); } return database; } diff --git a/tests/unit/database.spec.js b/tests/unit/database.spec.js new file mode 100644 index 000000000..1228a0d1a --- /dev/null +++ b/tests/unit/database.spec.js @@ -0,0 +1,226 @@ +import 'fake-indexeddb/auto'; +import { PromoWallet } from '../../scripts/promos.js'; +import { it, describe, vi, expect } from 'vitest'; +import { Database } from '../../scripts/database.js'; +import { Account } from '../../scripts/accounts'; +import * as misc from '../../scripts/misc.js'; +import { Settings } from '../../scripts/settings'; +import Masternode from '../../scripts/masternode'; +describe('database tests', () => { + beforeAll(() => { + // Mock createAlert + vi.spyOn(misc, 'createAlert').mockImplementation(vi.fn()); + vi.stubGlobal(global.console, 'error'); + return () => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }; + }); + beforeEach(async () => { + // Reset indexedDB before each test + vi.stubGlobal('indexedDB', new IDBFactory()); + return vi.unstubAllGlobals; + }); + it('stores account correctly', async () => { + const db = await Database.create('test'); + const account = new Account({ + publicKey: 'test1', + coldAddress: 'very cold', + }); + await db.addAccount(account); + expect(await db.getAccount()).toStrictEqual(account); + await db.updateAccount( + new Account({ + encWif: 'newWIF!', + localProposals: ['prop1', 'prop2'], + }) + ); + expect((await db.getAccount()).encWif).toBe('newWIF!'); + expect((await db.getAccount()).publicKey).toBe('test1'); + expect((await db.getAccount()).coldAddress).toBe('very cold'); + expect((await db.getAccount()).localProposals).toStrictEqual([ + 'prop1', + 'prop2', + ]); + + // Setting localProposals as empty doesn't overwrite the array + await db.updateAccount( + new Account({ + encWif: 'newWIF2!', + localProposals: [], + }) + ); + expect((await db.getAccount()).localProposals).toStrictEqual([ + 'prop1', + 'prop2', + ]); + + // Unless `allowDeletion` is set to true + await db.updateAccount( + new Account({ + encWif: 'newWIF2!', + localProposals: [], + }), + true + ); + expect((await db.getAccount()).localProposals).toHaveLength(0); + + await db.removeAccount({ publicKey: 'test1' }); + + expect(await db.getAccount()).toBeNull(); + }); + + it.todo('stores transaction correctly', () => { + // To avoid conflicts, I will implement this after #284 + }); + + it('stores masternodes correctly', async () => { + const db = await Database.create('test'); + // Masternode should be null by default + expect(await db.getMasternode()).toBe(null); + let masternode = new Masternode({ + collateralTxId: 'mntxid', + }); + await db.addMasternode(masternode); + expect(await db.getMasternode()).toStrictEqual(masternode); + masternode = new Masternode({ + collateralTxId: 'mntxid2', + }); + // Subsequent calls to `addMasternode` should overwrite it. + await db.addMasternode(masternode); + expect(await db.getMasternode()).toStrictEqual(masternode); + // Check that it removes mn correectly + await db.removeMasternode(); + expect(await db.getMasternode()).toBe(null); + }); + + it('stores promos correctly', async () => { + const testPromos = new Array(50).fill(0).map( + (_, i) => + new PromoWallet({ + code: `${i}`, + }) + ); + const db = await Database.create('test'); + // It starts with no promos + expect(await db.getAllPromos()).toHaveLength(0); + + await db.addPromo(testPromos[0]); + expect(await db.getAllPromos()).toStrictEqual([testPromos[0]]); + + // If we add the same promo twice, it should not duplicate it + await db.addPromo(testPromos[0]); + expect(await db.getAllPromos()).toStrictEqual([testPromos[0]]); + + // Removes correctly + await db.removePromo(testPromos[0].code); + expect(await db.getAllPromos()).toHaveLength(0); + + for (const promo of testPromos) { + await db.addPromo(promo); + } + expect( + (await db.getAllPromos()).sort( + (a, b) => parseInt(a.code) - parseInt(b.code) + ) + ).toStrictEqual(testPromos); + await db.removePromo('23'); + expect( + (await db.getAllPromos()).sort( + (a, b) => parseInt(a.code) - parseInt(b.code) + ) + ).toStrictEqual(testPromos.filter((p) => p.code != '23')); + }); + + it('stores settings correctly', async () => { + const db = await Database.create('test'); + const settings = new Settings({ + explorer: 'duddino.com', + node: 'pivx.com', + }); + // Settings should be left as default at the beginning + expect(await db.getSettings()).toStrictEqual(new Settings()); + await db.setSettings(settings); + expect(await db.getSettings()).toStrictEqual(settings); + // Test that overwrite works as expected + await db.setSettings({ + node: 'pivx.org', + }); + expect(await db.getSettings()).toStrictEqual( + new Settings({ + explorer: 'duddino.com', + node: 'pivx.org', + }) + ); + }); + + it('throws when calling addAccount twice', async () => { + const db = await Database.create('test'); + const account = new Account(); + db.addAccount(account); + expect(() => db.addAccount(account)).rejects.toThrow( + /account already exists/i + ); + }); + it('throws when called with an invalid account', async () => { + const db = await Database.create('test'); + expect(() => db.addAccount({ publicKey: 'jaeir' })).rejects.toThrow( + /invalid account/ + ); + expect(() => db.updateAccount({ publicKey: 'jaeir' })).rejects.toThrow( + /invalid account/ + ); + }); + it("throws when updating an account that doesn't exist", async () => { + const db = await Database.create('test'); + expect(() => db.updateAccount(new Account())).rejects.toThrow( + /account doesn't exist/ + ); + }); + + it('migrates from local storage correctly', async () => { + vi.stubGlobal('localStorage', { + explorer: 'duddino.com', + translation: 'DE', + encwif: 'ENCRYPTED_WIF', + publicKey: 'PUB_KEY', + masternode: JSON.stringify( + new Masternode({ collateralTxId: 'mntxid' }) + ), + }); + const db = await Database.create('test'); + expect(await db.getAccount()).toStrictEqual( + new Account({ + publicKey: 'PUB_KEY', + encWif: 'ENCRYPTED_WIF', + }) + ); + expect(await db.getSettings()).toStrictEqual( + new Settings({ + explorer: 'duddino.com', + translation: 'DE', + }) + ); + expect(await db.getMasternode()).toStrictEqual( + new Masternode({ collateralTxId: 'mntxid' }) + ); + + vi.unstubAllGlobals(); + }); + + it('is isolated between different instances', async () => { + const db = await Database.create('test'); + const db2 = await Database.create('test2'); + // Initially, both accounts are null + expect(await db.getAccount()).toBe(null); + expect(await db2.getAccount()).toBe(null); + const account = new Account({ + publicKey: 'test1', + }); + // Let's add an account to the first db + await db.addAccount(account); + // First DB has the account, the second one is undefined + expect((await db.getAccount())?.publicKey).toBe('test1'); + expect((await db2.getAccount())?.publicKey).toBeUndefined(); + }); +});