test: replace the Go compatibility fixtures with native round-trip tests
The Go-generated link.db, profiles.link and rec1.cast fixtures and the tests that read them are removed. Their coverage is kept by tests on data this build writes: a fresh database round-trips SSH profiles (env vars, login scripts, forwards), serial profiles, IRC room state, known hosts, snippets and preferences, and encrypted exports round-trip and reject a wrong password. The import dialog no longer offers .link files and the docs stop mentioning them.
This commit is contained in:
@@ -864,7 +864,7 @@ When exporting profiles, encrypted export (a `.nexus` file) protects the file wi
|
||||
"## Exporting Profiles
|
||||
Use **Export (Encrypted)** from the sidebar menu or command palette. Choose which profiles to include and set a password to encrypt the file. Exported files use the `.nexus` extension. Plain **JSON** export is also available, optionally including secrets.
|
||||
## Importing Profiles
|
||||
Use **Import (Encrypted)**, select a `.nexus` file (older `.link` exports from Alchementary Link work too) and enter the password used when exporting. Choose whether existing profiles are skipped, overwritten or duplicated. Imported profiles appear in your sidebar immediately.
|
||||
Use **Import (Encrypted)**, select a `.nexus` file and enter the password used when exporting. Choose whether existing profiles are skipped, overwritten or duplicated. Imported profiles appear in your sidebar immediately.
|
||||
## Importing from OpenSSH
|
||||
If you have an existing `~/.ssh/config` file, go to **Settings → About** and click **Import ~/.ssh/config**. Nexus Terminal creates profiles from your SSH Host blocks, including hostnames, usernames, ports, identity files, and jump hosts.",
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Profile export/import: encrypted `.nexus` (also reads `.link`) and plain JSON.
|
||||
//! Profile export/import: encrypted `.nexus` and plain JSON.
|
||||
|
||||
use crate::settings::{Job, poll};
|
||||
use crate::state::{Request, Shared};
|
||||
@@ -54,7 +54,7 @@ impl Transfer {
|
||||
|
||||
pub fn import_encrypted(&mut self) {
|
||||
if let Some(path) =
|
||||
rfd::FileDialog::new().set_title("Import Profiles").add_filter("Nexus Export", &[EXTENSION, "link"]).add_filter("All files", &["*"]).pick_file()
|
||||
rfd::FileDialog::new().set_title("Import Profiles").add_filter("Nexus Export", &[EXTENSION]).add_filter("All files", &["*"]).pick_file()
|
||||
{
|
||||
self.mode = Some(Mode::Import { path, pw: String::new(), strategy: "skip", status: String::new() });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Password-encrypted profile exports: `[version 1][salt 32][nonce 12][AES-256-GCM ciphertext]`
|
||||
//! over a JSON bundle, with the vault's Argon2id parameters. `.link` files from Alchementary
|
||||
//! Link are the same format.
|
||||
//! over a JSON bundle, with the vault's Argon2id parameters.
|
||||
|
||||
use crate::Result;
|
||||
use crate::models::Profile;
|
||||
@@ -76,14 +75,13 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reads_go_export_and_round_trips() {
|
||||
let fx = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/profiles.link");
|
||||
assert!(decrypt_and_import(&fx, "nope").is_err());
|
||||
let ps = decrypt_and_import(&fx, "export-pass").unwrap();
|
||||
assert_eq!(ps.len(), 6);
|
||||
assert_eq!(ps[0].ssh.as_ref().unwrap().jump_hosts[0].port, 2222);
|
||||
fn round_trips() {
|
||||
let mut p = Profile::new(crate::models::Protocol::Ssh);
|
||||
p.host = "box".into();
|
||||
let ps = vec![p];
|
||||
let out = std::env::temp_dir().join(format!("nexus-export-{}.nexus", std::process::id()));
|
||||
encrypt_and_export(&ps, "pw", &out).unwrap();
|
||||
assert!(decrypt_and_import(&out, "nope").is_err());
|
||||
assert_eq!(decrypt_and_import(&out, "pw").unwrap(), ps);
|
||||
std::fs::remove_file(out).unwrap();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Data model shared by storage, the session manager and the UI. Field names serialise exactly as
|
||||
//! the Go structs did, so database JSON columns and `.link`/`.nexus` exports round-trip unchanged.
|
||||
//! Data model shared by storage, the session manager and the UI. Field names are the JSON keys
|
||||
//! used in the database's config columns and in `.nexus` exports.
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Local};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
@@ -380,21 +380,6 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_go_fixture() {
|
||||
let store = Store::new(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/recordings"));
|
||||
let (hdr, events) = store.get("rec1").unwrap();
|
||||
assert_eq!((hdr.version, hdr.width, hdr.height, hdr.timestamp), (2, 80, 24, 1791633486));
|
||||
assert_eq!(hdr.title, "Fixture — host");
|
||||
assert_eq!(hdr.env["TERM"], "xterm-256color");
|
||||
assert_eq!(events.len(), 2);
|
||||
assert_eq!((events[0].kind.as_str(), events[0].data.as_str()), ("o", "hello \x1b[1mworld\x1b[0m\r\n"));
|
||||
assert_eq!((events[1].kind.as_str(), events[1].data.as_str()), ("i", "ls\r"));
|
||||
let metas = store.list().unwrap();
|
||||
assert_eq!(metas.len(), 1);
|
||||
assert_eq!(metas[0].duration, 0.000212149);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_and_close() {
|
||||
let dir = tmp("write");
|
||||
|
||||
@@ -274,7 +274,7 @@ impl Service {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Imports a `.nexus` (or legacy `.link`) export.
|
||||
/// Imports a `.nexus` export.
|
||||
pub fn import_encrypted(&self, path: &Path, password: &str, strategy: &str) -> Result<usize> {
|
||||
let ps = exportimport::decrypt_and_import(path, password)?;
|
||||
self.import(ps, strategy)
|
||||
|
||||
+55
-29
@@ -464,43 +464,69 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opens_go_database_and_migrates() {
|
||||
let fx = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/link.db");
|
||||
fn profiles_and_app_state_round_trip() {
|
||||
let tmp = std::env::temp_dir().join(format!("nexus-store-{}.db", std::process::id()));
|
||||
std::fs::copy(fx, &tmp).unwrap();
|
||||
let s = Store::open(&tmp).unwrap();
|
||||
let ps = s.list_profiles().unwrap();
|
||||
assert_eq!(ps.len(), 6);
|
||||
let ssh = ps.iter().find(|p| p.protocol == Protocol::Ssh).unwrap();
|
||||
assert!(ssh.favourite);
|
||||
assert_eq!(ssh.tags, ["fixture", "ssh"]);
|
||||
assert_eq!(ssh.last_connected.map(|t| fmt_time(&t)).as_deref(), Some("2026-01-02T03:04:05Z"));
|
||||
let c = ssh.ssh.as_ref().unwrap();
|
||||
assert_eq!(c.env_vars["LANG"], "en_GB.UTF-8");
|
||||
assert_eq!(c.login_scripts.len(), 2);
|
||||
assert_eq!(c.port_forwards[0].bind_port, 8080);
|
||||
let irc = ps.iter().find(|p| p.protocol == Protocol::Irc).unwrap();
|
||||
assert_eq!(s.irc_last_rooms(&irc.id).unwrap(), ["#rust", "#nexus"]);
|
||||
assert_eq!(s.list_known_hosts().unwrap()[0].fingerprint, "SHA256:abc");
|
||||
// Known hosts written by the Go app can be removed by id; unknown ids are an error.
|
||||
let mut ssh = Profile::new(Protocol::Ssh);
|
||||
ssh.host = "box".into();
|
||||
ssh.username = "me".into();
|
||||
ssh.favourite = true;
|
||||
ssh.tags = vec!["a".into(), "b".into()];
|
||||
let c = ssh.ssh.as_mut().unwrap();
|
||||
c.env_vars.insert("LANG".into(), "en_GB.UTF-8".into());
|
||||
c.login_scripts = vec![LoginScriptStep { kind: "wait".into(), value: "$".into(), timeout: 5 }];
|
||||
c.port_forwards = vec![PortForward { id: "f1".into(), bind_port: 8080, dest_host: "db".into(), dest_port: 5432, enabled: true, ..Default::default() }];
|
||||
ssh.normalize();
|
||||
s.create_profile(&ssh).unwrap();
|
||||
let ssh = ssh.with_secs();
|
||||
assert_eq!(s.get_profile(&ssh.id).unwrap(), ssh);
|
||||
s.update_profile(&ssh).unwrap();
|
||||
assert_eq!(s.get_profile(&ssh.id).unwrap(), ssh, "updates are lossless");
|
||||
let mut serial = Profile::new(Protocol::Serial);
|
||||
serial.host = "/dev/ttyUSB0".into();
|
||||
serial.normalize();
|
||||
s.create_profile(&serial).unwrap();
|
||||
assert_eq!(s.list_profiles().unwrap().len(), 2);
|
||||
|
||||
s.save_irc_last_rooms(&ssh.id, &["#rust".into(), "#nexus".into()]).unwrap();
|
||||
assert_eq!(s.irc_last_rooms(&ssh.id).unwrap(), ["#rust", "#nexus"]);
|
||||
let t = parse_time("2026-01-02T03:04:05Z");
|
||||
let kh = KnownHost {
|
||||
id: "k1".into(),
|
||||
host: "box".into(),
|
||||
port: 22,
|
||||
key_type: "ssh-ed25519".into(),
|
||||
fingerprint: "SHA256:abc".into(),
|
||||
public_key: String::new(),
|
||||
first_seen: t,
|
||||
last_seen: t,
|
||||
trusted: true,
|
||||
};
|
||||
s.save_known_host(&kh).unwrap();
|
||||
let kh = s.list_known_hosts().unwrap().remove(0);
|
||||
assert_eq!(kh.fingerprint, "SHA256:abc");
|
||||
s.delete_known_host(&kh.id).unwrap();
|
||||
assert!(s.list_known_hosts().unwrap().is_empty());
|
||||
assert!(s.delete_known_host(&kh.id).is_err());
|
||||
s.save_known_host(&kh).unwrap();
|
||||
assert!(s.delete_known_host(&kh.id).is_err(), "unknown ids are an error");
|
||||
s.create_snippet(&Snippet {
|
||||
id: "s1".into(),
|
||||
name: "up".into(),
|
||||
command: "uptime".into(),
|
||||
description: String::new(),
|
||||
folder: String::new(),
|
||||
tags: vec![],
|
||||
created_at: t,
|
||||
updated_at: t,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(s.list_snippets().unwrap()[0].command, "uptime");
|
||||
let prefs = AppPreferences { terminal_theme: "dracula".into(), ..Default::default() };
|
||||
s.save_preferences(&prefs).unwrap();
|
||||
assert_eq!(s.preferences().terminal_theme, "dracula");
|
||||
// Migration 007 lets serial profiles in.
|
||||
let mut p = Profile::new(Protocol::Serial);
|
||||
p.host = "/dev/ttyUSB0".into();
|
||||
p.normalize();
|
||||
s.create_profile(&p).unwrap();
|
||||
assert_eq!(s.get_profile(&p.id).unwrap(), p.clone().with_secs());
|
||||
// Round trip of an existing row is lossless.
|
||||
s.update_profile(ssh).unwrap();
|
||||
assert_eq!(&s.get_profile(&ssh.id).unwrap(), ssh);
|
||||
drop(s);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
for ext in ["", "-wal", "-shm"] {
|
||||
let _ = std::fs::remove_file(format!("{}{ext}", tmp.display()));
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
# Compatibility fixtures
|
||||
|
||||
Generated by the last Go build of Alchementary Link with `cmd/genfixtures` (see commit f588a79,
|
||||
`git show f588a79:cmd/genfixtures/main.go`). Nexus Terminal must keep reading them unchanged:
|
||||
|
||||
- `link.db` — a database through migration 006 with every protocol, a known host, a snippet,
|
||||
IRC channel state and preferences.
|
||||
- `profiles.link` — an encrypted export with password `export-pass`.
|
||||
- `recordings/rec1.cast` — an asciicast v2 recording.
|
||||
|
||||
Don't regenerate them; they are the reference for data written by the old app.
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
{"version":2,"width":80,"height":24,"timestamp":1791633486,"title":"Fixture — host","env":{"TERM":"xterm-256color"}}
|
||||
[0.000139144,"o","hello \u001b[1mworld\u001b[0m\r\n"]
|
||||
[0.000212149,"i","ls\r"]
|
||||
Reference in New Issue
Block a user