mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
All existing tests mock ssh2, so the actual auth + SFTP channel code has been entirely untested up to now. Three real-world bugs in PRs #49 (ssh_config) and #50 (jump host) — neither caught by mocks — confirmed it was time to wire up real-network tests. This PR adds a reproducible docker sshd container, a vitest integration suite that talks to it via the actual SftpClient, and a CI job that brings the container up, runs the suite, and tears it down. ## Components - `docker/test-sshd/Dockerfile` — Ubuntu 22.04 + openssh-server. Pre-creates a `tester` user (UID 1000) with `vault/` in their home, pubkey-only auth, `StrictModes no` so a bind-mounted authorized_keys works regardless of host UID. - `docker-compose.yml` (repo root) — single `sshd` service on 127.0.0.1:2222, bind-mounts the public key + a host-side `docker/test-vault/` directory, healthcheck on port 22. - `docker/test-vault/` — bind-mounted vault root the test writes into. Contents are gitignored aside from a README. - `docker/keys/` — gitignored; populated on first run. - `scripts/start-test-sshd.mjs` — generates an ed25519 keypair on first run (otherwise reuses), `docker compose up -d --build`, polls `Health.Status` until healthy. Idempotent. - `scripts/stop-test-sshd.mjs` — `docker compose down -v`. - `vitest.integration.config.ts` — separate config that includes `tests/integration/**` and serialises files (no fileParallelism) so they don't fight over the single sshd. Default `vitest.config.ts` excludes the integration dir so `npm test` stays fast. - `tests/integration/ssh.integration.test.ts` — opens a real SSH session with the generated keypair and exercises list / read / write (small text + 64KB binary) / stat / exists / remove through the actual SftpClient. Each run uses a unique subdir under `/home/tester/vault` so leftovers don't collide. - `package.json` scripts: `sshd:start`, `sshd:stop`, `test:integration`. `npm test` is unchanged. - `.github/workflows/integration.yml` — separate from `ci.yml` so the integration run doesn't gate other PR signals. Brings the container up before the test step, tears it down in a finally-style step, uploads `docker logs` as an artefact on failure. ## Local usage ``` cd plugin npm run sshd:start npm run test:integration npm run sshd:stop ``` Requires Docker (Desktop or daemon) on PATH. Unit tests (`npm test`) work without Docker. ## Out of scope (B-2 / B-4) - ProxyJump / two-container bastion → target topology - Daemon (obsidian-remote-server) auto-deploy + RPC handshake end-to-end against the container Both queued for a follow-up PR once this base lands. ## Version 0.3.2 → 0.4.0 (new feature surface, minor bump). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
3.2 KiB
JavaScript
82 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Bring up the test sshd container.
|
|
*
|
|
* - Generates `docker/keys/id_test{,.pub}` if missing (ed25519,
|
|
* no passphrase). Both files are gitignored.
|
|
* - Runs `docker compose up -d --build` from the repo root.
|
|
* - Waits for sshd to accept connections on 127.0.0.1:2222 (using
|
|
* the compose healthcheck) before returning.
|
|
* - Prints the public key, container name, and host:port so a
|
|
* human knows what just happened.
|
|
*
|
|
* Idempotent: re-running it just re-checks the keys, brings the
|
|
* container back up if it was stopped, and waits for healthy.
|
|
*
|
|
* Used by the integration test suite (`npm run test:integration`).
|
|
*/
|
|
|
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(here, '..', '..');
|
|
const keyDir = path.join(repoRoot, 'docker', 'keys');
|
|
const keyPath = path.join(keyDir, 'id_test');
|
|
const pubPath = `${keyPath}.pub`;
|
|
|
|
fs.mkdirSync(keyDir, { recursive: true });
|
|
fs.mkdirSync(path.join(repoRoot, 'docker', 'test-vault'), { recursive: true });
|
|
|
|
if (!fs.existsSync(keyPath)) {
|
|
console.log(`Generating ed25519 keypair at ${keyPath}`);
|
|
// -N '' = no passphrase, -q = no banner. -f overwrites (already
|
|
// checked nonexistence above).
|
|
spawnRequire('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', keyPath, '-q', '-C', 'obsidian-remote-ssh-test']);
|
|
} else {
|
|
console.log(`Reusing existing keypair at ${keyPath}`);
|
|
}
|
|
|
|
console.log('Bringing up docker compose service `sshd`…');
|
|
spawnRequire('docker', ['compose', 'up', '-d', '--build', 'sshd'], { cwd: repoRoot });
|
|
|
|
console.log('Waiting for sshd to be healthy…');
|
|
const deadline = Date.now() + 60_000;
|
|
while (Date.now() < deadline) {
|
|
const out = spawnSync('docker', ['inspect', '--format', '{{.State.Health.Status}}',
|
|
'obsidian-remote-ssh-test-sshd'], { encoding: 'utf8' });
|
|
const status = (out.stdout || '').trim();
|
|
if (status === 'healthy') {
|
|
console.log('sshd is healthy.');
|
|
console.log('');
|
|
console.log(` host: 127.0.0.1`);
|
|
console.log(` port: 2222`);
|
|
console.log(` user: tester`);
|
|
console.log(` private key: ${keyPath}`);
|
|
console.log(` public key: ${pubPath}`);
|
|
console.log('');
|
|
process.exit(0);
|
|
}
|
|
if (status === 'unhealthy' || status === 'exited') {
|
|
console.error(`sshd entered unhealthy state: ${status}`);
|
|
process.exit(1);
|
|
}
|
|
// 'starting' or empty (container not yet up) — wait a tick.
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
console.error('Timed out waiting for sshd to become healthy.');
|
|
process.exit(1);
|
|
|
|
// ─── helpers ───────────────────────────────────────────────────────────
|
|
|
|
function spawnRequire(cmd, args, opts = {}) {
|
|
const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
|
|
if (r.status !== 0) {
|
|
if (r.error && r.error.code === 'ENOENT') {
|
|
console.error(`Missing required tool: ${cmd}. Is it on PATH?`);
|
|
}
|
|
process.exit(r.status ?? 1);
|
|
}
|
|
}
|