I'm trying to understand kdbxweb, the database powering keeweb, compatible to Keepass.
I did setup a simple project using vite and vitest that creates, saves and loads a database.
Works like a charm in the browser (run npm run dev
to see). However I failed to create a unit test using vitest. Creation works, but save fails:
TypeError: Failed to execute 'digest' on 'SubtleCrypto': 2nd argument is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.
The vite.config.js
:
'use strict';
import { defineConfig } from 'vite';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['**/*.test.js'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html']
}
}
})
The method in question (full source code):
import * as kdbxweb from 'kdbxweb';
const VAULT_DB_NAME = 'Sample Vault';
const VAULT_DB_GROUP_NAME = 'Sample Group';
const subtle = window.crypto.subtle;
let vault = null;
const createDatabase = (passPhrase) => new Promise((resolve, reject) => {
try {
const protectedValue = kdbxweb.ProtectedValue.fromString(passPhrase);
const credentials = new kdbxweb.Credentials(protectedValue);
const db = kdbxweb.Kdbx.create(credentials, VAULT_DB_NAME);
db.createGroup(db.getDefaultGroup(), VAULT_DB_GROUP_NAME);
vault = db;
resolve(db);
} catch (error) {
reject(error);
}
});
const saveDatabase = () => new Promise((resolve, reject) => {
if (!vault) {
return reject('Vault not present');
}
vault.save().then(arrayBuffer => {
resolve(arrayBuffer);
}).catch(reject);
});
The test method looks like this:
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { init, createDatabase, saveDatabase, loadDatabase, clearDatabase } from '../src/vault';
import * as kdbxweb from 'kdbxweb';
describe('Vault Kxdb Tests', () => {
beforeEach(() => {
init();
});
afterEach(() => {
clearDatabase();
});
it('should create a new database', async () => {
const db = await createDatabase('test');
expect(db).toBeDefined();
});
it('should save the database', async () => {
const db = await createDatabase('test');
const arrayBuffer = await saveDatabase(db);
expect(arrayBuffer).toBeDefined();
});
it('should load the database', async () => {
const db = await createDatabase('test');
const arrayBuffer = await saveDatabase(db);
const loadedDb = await loadDatabase(arrayBuffer, 'test');
expect(loadedDb).toBeDefined();
});
});
Test of saveDatabase fails:
TypeError: Failed to execute 'digest' on 'SubtleCrypto': 2nd argument is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.
full project here: https://github.com/Stwissel/kxdbtest
What do I miss?
Turns out there are subtle differences (pun intended) how the crypto APIs are implemented in Browser and NodeJS Crypto.
So the solution, that worked for me, was to switch to browser based testing using playwright. The vite.config.js
now looks like this:
'use strict';
import { defineConfig } from 'vite';
export default defineConfig({
test: {
includeTaskLocation: true,
include: ['**/*.test.js'],
name: 'browser',
browser: {
provider: 'playwright',
headless: true,
enabled: true,
name: 'chromium',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html']
}
}
}
});
and the package.json
like this:
{
"name": "kdbx-issue",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest --coverage"
},
"devDependencies": {
"@vitest/browser": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8",
"express": "^4.21.2",
"playwright": "^1.49.1",
"vite": "^6.0.7",
"vitest": "^2.1.8"
},
"dependencies": {
"@noble/hashes": "^1.7.0",
"kdbxweb": "^2.1.1"
}
}
To note:
headless: false
to get a unit test webUIvitest.workspace.js
file distinguishing how which test files should be processed. The vite.config.js
would contain only the common settings. You can test the same tests against multiple browsers.'use strict';
import { defineConfig } from 'vite';
export default defineConfig({
test: {
includeTaskLocation: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html']
}
}
});
import { defineWorkspace } from 'vitest/config'
export default defineWorkspace([
{
test: {
globals: true,
include: ['**/*.test.js'],
name: 'unit',
environment: 'jsdom'
},
},
{
test: {
include: ['**/*.browsertest.js'],
name: 'browser1',
browser: {
provider: 'playwright',
headless: false,
enabled: true,
name: 'chromium',
providerOptions: {
launch: {
devtools: true,
}
}
}
},
},
{
test: {
include: ['**/*.browsertest.js'],
name: 'browser2',
browser: {
provider: 'playwright',
headless: false,
enabled: true,
name: 'safari',
providerOptions: {
launch: {
devtools: true,
}
}
}
},
},
])
full project here: https://github.com/Stwissel/kxdbtest