mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 08:17:57 -04:00
Update e2e tests (#1404)
This commit is contained in:
1
apps/browser-extension/.gitignore
vendored
1
apps/browser-extension/.gitignore
vendored
@@ -28,4 +28,5 @@ web-ext.config.ts
|
||||
# Test related output files
|
||||
playwright-report
|
||||
tests/screenshots
|
||||
tests/test-results
|
||||
test-results
|
||||
|
||||
459
apps/browser-extension/package-lock.json
generated
459
apps/browser-extension/package-lock.json
generated
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.10",
|
||||
@@ -41,7 +42,9 @@
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@wxt-dev/module-react": "^1.1.2",
|
||||
"argon2": "^0.44.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
@@ -739,6 +742,13 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@epic-web/invariant": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@es-joy/jsdoccomment": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz",
|
||||
@@ -1600,6 +1610,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -2044,6 +2064,16 @@
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.0.280",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.280.tgz",
|
||||
@@ -3051,6 +3081,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argon2": {
|
||||
"version": "0.44.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz",
|
||||
"integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@phc/format": "^1.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"node-addon-api": "^8.5.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argon2-browser": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
|
||||
@@ -3392,6 +3439,21 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
|
||||
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -3405,6 +3467,68 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
@@ -3889,6 +4013,13 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/chrome-launcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz",
|
||||
@@ -4261,6 +4392,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@epic-web/invariant": "^1.0.0",
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "dist/bin/cross-env.js",
|
||||
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -4470,6 +4619,22 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
@@ -4599,6 +4764,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -4817,6 +4992,16 @@
|
||||
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
|
||||
@@ -5657,6 +5842,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
|
||||
@@ -5757,6 +5952,13 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "11.0.13",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.13.tgz",
|
||||
@@ -5905,6 +6107,13 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
|
||||
@@ -6154,6 +6363,13 @@
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@@ -8068,6 +8284,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
@@ -8104,6 +8333,13 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||
@@ -8237,6 +8473,13 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz",
|
||||
@@ -8260,6 +8503,29 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.85.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
|
||||
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||
@@ -8277,6 +8543,18 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-notifier": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz",
|
||||
@@ -8557,6 +8835,16 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
|
||||
@@ -9252,6 +9540,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -9368,6 +9683,17 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -10141,6 +10467,27 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-push-apply": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||
@@ -10488,6 +10835,53 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -11061,6 +11455,51 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
|
||||
@@ -11337,6 +11776,19 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -12396,6 +12848,13 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.10",
|
||||
@@ -60,7 +61,9 @@
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@wxt-dev/module-react": "^1.1.2",
|
||||
"argon2": "^0.44.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
|
||||
@@ -1,23 +1,57 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Playwright configuration for browser extension E2E tests.
|
||||
*
|
||||
* These tests load the real extension and interact with it using Playwright.
|
||||
* The extension must be built first using `npm run build:chrome`.
|
||||
*
|
||||
* Test Organization:
|
||||
* - Tests are numbered (1.x, 2.x, etc.) and run in alphabetical order
|
||||
* - fullyParallel is disabled to ensure tests run sequentially
|
||||
* - Each test gets a fresh browser context via fixtures
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: true,
|
||||
testDir: './tests/e2e',
|
||||
|
||||
// Global teardown to clean up browser contexts
|
||||
globalTeardown: './tests/global-teardown.ts',
|
||||
|
||||
// Run tests sequentially to ensure predictable order
|
||||
fullyParallel: false,
|
||||
|
||||
// Fail the build on CI if you accidentally left test.only in the source code
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
timeout: 30000,
|
||||
|
||||
// Retry failed tests (more retries on CI)
|
||||
retries: process.env.CI ? 2 : 1,
|
||||
|
||||
// Use single worker to ensure sequential execution
|
||||
workers: 1,
|
||||
|
||||
// Reporter configuration
|
||||
reporter: [
|
||||
['html', { open: 'never' }],
|
||||
['list'], // Show test names in console
|
||||
],
|
||||
|
||||
// Global timeout for each test
|
||||
timeout: 60000,
|
||||
|
||||
// Expect timeout
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
},
|
||||
|
||||
use: {
|
||||
// Collect trace on first retry
|
||||
trace: 'on-first-retry',
|
||||
|
||||
// Take screenshot on failure
|
||||
screenshot: 'only-on-failure',
|
||||
|
||||
// Record video on first retry
|
||||
video: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
@@ -32,4 +66,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Output folder for test artifacts
|
||||
outputDir: 'tests/test-results',
|
||||
});
|
||||
|
||||
@@ -2,12 +2,13 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
|
||||
import FieldHistoryModal from './FieldHistoryModal';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { ItemField } from '@/utils/dist/shared/models/vault';
|
||||
import { getSystemField } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import FieldHistoryModal from './FieldHistoryModal';
|
||||
|
||||
interface FieldBlockProps {
|
||||
field: ItemField;
|
||||
itemId?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SrpAuthService } from '@/utils/auth/SrpAuthService';
|
||||
import type { LoginResponse, ValidateLoginResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, BadRequestResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
import { SrpAuthService } from '@/utils/auth/SrpAuthService';
|
||||
|
||||
/**
|
||||
* Utility class for SRP authentication operations.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { test, expect, openPopup } from '../fixtures';
|
||||
|
||||
/**
|
||||
* Category 1: Extension Loading (No API required)
|
||||
*
|
||||
* These tests verify the extension loads and renders correctly.
|
||||
* They don't require an API server to be running.
|
||||
* Tests run sequentially and share the same popup page.
|
||||
*/
|
||||
test.describe.serial('1. Extension Loading', () => {
|
||||
let popup: Page;
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Close popup to start fresh for next test group
|
||||
await popup?.close();
|
||||
});
|
||||
|
||||
test('1.1 should load the popup and show login form', async ({ context, extensionId }) => {
|
||||
// Open popup (first test opens it, subsequent tests reuse it)
|
||||
popup = await openPopup(context, extensionId);
|
||||
|
||||
// Check that React rendered (the app container should exist)
|
||||
const appContent = popup.locator('#root');
|
||||
await expect(appContent).toBeVisible();
|
||||
|
||||
// Should show login form elements
|
||||
await expect(popup.locator('input[type="text"]')).toBeVisible();
|
||||
await expect(popup.locator('input[type="password"]')).toBeVisible();
|
||||
|
||||
// Take a screenshot for debugging
|
||||
await popup.screenshot({ path: 'tests/screenshots/1.1-popup-loaded.png' });
|
||||
});
|
||||
|
||||
test('1.2 should have a running service worker', async ({ context, extensionId }) => {
|
||||
// Service worker should be running
|
||||
const serviceWorkers = context.serviceWorkers();
|
||||
expect(serviceWorkers.length).toBeGreaterThan(0);
|
||||
|
||||
// The service worker URL should match our extension
|
||||
const swUrl = serviceWorkers[0].url();
|
||||
expect(swUrl).toContain(extensionId);
|
||||
expect(swUrl).toContain('background');
|
||||
});
|
||||
});
|
||||
57
apps/browser-extension/tests/e2e/02-ui-navigation.spec.ts
Normal file
57
apps/browser-extension/tests/e2e/02-ui-navigation.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { test, expect, openPopup, configureApiUrl } from '../fixtures';
|
||||
|
||||
/**
|
||||
* Category 2: UI Navigation (No API required)
|
||||
*
|
||||
* These tests verify UI navigation works without needing authentication.
|
||||
* They don't require an API server to be running.
|
||||
* Tests run sequentially and share the same popup page.
|
||||
*/
|
||||
test.describe.serial('2. UI Navigation', () => {
|
||||
let popup: Page;
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Close popup to start fresh for next test group
|
||||
await popup?.close();
|
||||
});
|
||||
|
||||
test('2.1 should have working settings button', async ({ context, extensionId }) => {
|
||||
// Open popup (first test opens it)
|
||||
popup = await openPopup(context, extensionId);
|
||||
|
||||
// Click settings button
|
||||
const settingsButton = popup.locator('button#settings');
|
||||
await expect(settingsButton).toBeVisible();
|
||||
await settingsButton.click();
|
||||
|
||||
// Should navigate to settings page
|
||||
// Check for settings-related elements (like the server selection dropdown)
|
||||
await expect(popup.locator('select')).toBeVisible();
|
||||
|
||||
// Take a screenshot
|
||||
await popup.screenshot({ path: 'tests/screenshots/2.1-settings-navigation.png' });
|
||||
|
||||
// Should have a back button
|
||||
const backButton = popup.locator('button#back');
|
||||
await expect(backButton).toBeVisible();
|
||||
|
||||
// Click back and verify we return to login
|
||||
await backButton.click();
|
||||
await expect(popup.locator('input[type="text"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('2.2 should allow configuring custom API URL', async ({ apiUrl }) => {
|
||||
// Reuse popup from previous test
|
||||
// Configure the API URL
|
||||
await configureApiUrl(popup, apiUrl);
|
||||
|
||||
// After going back, we should still be on the login page
|
||||
await expect(popup.locator('input[type="text"]')).toBeVisible();
|
||||
await expect(popup.locator('input[type="password"]')).toBeVisible();
|
||||
|
||||
// Take a screenshot
|
||||
await popup.screenshot({ path: 'tests/screenshots/2.2-api-configured.png' });
|
||||
});
|
||||
});
|
||||
53
apps/browser-extension/tests/e2e/03-authentication.spec.ts
Normal file
53
apps/browser-extension/tests/e2e/03-authentication.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { test, expect, openPopup, configureApiUrl, login, waitForLoggedIn } from '../fixtures';
|
||||
|
||||
/**
|
||||
* Category 3: Authentication Flow (Requires API)
|
||||
*
|
||||
* These tests verify login/authentication works correctly.
|
||||
* They require an API server to be running at localhost:5092.
|
||||
* Tests run sequentially and share the same popup page.
|
||||
*/
|
||||
test.describe.serial('3. Authentication Flow', () => {
|
||||
let popup: Page;
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Close popup to start fresh for next test group
|
||||
await popup?.close();
|
||||
});
|
||||
|
||||
test('3.1 should display error for invalid credentials', async ({ context, extensionId, apiUrl }) => {
|
||||
// Open popup (first test opens it)
|
||||
popup = await openPopup(context, extensionId);
|
||||
|
||||
// Configure API URL
|
||||
await configureApiUrl(popup, apiUrl);
|
||||
|
||||
// Try to login with invalid credentials
|
||||
await popup.fill('input[type="text"]', 'nonexistent@test.com');
|
||||
await popup.fill('input[type="password"]', 'wrongpassword');
|
||||
await popup.click('button:has-text("Log in")');
|
||||
|
||||
// Wait for error message to appear
|
||||
await expect(popup.locator('text=Invalid username or password')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Take a screenshot
|
||||
await popup.screenshot({ path: 'tests/screenshots/3.1-login-failed.png' });
|
||||
|
||||
// Clear the form for the next test
|
||||
await popup.fill('input[type="text"]', '');
|
||||
await popup.fill('input[type="password"]', '');
|
||||
});
|
||||
|
||||
test('3.2 should successfully login with valid credentials', async ({ testUser }) => {
|
||||
// Login with valid credentials
|
||||
await login(popup, testUser.username, testUser.password);
|
||||
|
||||
// Verify we're logged in
|
||||
await waitForLoggedIn(popup);
|
||||
|
||||
// Take a screenshot
|
||||
await popup.screenshot({ path: 'tests/screenshots/3.2-login-success.png' });
|
||||
});
|
||||
});
|
||||
38
apps/browser-extension/tests/e2e/04-post-login.spec.ts
Normal file
38
apps/browser-extension/tests/e2e/04-post-login.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { test, expect, openPopup, fullLoginFlow, waitForLoggedIn } from '../fixtures';
|
||||
|
||||
/**
|
||||
* Category 4: Post-Login Features (Requires API + Authentication)
|
||||
*
|
||||
* These tests verify functionality after successful login.
|
||||
* They require an API server to be running at localhost:5092.
|
||||
* Tests run sequentially and share the same popup page (already logged in).
|
||||
*/
|
||||
test.describe.serial('4. Post-Login Features', () => {
|
||||
let popup: Page;
|
||||
|
||||
test('4.1 should show vault content after login', async ({ context, extensionId, testUser, apiUrl }) => {
|
||||
// Open popup and login
|
||||
popup = await openPopup(context, extensionId);
|
||||
await fullLoginFlow(popup, apiUrl, testUser.username, testUser.password);
|
||||
|
||||
// Wait for the vault to load
|
||||
await waitForLoggedIn(popup);
|
||||
|
||||
// For a new user, the vault should be empty but rendered
|
||||
const rootContent = await popup.locator('#root').textContent();
|
||||
expect(rootContent).toBeTruthy();
|
||||
expect(rootContent!.length).toBeGreaterThan(0);
|
||||
|
||||
// Take a screenshot
|
||||
await popup.screenshot({ path: 'tests/screenshots/4.1-vault-content.png' });
|
||||
});
|
||||
|
||||
test('4.2 should be able to navigate tabs after login', async () => {
|
||||
await waitForLoggedIn(popup);
|
||||
|
||||
// Take a screenshot
|
||||
await popup.screenshot({ path: 'tests/screenshots/4.2-post-login-navigation.png' });
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { test, expect, openPopup } from './fixtures';
|
||||
|
||||
/**
|
||||
* Basic browser extension E2E tests.
|
||||
*
|
||||
* These tests verify the extension loads correctly and basic UI interactions work.
|
||||
* Run with: npm run test:e2e
|
||||
*
|
||||
* Prerequisites:
|
||||
* 1. Build the extension: npm run build:chrome
|
||||
* 2. Have an AliasVault API server running (or mock one)
|
||||
*/
|
||||
|
||||
test.describe('Extension Popup', () => {
|
||||
test('should load the popup and show login form', async ({ context, extensionId }) => {
|
||||
const popup = await openPopup(context, extensionId);
|
||||
|
||||
// Wait for the popup to load
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
|
||||
// The popup should show something - either login form or vault content
|
||||
// Check for common elements that indicate the popup loaded successfully
|
||||
const body = popup.locator('body');
|
||||
await expect(body).toBeVisible();
|
||||
|
||||
// Check that React rendered (the app container should exist)
|
||||
const appContent = popup.locator('#root');
|
||||
await expect(appContent).toBeVisible();
|
||||
|
||||
// Take a screenshot for debugging
|
||||
await popup.screenshot({ path: 'tests/screenshots/popup-loaded.png' });
|
||||
});
|
||||
|
||||
test('should have working navigation elements', async ({ context, extensionId }) => {
|
||||
const popup = await openPopup(context, extensionId);
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Wait for React to render
|
||||
await popup.waitForSelector('#root', { state: 'visible' });
|
||||
|
||||
// Give React a moment to render content
|
||||
await popup.waitForTimeout(500);
|
||||
|
||||
// The popup should contain some text (not be empty)
|
||||
const rootContent = await popup.locator('#root').textContent();
|
||||
expect(rootContent).toBeTruthy();
|
||||
expect(rootContent!.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Extension Service Worker', () => {
|
||||
test('should have a running service worker', async ({ context, extensionId }) => {
|
||||
// Service worker should be running
|
||||
const serviceWorkers = context.serviceWorkers();
|
||||
expect(serviceWorkers.length).toBeGreaterThan(0);
|
||||
|
||||
// The service worker URL should match our extension
|
||||
const swUrl = serviceWorkers[0].url();
|
||||
expect(swUrl).toContain(extensionId);
|
||||
expect(swUrl).toContain('background');
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import { test as base, chromium, type BrowserContext } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Path to the built Chrome extension.
|
||||
* Run `npm run build:chrome` before running E2E tests.
|
||||
*/
|
||||
const EXTENSION_PATH = path.join(__dirname, '..', 'dist', 'chrome-mv3');
|
||||
|
||||
/**
|
||||
* Extended test fixture that provides a browser context with the extension loaded.
|
||||
*/
|
||||
export const test = base.extend<{
|
||||
context: BrowserContext;
|
||||
extensionId: string;
|
||||
}>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
context: async ({}, use) => {
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
headless: false, // Extensions require headed mode
|
||||
args: [
|
||||
`--disable-extensions-except=${EXTENSION_PATH}`,
|
||||
`--load-extension=${EXTENSION_PATH}`,
|
||||
'--no-first-run',
|
||||
'--disable-gpu',
|
||||
],
|
||||
});
|
||||
|
||||
await use(context);
|
||||
await context.close();
|
||||
},
|
||||
|
||||
extensionId: async ({ context }, use) => {
|
||||
// Wait for service worker to be ready
|
||||
let [background] = context.serviceWorkers();
|
||||
if (!background) {
|
||||
background = await context.waitForEvent('serviceworker');
|
||||
}
|
||||
|
||||
// Extract extension ID from the service worker URL
|
||||
const extensionId = background.url().split('/')[2];
|
||||
await use(extensionId);
|
||||
},
|
||||
});
|
||||
|
||||
export const expect = test.expect;
|
||||
|
||||
/**
|
||||
* Helper to open the extension popup.
|
||||
*/
|
||||
export async function openPopup(context: BrowserContext, extensionId: string) {
|
||||
const popupPage = await context.newPage();
|
||||
await popupPage.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
return popupPage;
|
||||
}
|
||||
259
apps/browser-extension/tests/fixtures/fixtures.ts
vendored
Normal file
259
apps/browser-extension/tests/fixtures/fixtures.ts
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
import { test as base, chromium, type BrowserContext, type Page } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createTestUser, isApiAvailable, type TestUser } from '../helpers/test-api';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Path to the built Chrome extension.
|
||||
* Run `npm run build:chrome` before running E2E tests.
|
||||
*/
|
||||
const EXTENSION_PATH = path.join(__dirname, '..', '..', 'dist', 'chrome-mv3');
|
||||
|
||||
/**
|
||||
* Default API URL for local development.
|
||||
*/
|
||||
const DEFAULT_API_URL = process.env.ALIASVAULT_API_URL || 'http://localhost:5092';
|
||||
|
||||
/**
|
||||
* Test-scoped fixtures (created per test file/describe block).
|
||||
*/
|
||||
type TestFixtures = {
|
||||
context: BrowserContext;
|
||||
extensionId: string;
|
||||
testUser: TestUser;
|
||||
apiUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache for browser context within a test file.
|
||||
* This allows tests within the same describe.serial block to share context,
|
||||
* while ensuring each test FILE gets a fresh context (logged out state).
|
||||
*/
|
||||
let cachedContext: BrowserContext | null = null;
|
||||
let cachedExtensionId: string | null = null;
|
||||
let contextTestFile: string | null = null;
|
||||
|
||||
/**
|
||||
* Extended test fixture that provides a browser context with the extension loaded,
|
||||
* a test user, and helper functions.
|
||||
*
|
||||
* Each test file gets a fresh browser context, ensuring tests start in a logged-out state.
|
||||
* Tests within the same file can share the context via describe.serial blocks.
|
||||
*/
|
||||
export const test = base.extend<TestFixtures>({
|
||||
apiUrl: [DEFAULT_API_URL, { option: true }],
|
||||
|
||||
// Context that's fresh per test file but shared within a file
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
context: async ({}, use, testInfo) => {
|
||||
const currentTestFile = testInfo.file;
|
||||
|
||||
// If we have a cached context from a different file, close it
|
||||
if (cachedContext && contextTestFile !== currentTestFile) {
|
||||
await cachedContext.close();
|
||||
cachedContext = null;
|
||||
cachedExtensionId = null;
|
||||
contextTestFile = null;
|
||||
}
|
||||
|
||||
// Create new context if we don't have one for this file
|
||||
if (!cachedContext) {
|
||||
cachedContext = await chromium.launchPersistentContext('', {
|
||||
headless: false, // Extensions require headed mode
|
||||
args: [
|
||||
`--disable-extensions-except=${EXTENSION_PATH}`,
|
||||
`--load-extension=${EXTENSION_PATH}`,
|
||||
'--no-first-run',
|
||||
'--disable-gpu',
|
||||
],
|
||||
});
|
||||
contextTestFile = currentTestFile;
|
||||
|
||||
// Wait for service worker and get extension ID
|
||||
let [background] = cachedContext.serviceWorkers();
|
||||
if (!background) {
|
||||
background = await cachedContext.waitForEvent('serviceworker');
|
||||
}
|
||||
cachedExtensionId = background.url().split('/')[2];
|
||||
}
|
||||
|
||||
await use(cachedContext);
|
||||
|
||||
// Don't close here - let it be reused within the same file
|
||||
// It will be closed when the next file starts or when tests end
|
||||
},
|
||||
|
||||
extensionId: async ({ context }, use) => {
|
||||
// Extension ID is cached along with context
|
||||
if (!cachedExtensionId) {
|
||||
let [background] = context.serviceWorkers();
|
||||
if (!background) {
|
||||
background = await context.waitForEvent('serviceworker');
|
||||
}
|
||||
cachedExtensionId = background.url().split('/')[2];
|
||||
}
|
||||
await use(cachedExtensionId);
|
||||
},
|
||||
|
||||
testUser: async ({ apiUrl }, use) => {
|
||||
// Check if the API is available
|
||||
const apiAvailable = await isApiAvailable(apiUrl);
|
||||
if (!apiAvailable) {
|
||||
console.warn(`API not available at ${apiUrl}. Tests requiring authentication will fail.`);
|
||||
// Return a placeholder user - tests should handle this gracefully
|
||||
await use({
|
||||
username: 'api_unavailable',
|
||||
password: 'api_unavailable',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a test user for this test run
|
||||
const testUser = await createTestUser(apiUrl);
|
||||
await use(testUser);
|
||||
},
|
||||
});
|
||||
|
||||
export const expect = test.expect;
|
||||
|
||||
/**
|
||||
* Close the cached browser context.
|
||||
* Call this in afterAll hooks or global teardown to ensure clean shutdown.
|
||||
*/
|
||||
export async function closeCachedContext(): Promise<void> {
|
||||
if (cachedContext) {
|
||||
await cachedContext.close();
|
||||
cachedContext = null;
|
||||
cachedExtensionId = null;
|
||||
contextTestFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wait for the popup to finish initial loading.
|
||||
*
|
||||
* The popup shows a loading spinner overlay (with z-50) during initial load.
|
||||
* This function waits for actual content to be visible, indicating loading is complete.
|
||||
*
|
||||
* @param popup - The popup page
|
||||
* @param timeout - Maximum time to wait in milliseconds (default: 10000)
|
||||
*/
|
||||
export async function waitForPopupReady(popup: Page, timeout: number = 10000): Promise<void> {
|
||||
// Wait for the loading overlay to disappear by waiting for actual content to be interactable.
|
||||
// The login form has input fields that only become visible after loading completes.
|
||||
// We wait for either the login form inputs OR the settings button (visible on login page).
|
||||
// Using CSS-only selectors to avoid mixing with Playwright-specific selectors.
|
||||
await popup.waitForSelector(
|
||||
'input[type="text"], input[type="password"], button#settings',
|
||||
{ state: 'visible', timeout }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to open the extension popup and wait for it to be ready.
|
||||
*
|
||||
* @param context - The browser context
|
||||
* @param extensionId - The extension ID
|
||||
* @param waitForReady - Whether to wait for the popup to finish loading (default: true)
|
||||
*/
|
||||
export async function openPopup(
|
||||
context: BrowserContext,
|
||||
extensionId: string,
|
||||
waitForReady: boolean = true
|
||||
): Promise<Page> {
|
||||
const popupPage = await context.newPage();
|
||||
await popupPage.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
|
||||
if (waitForReady) {
|
||||
await waitForPopupReady(popupPage);
|
||||
}
|
||||
|
||||
return popupPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wait for post-login state (vault is visible).
|
||||
*
|
||||
* This confirms the user has successfully logged in and the vault UI is ready.
|
||||
*
|
||||
* @param popup - The popup page
|
||||
* @param timeout - Maximum time to wait in milliseconds (default: 30000)
|
||||
*/
|
||||
export async function waitForLoggedIn(popup: Page, timeout: number = 30000): Promise<void> {
|
||||
await popup.getByRole('button', { name: 'Vault' }).waitFor({ state: 'visible', timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to configure the extension to use a custom API URL.
|
||||
*
|
||||
* @param popup - The popup page
|
||||
* @param apiUrl - The API URL to configure
|
||||
*/
|
||||
export async function configureApiUrl(popup: Page, apiUrl: string): Promise<void> {
|
||||
// Click settings button
|
||||
const settingsButton = await popup.waitForSelector('button#settings');
|
||||
await settingsButton.click();
|
||||
|
||||
// Select "Self-hosted" (custom) option
|
||||
await popup.selectOption('select', ['custom']);
|
||||
|
||||
// Fill in the custom URL input
|
||||
await popup.fill('input#custom-api-url', apiUrl);
|
||||
|
||||
// Go back to main page
|
||||
await popup.click('button#back');
|
||||
|
||||
// Wait a moment for the settings to be saved
|
||||
await popup.waitForTimeout(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to login to the extension.
|
||||
*
|
||||
* @param popup - The popup page
|
||||
* @param username - The username to login with
|
||||
* @param password - The password to login with
|
||||
* @param waitForSuccess - Whether to wait for the login to complete successfully
|
||||
*/
|
||||
export async function login(
|
||||
popup: Page,
|
||||
username: string,
|
||||
password: string,
|
||||
waitForSuccess: boolean = true
|
||||
): Promise<void> {
|
||||
// Fill in credentials
|
||||
await popup.fill('input[type="text"]', username);
|
||||
await popup.fill('input[type="password"]', password);
|
||||
|
||||
// Click login button
|
||||
await popup.click('button:has-text("Log in")');
|
||||
|
||||
// Wait for login to complete if requested
|
||||
if (waitForSuccess) {
|
||||
await waitForLoggedIn(popup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to perform full login flow: configure API URL, then login.
|
||||
*
|
||||
* @param popup - The popup page
|
||||
* @param apiUrl - The API URL to use
|
||||
* @param username - The username to login with
|
||||
* @param password - The password to login with
|
||||
* @param waitForSuccess - Whether to wait for the login to complete successfully
|
||||
*/
|
||||
export async function fullLoginFlow(
|
||||
popup: Page,
|
||||
apiUrl: string,
|
||||
username: string,
|
||||
password: string,
|
||||
waitForSuccess: boolean = true
|
||||
): Promise<void> {
|
||||
await configureApiUrl(popup, apiUrl);
|
||||
await login(popup, username, password, waitForSuccess);
|
||||
}
|
||||
17
apps/browser-extension/tests/fixtures/index.ts
vendored
Normal file
17
apps/browser-extension/tests/fixtures/index.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Test fixtures index.
|
||||
*
|
||||
* This module exports all Playwright test fixtures and helpers.
|
||||
*/
|
||||
|
||||
export {
|
||||
test,
|
||||
expect,
|
||||
openPopup,
|
||||
waitForPopupReady,
|
||||
waitForLoggedIn,
|
||||
configureApiUrl,
|
||||
login,
|
||||
fullLoginFlow,
|
||||
closeCachedContext,
|
||||
} from './fixtures';
|
||||
11
apps/browser-extension/tests/global-teardown.ts
Normal file
11
apps/browser-extension/tests/global-teardown.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Global teardown for E2E tests.
|
||||
*
|
||||
* Ensures all browser contexts are properly closed after test runs.
|
||||
*/
|
||||
|
||||
import { closeCachedContext } from './fixtures';
|
||||
|
||||
export default async function globalTeardown(): Promise<void> {
|
||||
await closeCachedContext();
|
||||
}
|
||||
432
apps/browser-extension/tests/helpers/test-api.ts
Normal file
432
apps/browser-extension/tests/helpers/test-api.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* Test API utilities for E2E tests.
|
||||
*
|
||||
* This module provides utilities for interacting with the AliasVault API
|
||||
* during E2E tests, including user registration using SRP protocol.
|
||||
*
|
||||
* Note: This module uses Node.js native argon2 for password hashing,
|
||||
* while the browser extension uses argon2-browser. The SRP protocol
|
||||
* logic is shared where possible.
|
||||
*/
|
||||
|
||||
// Import argon2 for Node.js environment (different from browser version)
|
||||
import { webcrypto } from 'crypto';
|
||||
import { readFileSync, unlinkSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
import argon2 from 'argon2';
|
||||
import Database from 'better-sqlite3';
|
||||
import * as srp from 'secure-remote-password/client.js';
|
||||
|
||||
// Get the vault schema SQL from the shared vault-sql package
|
||||
import { COMPLETE_SCHEMA_SQL } from '../../src/utils/dist/shared/vault-sql/index.mjs';
|
||||
|
||||
/**
|
||||
* Token model returned from successful registration/login.
|
||||
*/
|
||||
export type TokenModel = {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test user credentials.
|
||||
*/
|
||||
export type TestUser = {
|
||||
username: string;
|
||||
password: string;
|
||||
token?: TokenModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vault upload request payload.
|
||||
*/
|
||||
type VaultUploadRequest = {
|
||||
username: string;
|
||||
blob: string;
|
||||
version: string;
|
||||
currentRevisionNumber: number;
|
||||
encryptionPublicKey: string;
|
||||
credentialsCount: number;
|
||||
emailAddressList: string[];
|
||||
privateEmailDomainList: string[];
|
||||
hiddenPrivateEmailDomainList: string[];
|
||||
publicEmailDomainList: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registration request payload.
|
||||
* This matches the server's RegisterRequest model.
|
||||
*/
|
||||
type RegisterRequest = {
|
||||
username: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
encryptionType: string;
|
||||
encryptionSettings: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default encryption settings for Argon2Id.
|
||||
* These match the server defaults in AliasVault.Cryptography.Client/Defaults.cs
|
||||
*/
|
||||
const DEFAULT_ENCRYPTION = {
|
||||
type: 'Argon2Id',
|
||||
settings: JSON.stringify({
|
||||
DegreeOfParallelism: 1,
|
||||
MemorySize: 19456,
|
||||
Iterations: 2,
|
||||
}),
|
||||
// Parsed settings for argon2 usage
|
||||
iterations: 2,
|
||||
memorySize: 19456,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Current vault version - should match the latest version in vault-sql.
|
||||
*/
|
||||
const CURRENT_VAULT_VERSION = '1.7.2';
|
||||
|
||||
/**
|
||||
* Normalizes a username by converting to lowercase and trimming whitespace.
|
||||
* This matches the server's username normalization.
|
||||
*/
|
||||
export function normalizeUsername(username: string): string {
|
||||
return username.toLowerCase().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random test username.
|
||||
*/
|
||||
export function generateTestUsername(): string {
|
||||
const randomPart = Math.random().toString(36).substring(2, 12);
|
||||
return `test_${randomPart}@test.com`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random test password.
|
||||
*/
|
||||
export function generateTestPassword(): string {
|
||||
return `TestPass_${Math.random().toString(36).substring(2, 15)}!`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Uint8Array to an uppercase hex string.
|
||||
*/
|
||||
function bytesToHexString(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a key from password using Argon2Id (Node.js version).
|
||||
*
|
||||
* Note: This uses the native argon2 module for Node.js, which is different
|
||||
* from the argon2-browser WASM module used in the browser extension.
|
||||
*
|
||||
* @param password - The password to derive the key from
|
||||
* @param salt - The salt string
|
||||
* @returns The derived key as Uint8Array
|
||||
*/
|
||||
async function deriveKeyFromPassword(password: string, salt: string): Promise<Uint8Array> {
|
||||
// Note: argon2 in Node.js expects salt as Buffer
|
||||
const saltBuffer = Buffer.from(salt, 'utf8');
|
||||
|
||||
const hash = await argon2.hash(password, {
|
||||
type: argon2.argon2id,
|
||||
salt: saltBuffer,
|
||||
timeCost: DEFAULT_ENCRYPTION.iterations,
|
||||
memoryCost: DEFAULT_ENCRYPTION.memorySize,
|
||||
parallelism: DEFAULT_ENCRYPTION.parallelism,
|
||||
hashLength: 32,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
return new Uint8Array(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts data using AES-GCM symmetric encryption (matching the browser extension's EncryptionUtility).
|
||||
*
|
||||
* @param plaintext - The plaintext string to encrypt
|
||||
* @param keyBytes - The 256-bit encryption key as Uint8Array
|
||||
* @returns Base64-encoded ciphertext (IV prepended to ciphertext)
|
||||
*/
|
||||
async function symmetricEncrypt(plaintext: string, keyBytes: Uint8Array): Promise<string> {
|
||||
const key = await webcrypto.subtle.importKey(
|
||||
'raw',
|
||||
keyBytes,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
// Generate random 12-byte IV
|
||||
const iv = webcrypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Encode plaintext to bytes
|
||||
const encoder = new TextEncoder();
|
||||
const plaintextBytes = encoder.encode(plaintext);
|
||||
|
||||
// Encrypt
|
||||
const ciphertext = await webcrypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintextBytes);
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(ciphertext), iv.length);
|
||||
|
||||
// Convert to base64
|
||||
return Buffer.from(combined).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty vault database with the latest schema.
|
||||
*
|
||||
* @returns Base64-encoded SQLite database
|
||||
*/
|
||||
function createEmptyVaultDatabase(): string {
|
||||
// Create a temporary file for the database
|
||||
const tempPath = join(tmpdir(), `vault_${Date.now()}_${Math.random().toString(36).substring(2)}.db`);
|
||||
|
||||
try {
|
||||
// Create a new SQLite database
|
||||
const db = new Database(tempPath);
|
||||
|
||||
// Execute the complete schema SQL to create all tables
|
||||
// The schema is a series of SQL statements separated by semicolons
|
||||
db.exec(COMPLETE_SCHEMA_SQL);
|
||||
|
||||
// Close the database
|
||||
db.close();
|
||||
|
||||
// Read the database file and convert to base64
|
||||
const dbBytes = readFileSync(tempPath);
|
||||
return Buffer.from(dbBytes).toString('base64');
|
||||
} finally {
|
||||
// Clean up the temp file
|
||||
try {
|
||||
unlinkSync(tempPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an RSA key pair for the vault's encryption key.
|
||||
*
|
||||
* @returns Object with public and private keys as JSON strings
|
||||
*/
|
||||
async function generateRsaKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
|
||||
const keyPair = await webcrypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
const publicKey = await webcrypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
const privateKey = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey);
|
||||
|
||||
return {
|
||||
publicKey: JSON.stringify(publicKey),
|
||||
privateKey: JSON.stringify(privateKey),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares SRP registration data for a new user.
|
||||
*
|
||||
* @param username - The username for registration
|
||||
* @param password - The password for registration
|
||||
* @returns Registration request data and the encryption key
|
||||
*/
|
||||
async function prepareRegistration(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<{ request: RegisterRequest; salt: string; encryptionKey: Uint8Array }> {
|
||||
const normalizedUsername = normalizeUsername(username);
|
||||
|
||||
// Generate salt using SRP client
|
||||
const salt = srp.generateSalt();
|
||||
|
||||
// Derive key from password using Argon2Id
|
||||
const encryptionKey = await deriveKeyFromPassword(password, salt);
|
||||
|
||||
// Convert to uppercase hex string (expected by server)
|
||||
const passwordHashString = bytesToHexString(encryptionKey);
|
||||
|
||||
// Generate SRP private key and verifier
|
||||
const privateKey = srp.derivePrivateKey(salt, normalizedUsername, passwordHashString);
|
||||
const verifier = srp.deriveVerifier(privateKey);
|
||||
|
||||
return {
|
||||
request: {
|
||||
username: normalizedUsername,
|
||||
salt,
|
||||
verifier,
|
||||
encryptionType: DEFAULT_ENCRYPTION.type,
|
||||
encryptionSettings: DEFAULT_ENCRYPTION.settings,
|
||||
},
|
||||
salt,
|
||||
encryptionKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an initial empty vault to the server.
|
||||
*
|
||||
* @param apiBaseUrl - The base URL of the API
|
||||
* @param token - The authentication token
|
||||
* @param username - The username
|
||||
* @param encryptionKey - The encryption key as Uint8Array
|
||||
*/
|
||||
async function uploadInitialVault(
|
||||
apiBaseUrl: string,
|
||||
token: string,
|
||||
username: string,
|
||||
encryptionKey: Uint8Array
|
||||
): Promise<void> {
|
||||
const baseUrl = apiBaseUrl.replace(/\/$/, '') + '/v1/';
|
||||
|
||||
// Create an empty vault database
|
||||
const vaultBase64 = createEmptyVaultDatabase();
|
||||
|
||||
// Encrypt the vault
|
||||
const encryptedVault = await symmetricEncrypt(vaultBase64, encryptionKey);
|
||||
|
||||
// Generate RSA key pair for the vault
|
||||
const rsaKeyPair = await generateRsaKeyPair();
|
||||
|
||||
// Prepare the vault upload request
|
||||
const now = new Date().toISOString();
|
||||
const vaultRequest: VaultUploadRequest = {
|
||||
username: normalizeUsername(username),
|
||||
blob: encryptedVault,
|
||||
version: CURRENT_VAULT_VERSION,
|
||||
currentRevisionNumber: 0,
|
||||
encryptionPublicKey: rsaKeyPair.publicKey,
|
||||
credentialsCount: 0,
|
||||
emailAddressList: [],
|
||||
privateEmailDomainList: [],
|
||||
hiddenPrivateEmailDomainList: [],
|
||||
publicEmailDomainList: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
// Upload the vault
|
||||
const response = await fetch(`${baseUrl}Vault`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(vaultRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to upload initial vault: ${response.status} ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new test user via the API using SRP protocol and initializes their vault.
|
||||
*
|
||||
* @param apiBaseUrl - The base URL of the API (e.g., 'http://localhost:5092')
|
||||
* @param username - The username for the new account
|
||||
* @param password - The password for the new account
|
||||
* @returns The token model on success
|
||||
* @throws Error if registration fails
|
||||
*/
|
||||
export async function registerTestUser(
|
||||
apiBaseUrl: string,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<TokenModel> {
|
||||
// Normalize the API URL
|
||||
const baseUrl = apiBaseUrl.replace(/\/$/, '') + '/v1/';
|
||||
|
||||
// Prepare registration data
|
||||
const { request: registerRequest, encryptionKey } = await prepareRegistration(username, password);
|
||||
|
||||
// Send registration request to API
|
||||
const response = await fetch(`${baseUrl}Auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(registerRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let errorMessage = `Registration failed with status ${response.status}`;
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
errorMessage = errorJson.title || errorJson.message || errorMessage;
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const tokenModel = (await response.json()) as TokenModel;
|
||||
|
||||
// Upload initial empty vault
|
||||
await uploadInitialVault(apiBaseUrl, tokenModel.token, username, encryptionKey);
|
||||
|
||||
return tokenModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test user with random credentials.
|
||||
*
|
||||
* @param apiBaseUrl - The base URL of the API
|
||||
* @returns A TestUser object with credentials and token
|
||||
*/
|
||||
export async function createTestUser(apiBaseUrl: string): Promise<TestUser> {
|
||||
const username = generateTestUsername();
|
||||
const password = generateTestPassword();
|
||||
|
||||
const token = await registerTestUser(apiBaseUrl, username, password);
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the API is available.
|
||||
*
|
||||
* @param apiBaseUrl - The base URL of the API
|
||||
* @returns True if the API is reachable
|
||||
*/
|
||||
export async function isApiAvailable(apiBaseUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl.replace(/\/$/, '')}/v1/Auth/status`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
// The status endpoint returns 401 when not authenticated, but that means the API is running
|
||||
return response.status === 401 || response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user