# netssh test environment A local Docker environment for testing every netssh feature against real services: two NetBox instances, SSH targets on three networks, a jump host, and a client with ssh, sftp, scp, tmux, zellij, bash, zsh, and fish. It is for local use only and is not part of CI/CD. ```sh testenv/up.sh # build, start, seed NetBox (first start ~6 min, then ~1 min) testenv/test.sh # run all tests; testenv/test.sh tunnel runs only matching files testenv/shell.sh # interactive shell in the client, netssh configured testenv/down.sh # stop and delete everything (volumes, generated state) ``` `test.sh` rebuilds the client image first, so the tests always run the current source. `NO_BUILD=1 testenv/test.sh` skips that. Exit code 1 if any check fails. Requirements: Docker with Compose v2, about 3 GB RAM, ports 18000/18001 on 127.0.0.1 free. ## Topology ``` public 10.231.0.0/24 + fd00:30::/64 mgmt 10.231.1.0/24 prod 10.231.2.0/24 client .10 ────────────┬─ bastion .30 ───────────────────────────────┬─ (only via bastion) netbox-fsn .20 │ fsn1-pve1..3 .11-.13 (mgmt0) │ fsn1-pve1..3 .11-.13 (eth0) netbox-lab .21 │ lab-db1 .41, lab-web1 .42 │ web1 .21, web2 .22 legacy-box .50 client .10 ───────┘ v6-host fd00:30::60 ``` | NetBox | Host | Kind | Primary IP | Other | Tags | |---|---|---|---|---|---| | fsn | `fsn1-pve1..3.hoster.test` | device, cluster `pve-fsn1` | eth0 = prod (needs bastion) | mgmt0 = mgmt (direct) | `pve` | | fsn | `bastion.hoster.test` | device | 10.231.0.30 | | `bastion` | | fsn | `v6-host` | device | fd00:30::60 (IPv6 only) | | `ipv6` | | fsn | `web1`, `web2` | VM, cluster `pve-fsn1` | prod (needs bastion) | | `web` | | lab | `lab-db1` | device | 10.231.1.41 | | `db` | | lab | `web1` | VM, cluster `lab-k8s` | 10.231.1.42 | | `web` | | — | `legacy-box` (10.231.0.50) | static host | | | | - **fsn** uses a v2 token (`nbt_…`), **lab** a legacy v1 token; both come from `.state/env`. - `web1` exists in both instances (ambiguity, `instance/host`). - Docker DNS resolves the `*.hoster.test` names on the mgmt network (`hostname` strategy). - Every target runs sshd on ports 22 and 2222 with users `ops` and `admin`, and an HTTP server on its own 127.0.0.1:8081 for tunnel tests ("hello from HOST"). - NetBox UI: http://127.0.0.1:18000 (fsn), http://127.0.0.1:18001 (lab), login `admin` / `admin`. ## Files | Path | Purpose | |---|---| | `compose.yaml` | All services and networks. | | `client.Dockerfile`, `client-entrypoint.sh` | Client image; builds netssh from the repository. | | `targets/` | SSH target image (sshd config, entrypoint). | | `seed/seed.py` | Fills both NetBox instances via the REST API (idempotent), writes `.state/`. | | `tests/run.sh`, `tests/lib.sh` | Test runner and helpers (`check`, `expect_out`, `expect_eq`, `expect_fail`, `wait_for`). | | `tests/NN-*.sh` | Test files; each runs in a fresh `HOME`. | | `tests/drive.py` | Drives `netssh configure` in huh's accessible mode. | | `.keys/`, `.state/` | Generated test key and seed output (git-ignored). | ## Writing tests Each `tests/NN-name.sh` is sourced by `run.sh` in a subshell with a new `HOME`, `XDG_CONFIG_HOME`, and `XDG_CACHE_HOME`. Write the netssh config with `config <<'EOF' … EOF` (both NetBox instances plus your YAML) or `config_only` (no NetBox), then use the helpers: ```sh config <<'EOF' ssh: {default_user: ops} EOF expect_eq "connect" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname expect_out "dry run" "-J ops@10.231.0.30" netssh --dry-run fsn/web1 expect_fail "ambiguous" "found 2 exact matches" netssh web1 ``` ssh always reads `/home/tester/.ssh/config` (OpenSSH ignores `$HOME`): it uses the test key, `BatchMode`, `StrictHostKeyChecking accept-new`, and `LogLevel ERROR`. Tests that need their own known_hosts set `ssh.options.UserKnownHostsFile`. The TUI test runs netssh inside tmux and reads the screen with `tmux capture-pane`. ## Not covered - The installer (`install.sh`) downloads release assets from Gitea; it is tested by the Go tests with local fixtures. - macOS paths and behaviour. - `netssh uninstall` runs only with `--dry-run`, so the client keeps its binary.