Testing¶
Keybund edits the files that decide how you log in to every server and Git host – mistakes there
lock people out. So every layer has its own tests, and the important ones run against the real
programs: a real git, a real ssh, a real sshd on localhost. All of it runs in CI on every
change: about 150 Rust tests, 120 Vitest tests and 35 Playwright flows on two viewports.
just # lint + all tests
just test-rust # cargo test --workspace
just test-frontend # Vitest with coverage thresholds
just e2e # Playwright
just coverage # Rust coverage (cargo-llvm-cov)
The layers¶
| Layer | What it proves | Where |
|---|---|---|
| Unit (Rust) | parsing of profiles.toml and ssh_config, validation, include placement, diffing, ssh output interpretation, URL parsing, provider data, quoting |
next to the code in crates/*/src/** |
| Snapshot | the exact bytes of every generated file for a sample with servers, two clients on github.com, signing and an archived key (insta) | crates/core/src/snapshots/ |
| Integration (Rust) | apply against a temporary home; the result resolved by the real git (includeIf, worktrees, the include order) and ssh -G; backups, uninstall, import with cleanup, doctor checks, archive |
crates/core/tests/integration.rs |
| Agent | ssh-agent detection with a private SSH_AUTH_SOCK |
crates/core/tests/agent.rs |
| Real sshd | a throw-away sshd on localhost: a key with a passphrase is asked once and then served by the agent – for the connection test and for git; a wrong key is detected; installing a key like ssh-copy-id |
crates/core/tests/sshd.rs |
| CLI integration | every command against a temporary home, with fake ssh/xdg-open where a network would be needed; exit codes; refusing to ask without a terminal |
crates/cli/tests/cli.rs |
| CLI end-to-end | the full life cycle through the keybund binary only: two clients on github.com, a private default, a server, clone, which, doctor, archive – checked with the real git; and a NAS against a throw-away sshd: ssh -G resolves alias and IP from the generated files, trust, install-key (once), test succeeds and fails for real, git ls-remote over the alias, folder rules through a symlink, a linked worktree and a same-prefix sibling |
crates/cli/tests/e2e.rs |
| Scenario: two companies, private, two servers | one person with five keys and every connection real: a GitHub stand-in (an sshd whose authorized_keys maps each key to an account and lets an account see only its own repositories, like GitHub), two server sshds that allow two login tries, and an ssh-agent that before every step holds the other keys – another account's first – but not the one needed. First without Keybund: plain ssh logs in as the wrong account (“Repository not found”) and the server gives up – the setup catches a mix-up. Then with Keybund: test reaches each account, git clone inside a folder and keybund clone, commit, push and pull per folder log in as that folder's account with its e-mail, signed private commits verify, one company never reaches the other's repositories, outside every folder the private account, trust/test/ssh/git on both servers, a deploy push from a company folder to the NAS, and offboarding one company. Each key lands in the agent after its first use. Breaking IdentitiesOnly (git, Host blocks or the test) or AddKeysToAgent makes it fail |
crates/cli/tests/scenario.rs |
| macOS | the whole Rust suite once more on a macOS runner, against Apple's ssh and git (/usr/bin goes before the runner's Homebrew, and the job fails if it does not); the CLI tests expect what the binary writes there (gitdir/i:, the Keychain options). Rendering for macOS is also tested on Linux through Env::platform |
.github/workflows/ci.yml (rust-macos) |
| IPC (Tauri) | commands called through Tauri's IPC layer with the mock runtime and the real capabilities: JSON in, JSON out | app/src-tauri/tests/ipc.rs |
| Contract | bindings.ts matches the Rust commands; the mock backend behaves like the core (validation, rendering, errors) |
app/src-tauri/tests/bindings.rs, app/src/lib/mock/backend.test.ts |
| Unit & component (frontend) | Vitest + Testing Library: whole pages and flows against the mock backend – creating keys, adding uses, tests and their fixes, imports, uninstall; 90 % line coverage required | app/src/**/*.test.ts(x) |
| Translations | both catalogs complete with the same placeholders, every message used, every key used in Rust present; language detection and switching; an English browser walks every page, dialog and test result looking for German left behind | crates/core/tests/i18n.rs, app/src/lib/i18n.test.ts, app/e2e/i18n.spec.ts |
| End-to-end (frontend) | Playwright: every flow in Chromium on a desktop and a narrow window, including layout checks, and in CI once more in WebKit – the engine Tauri uses on macOS (locally with PW_WEBKIT=1) |
app/e2e/ |
Writing tests¶
- Use a temporary home. Every core function takes an
Env;Env::with_home(tempdir)points it at a throw-away directory. Never test against the real~/.ssh. - Ask the real tools. When a test is about what Git or ssh will do, let them answer: run
git config --show-originorssh -Gon the generated files instead of comparing strings. Tests that needssh,sshdorgitskip themselves when the program is missing. ssh reads~/.ssh/configfrom the passwd entry, not fromHOME: a test in a sandbox that runs the realsshthrough the binary usesSandbox::ssh_shim(), which hands ssh the sandbox's generated files (-F), so the developer's own config never leaks in. In core tests,Env::with_homedoes the same forEnv::ssh_g: it asks ssh with-F <home>/.ssh/configand maps paths ssh expanded below the real home back into the test home. - Platforms. What differs between Linux and macOS goes through
Env::platform, nevercfg!in the rendering, so a test on Linux can render and check the macOS files (Env { platform: Platform::MacOs, ..Env::with_home(dir) }). - Stand-in scripts through the helper. A fake
ssh,osascriptor opener is written withfsutil::write_test_script(core) orcommon::write_executable(CLI), never withfs::writeandchmod: while another test thread forks, a file written in the test process can still be open for writing in the child, and Linux then refuses to run it (“Text file busy”). - Snapshots for rendering. A change in
render.rsshows up as a snapshot diff; review it like code, thenjust snapshots. - Frontend tests click like a user. Query by role and label (
getByRole("button", { name: "Testen" })), not by CSS classes. The tests run in German (src/test/setup.ts,playwright.config.ts); the English texts are covered bysrc/lib/i18n.test.tsande2e/i18n.spec.ts. - Keep the mock honest. A new backend behaviour gets a contract test in
backend.test.tsand the same behaviour in Rust – otherwise the frontend tests would pass against a backend that does not exist.
Manual checks before a release¶
Some things only a real desktop shows: ssh <alias> against a real server, sftp://<alias> in
Dolphin, a push in two client folders with different accounts, a clone through the helper, a key
with a passphrase through ksshaskpass, and keybund doctor green on a fresh user account.