Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
159 lines
4.7 KiB
Rust
159 lines
4.7 KiB
Rust
//! L9 tests for [`ConnectRemote`] with a mock [`RemoteHost`]. The same use case
|
|
//! must behave identically whatever the host kind (Liskov), so we drive it with a
|
|
//! fake host parameterised by kind + root reachability.
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use async_trait::async_trait;
|
|
use domain::events::DomainEvent;
|
|
use domain::ports::{
|
|
DirEntry, EventBus, EventStream, FileSystem, FsError, ProcessSpawner, PtyPort, RemoteError,
|
|
RemoteHost, RemotePath,
|
|
};
|
|
use domain::{ProjectId, RemoteKind};
|
|
use uuid::Uuid;
|
|
|
|
use application::{ConnectRemote, ConnectRemoteInput};
|
|
|
|
// --- Fake filesystem (only `exists` matters here) -------------------------
|
|
|
|
struct FakeFs {
|
|
existing_root: Option<String>,
|
|
}
|
|
#[async_trait]
|
|
impl FileSystem for FakeFs {
|
|
async fn read(&self, p: &RemotePath) -> Result<Vec<u8>, FsError> {
|
|
Err(FsError::NotFound(p.as_str().to_owned()))
|
|
}
|
|
async fn write(&self, _p: &RemotePath, _d: &[u8]) -> Result<(), FsError> {
|
|
Ok(())
|
|
}
|
|
async fn exists(&self, p: &RemotePath) -> Result<bool, FsError> {
|
|
Ok(self.existing_root.as_deref() == Some(p.as_str()))
|
|
}
|
|
async fn create_dir_all(&self, _p: &RemotePath) -> Result<(), FsError> {
|
|
Ok(())
|
|
}
|
|
async fn list(&self, _p: &RemotePath) -> Result<Vec<DirEntry>, FsError> {
|
|
Ok(Vec::new())
|
|
}
|
|
async fn symlink(&self, _s: &RemotePath, _d: &RemotePath) -> Result<(), FsError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// --- Fake remote host -----------------------------------------------------
|
|
|
|
struct FakeHost {
|
|
kind: RemoteKind,
|
|
connect_ok: bool,
|
|
fs: Arc<dyn FileSystem>,
|
|
}
|
|
impl FakeHost {
|
|
fn make(kind: RemoteKind, connect_ok: bool, existing_root: Option<&str>) -> Arc<dyn RemoteHost> {
|
|
Arc::new(Self {
|
|
kind,
|
|
connect_ok,
|
|
fs: Arc::new(FakeFs {
|
|
existing_root: existing_root.map(ToOwned::to_owned),
|
|
}),
|
|
})
|
|
}
|
|
}
|
|
#[async_trait]
|
|
impl RemoteHost for FakeHost {
|
|
fn kind(&self) -> RemoteKind {
|
|
self.kind
|
|
}
|
|
async fn connect(&self) -> Result<(), RemoteError> {
|
|
if self.connect_ok {
|
|
Ok(())
|
|
} else {
|
|
Err(RemoteError::Connection("refused".to_owned()))
|
|
}
|
|
}
|
|
fn file_system(&self) -> Arc<dyn FileSystem> {
|
|
Arc::clone(&self.fs)
|
|
}
|
|
fn process_spawner(&self) -> Arc<dyn ProcessSpawner> {
|
|
unreachable!("ConnectRemote does not use the spawner")
|
|
}
|
|
fn pty(&self) -> Arc<dyn PtyPort> {
|
|
unreachable!("ConnectRemote does not use the pty")
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
struct SpyBus(Arc<Mutex<Vec<DomainEvent>>>);
|
|
impl SpyBus {
|
|
fn events(&self) -> Vec<DomainEvent> {
|
|
self.0.lock().unwrap().clone()
|
|
}
|
|
}
|
|
impl EventBus for SpyBus {
|
|
fn publish(&self, e: DomainEvent) {
|
|
self.0.lock().unwrap().push(e);
|
|
}
|
|
fn subscribe(&self) -> EventStream {
|
|
Box::new(std::iter::empty())
|
|
}
|
|
}
|
|
|
|
fn pid() -> ProjectId {
|
|
ProjectId::from_uuid(Uuid::from_u128(1))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn connect_succeeds_and_emits_event_for_any_host_kind() {
|
|
// Liskov: identical behaviour for Local, Ssh and Wsl hosts.
|
|
for kind in [RemoteKind::Local, RemoteKind::Ssh, RemoteKind::Wsl] {
|
|
let host = FakeHost::make(kind, true, Some("/srv/app"));
|
|
let bus = SpyBus::default();
|
|
let out = ConnectRemote::new(Arc::new(bus.clone()))
|
|
.execute(ConnectRemoteInput {
|
|
host,
|
|
project_id: pid(),
|
|
root: "/srv/app".to_owned(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(out.kind, kind);
|
|
assert_eq!(
|
|
bus.events(),
|
|
vec![DomainEvent::RemoteConnected { project_id: pid() }]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn connect_propagates_connection_failure() {
|
|
let host = FakeHost::make(RemoteKind::Ssh, false, Some("/srv/app"));
|
|
let bus = SpyBus::default();
|
|
let err = ConnectRemote::new(Arc::new(bus.clone()))
|
|
.execute(ConnectRemoteInput {
|
|
host,
|
|
project_id: pid(),
|
|
root: "/srv/app".to_owned(),
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(err.code(), "REMOTE", "got {err:?}");
|
|
assert!(bus.events().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn connect_fails_when_root_unreachable() {
|
|
let host = FakeHost::make(RemoteKind::Local, true, Some("/other"));
|
|
let bus = SpyBus::default();
|
|
let err = ConnectRemote::new(Arc::new(bus.clone()))
|
|
.execute(ConnectRemoteInput {
|
|
host,
|
|
project_id: pid(),
|
|
root: "/srv/app".to_owned(),
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
|
|
assert!(bus.events().is_empty(), "no event when root is missing");
|
|
}
|