ci: remove stronghold plugin from e2e tests (#3638)

* ci: remove stronghold plugin from e2e tests

* revert unrelated change
This commit is contained in:
Tony
2026-09-24 21:55:25 +08:00
committed by GitHub
parent 1cae06a55b
commit 1e166e93c8
15 changed files with 3 additions and 392 deletions
-1
View File
@@ -83,7 +83,6 @@ skipped on desktop, and the rest run everywhere with the odd test gated.
| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. |
| `sql` | SQLite `load`/`execute`/`select`/`close`, bound values, column types, app-registered migrations, error paths. | Same. |
| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. |
| `stronghold` | Store records (persisted across reloads), wrong-password and unknown-client errors, vault secrets, BIP39/SLIP10 derivation and Ed25519 signing. | Same. |
| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. |
| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). |
| `websocket` | Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through `adb reverse` on Android). |
-1
View File
@@ -38,7 +38,6 @@
"@tauri-apps/plugin-shell": "workspace:*",
"@tauri-apps/plugin-sql": "workspace:*",
"@tauri-apps/plugin-store": "workspace:*",
"@tauri-apps/plugin-stronghold": "workspace:*",
"@tauri-apps/plugin-updater": "workspace:*",
"@tauri-apps/plugin-upload": "workspace:*",
"@tauri-apps/plugin-websocket": "workspace:*",
-2
View File
@@ -26,7 +26,6 @@ import type * as Process from '@tauri-apps/plugin-process'
import type * as Shell from '@tauri-apps/plugin-shell'
import type * as Sql from '@tauri-apps/plugin-sql'
import type * as Store from '@tauri-apps/plugin-store'
import type * as Stronghold from '@tauri-apps/plugin-stronghold'
import type * as Updater from '@tauri-apps/plugin-updater'
import type * as Upload from '@tauri-apps/plugin-upload'
import type * as WebSocket from '@tauri-apps/plugin-websocket'
@@ -52,7 +51,6 @@ export interface CommonPluginApi {
shell: typeof Shell
sql: typeof Sql.default
store: typeof Store
stronghold: typeof Stronghold
upload: typeof Upload
websocket: typeof WebSocket.default
}
@@ -108,7 +108,6 @@ const commonSurface: Surface<CommonPluginApi> = {
// the `Database` class itself
sql: ['load', 'get'],
store: ['load', 'getStore', 'LazyStore', 'Store'],
stronghold: ['Location', 'Client', 'Store', 'Vault', 'Stronghold'],
upload: ['download', 'upload', 'HttpMethod'],
// the `WebSocket` class itself
websocket: ['connect']
@@ -1,248 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { expect } from '@wdio/globals'
import {
tauri,
tauriError,
describePlugin,
scratchDir
} from '../helpers/index.js'
// The example derives the snapshot key from the password with argon2. Each
// test works on its own snapshot file under the spec's scratch directory,
// which the fs plugin cleans up; the snapshot path is resolved inside the page.
const dir = scratchDir('stronghold')
const password = 'e2e-password'
const clientName = 'e2e-client'
/** An arbitrary, valid BIP39 mnemonic, so derived keys are deterministic. */
const mnemonic =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
describePlugin('stronghold', () => {
before(async () => {
await tauri(async (api, dir) => {
const baseDir = api.fs.BaseDirectory.AppData
if (await api.fs.exists(dir, { baseDir })) {
await api.fs.remove(dir, { baseDir, recursive: true })
}
await api.fs.mkdir(dir, { baseDir, recursive: true })
}, dir)
})
after(async () => {
await tauri(async (api, dir) => {
const baseDir = api.fs.BaseDirectory.AppData
if (await api.fs.exists(dir, { baseDir })) {
await api.fs.remove(dir, { baseDir, recursive: true })
}
}, dir)
})
it('store records round-trip and persist in the snapshot', async () => {
const result = await tauri(
async (api, dir, password, clientName) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'store.stronghold'
)
const value = Array.from(new TextEncoder().encode('top secret'))
const stronghold = await api.stronghold.Stronghold.load(path, password)
const client = await stronghold.createClient(clientName)
await client.getStore().insert('key', value)
const sameSession = await client.getStore().get('key')
await stronghold.save()
await stronghold.unload()
const reopened = await api.stronghold.Stronghold.load(path, password)
const store = (await reopened.loadClient(clientName)).getStore()
const persisted = await store.get('key')
const missing = await store.get('missing')
await reopened.unload()
return {
sameSession: sameSession && Array.from(sameSession),
persisted: persisted && new TextDecoder().decode(persisted),
missing,
fileExists: await api.fs.exists(path)
}
},
dir,
password,
clientName
)
expect(result.sameSession).toEqual(
Array.from(new TextEncoder().encode('top secret'))
)
expect(result.persisted).toBe('top secret')
expect(result.missing).toBeNull()
expect(result.fileExists).toBe(true)
})
it('store.remove returns the removed value', async () => {
const result = await tauri(
async (api, dir, password, clientName) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'remove.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
const store = (await stronghold.createClient(clientName)).getStore()
await store.insert('key', [1, 2, 3])
const removed = await store.remove('key')
const after = await store.get('key')
await stronghold.unload()
return { removed: removed && Array.from(removed), after }
},
dir,
password,
clientName
)
expect(result).toEqual({ removed: [1, 2, 3], after: null })
})
it('a snapshot cannot be opened with the wrong password', async () => {
const path = await tauri(
async (api, dir, password, clientName) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'password.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
await stronghold.createClient(clientName)
await stronghold.unload()
return path
},
dir,
password,
clientName
)
const error = await tauriError(
(api, path) => api.stronghold.Stronghold.load(path, 'wrong-password'),
path
)
expect(error).toMatch(/failed to decode\/decrypt/)
})
it('loading a client that was never created is rejected', async () => {
const error = await tauriError(
async (api, dir, password) => {
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'no-client.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
try {
await stronghold.loadClient('never-created')
} finally {
await stronghold.unload()
}
},
dir,
password
)
expect(error).toMatch(/error loading client data/)
})
it('vault procedures derive keys and sign without exposing secrets', async () => {
const result = await tauri(
async (api, dir, password, clientName, mnemonic) => {
const { Location } = api.stronghold
const path = await api.path.join(
await api.path.appDataDir(),
dir,
'vault.stronghold'
)
const stronghold = await api.stronghold.Stronghold.load(path, password)
const vault = (await stronghold.createClient(clientName)).getVault(
'vault'
)
// the same mnemonic recovered twice derives the same key
const seedA = Location.generic('vault', 'seed-a')
const seedB = Location.generic('vault', 'seed-b')
await vault.recoverBIP39(mnemonic, seedA)
await vault.recoverBIP39(mnemonic, seedB)
// Ed25519 SLIP-10 only derives hardened indices (the high bit set).
// (No named helper: the transpiler would wrap it in a `__name` call
// that does not exist in the page.)
const chain = [44, 4218, 0, 0, 0].map((i) => (i | 0x80000000) >>> 0)
const otherChain = [44, 4218, 0, 0, 1].map(
(i) => (i | 0x80000000) >>> 0
)
const keyA = Location.generic('vault', 'key-a')
const keyB = Location.generic('vault', 'key-b')
const keyOther = Location.generic('vault', 'key-other')
await vault.deriveSLIP10(chain, 'Seed', seedA, keyA)
await vault.deriveSLIP10(chain, 'Seed', seedB, keyB)
await vault.deriveSLIP10(otherChain, 'Seed', seedA, keyOther)
const publicA = Array.from(await vault.getEd25519PublicKey(keyA))
const publicB = Array.from(await vault.getEd25519PublicKey(keyB))
const publicOther = Array.from(
await vault.getEd25519PublicKey(keyOther)
)
const signature = Array.from(await vault.signEd25519(keyA, 'message'))
const signatureAgain = Array.from(
await vault.signEd25519(keyA, 'message')
)
// a random seed and a raw secret can be stored and removed
const random = Location.generic('vault', 'random')
await vault.generateSLIP10Seed(random)
await vault.insert('raw', [9, 9, 9])
await vault.remove(Location.generic('vault', 'raw'))
// a generated mnemonic recovers the seed generateBIP39 stored
const generatedSeed = Location.generic('vault', 'generated')
const mnemonicBytes = await vault.generateBIP39(generatedSeed)
const recoveredSeed = Location.generic('vault', 'recovered')
await vault.recoverBIP39(
new TextDecoder().decode(mnemonicBytes),
recoveredSeed
)
const generatedKey = Location.generic('vault', 'generated-key')
const recoveredKey = Location.generic('vault', 'recovered-key')
await vault.deriveSLIP10(chain, 'Seed', generatedSeed, generatedKey)
await vault.deriveSLIP10(chain, 'Seed', recoveredSeed, recoveredKey)
const generatedPublic = Array.from(
await vault.getEd25519PublicKey(generatedKey)
)
const recoveredPublic = Array.from(
await vault.getEd25519PublicKey(recoveredKey)
)
await stronghold.unload()
return {
publicA,
publicB,
publicOther,
signature,
signatureAgain,
generatedPublic,
recoveredPublic
}
},
dir,
password,
clientName,
mnemonic
)
expect(result.publicA).toHaveLength(32)
expect(result.publicB).toEqual(result.publicA)
expect(result.publicOther).not.toEqual(result.publicA)
// Ed25519 signatures are 64 bytes and deterministic
expect(result.signature).toHaveLength(64)
expect(result.signatureAgain).toEqual(result.signature)
expect(result.recoveredPublic).toEqual(result.generatedPublic)
expect(result.generatedPublic).not.toEqual(result.publicA)
})
})
+2 -23
View File
@@ -303,33 +303,12 @@ function tauriCli(args: string[], env: NodeJS.ProcessEnv = {}): void {
* Environment for the Android build.
*
* - No debug info: the suite needs a debug build (webview debugging follows
* `debug_assertions`), but with the stronghold, sql and websocket plugins the
* `debug_assertions`), but with the sql and websocket plugins the
* debug info alone grows the APK past what a default emulator can install
* ("not enough space").
* - On a macOS host, `AR`/`RANLIB` point at the NDK's LLVM tools. Autotools-built
* C dependencies (libsodium, through the stronghold plugin) otherwise fall back
* to Apple's `ar`/`ranlib`, which silently produce an empty archive from the
* Android (ELF) objects, and the app then fails to load its library with an
* unresolved symbol. Linux hosts' GNU `ar` is fine.
*/
function androidBuildEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { CARGO_PROFILE_DEV_DEBUG: '0' }
const ndk = process.env.NDK_HOME ?? process.env.ANDROID_NDK_HOME
if (process.platform !== 'darwin' || !ndk) return env
// the NDK only ships an x86_64 (Rosetta-compatible) macOS toolchain
const bin = path.join(
ndk,
'toolchains',
'llvm',
'prebuilt',
'darwin-x86_64',
'bin'
)
return {
...env,
AR: path.join(bin, 'llvm-ar'),
RANLIB: path.join(bin, 'llvm-ranlib')
}
return { CARGO_PROFILE_DEV_DEBUG: '0' }
}
/** `adb` from the Android SDK, else whatever is on `PATH`. */