use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use holger_ui::data::UiData;
use server_lib::exposed::fast_routes::FastRoutes;
use server_lib::grpc::holger_proto::{
admin_service_server::AdminServiceServer, archive_service_server::ArchiveServiceServer,
repository_service_server::RepositoryServiceServer,
};
use server_lib::grpc::HolgerGrpc;
use server_lib::RemoteHolger;
use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait};
#[cfg(feature = "testmatrix")]
fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
struct MemRepo {
name: String,
writable: bool,
store: Mutex<HashMap<(Option<String>, String, String), Vec<u8>>>,
}
impl MemRepo {
fn new(name: &str, writable: bool) -> Self {
Self {
name: name.to_string(),
writable,
store: Mutex::new(HashMap::new()),
}
}
fn key(id: &ArtifactId) -> (Option<String>, String, String) {
(id.namespace.clone(), id.name.clone(), id.version.clone())
}
}
impl RepositoryBackendTrait for MemRepo {
fn name(&self) -> &str {
&self.name
}
fn format(&self) -> ArtifactFormat {
ArtifactFormat::Rust
}
fn is_writable(&self) -> bool {
self.writable
}
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
Ok(self.store.lock().unwrap().get(&Self::key(id)).cloned())
}
fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
self.store.lock().unwrap().insert(Self::key(id), data.to_vec());
Ok(())
}
fn list(
&self,
name_filter: Option<&str>,
limit: usize,
) -> anyhow::Result<Vec<traits::ArtifactEntry>> {
let store = self.store.lock().unwrap();
let mut out = Vec::new();
for ((namespace, name, version), bytes) in store.iter() {
if let Some(f) = name_filter {
if !name.contains(f) {
continue;
}
}
out.push(traits::ArtifactEntry {
id: ArtifactId {
namespace: namespace.clone(),
name: name.clone(),
version: version.clone(),
},
size_bytes: bytes.len() as i64,
content_type: "application/octet-stream".into(),
});
if limit != 0 && out.len() >= limit {
break;
}
}
Ok(out)
}
fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
let files = vec![
"crates/serde-1.0.0.crate".to_string(),
"crates/tokio-1.2.0.crate".to_string(),
];
Ok(match prefix {
Some(p) => files.into_iter().filter(|f| f.starts_with(p)).collect(),
None => files,
})
}
fn archive_info(&self) -> anyhow::Result<traits::ArchiveInfo> {
Ok(traits::ArchiveInfo {
file_count: 2,
total_uncompressed_bytes: 1234,
archive_path: self.name.clone(),
})
}
fn handle_http2_request(
&self,
_method: &str,
_suburl: &str,
_body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
Ok((404, Vec::new(), b"not found".to_vec()))
}
}
fn routes() -> FastRoutes {
let writable: Arc<dyn RepositoryBackendTrait> = Arc::new(MemRepo::new("rust-dev", true));
let readonly: Arc<dyn RepositoryBackendTrait> = Arc::new(MemRepo::new("rust-prod", false));
FastRoutes::new(vec![
("rust-dev".to_string(), writable),
("rust-prod".to_string(), readonly),
])
}
fn spawn_holger() -> SocketAddr {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("server runtime");
rt.block_on(async move {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port");
tx.send(listener.local_addr().expect("local_addr"))
.expect("send addr");
let state = Arc::new(HolgerGrpc::new(routes())); tonic::transport::Server::builder()
.add_service(RepositoryServiceServer::new(state.clone()))
.add_service(ArchiveServiceServer::new(state.clone()))
.add_service(AdminServiceServer::new(state.clone()))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
.expect("serve");
});
});
rx.recv().expect("server address")
}
fn remote_ui(addr: SocketAddr, token: Option<&str>) -> UiData {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let endpoint = format!("http://{addr}");
let remote = match token {
Some(t) => rt.block_on(RemoteHolger::connect_with_token(endpoint, t)),
None => rt.block_on(RemoteHolger::connect(endpoint)),
}
.expect("connect to in-process holger");
UiData::with_runtime(Arc::new(remote), rt)
}
#[test]
fn ui_data_round_trips_over_real_grpc() {
let addr = spawn_holger();
let mut ui = remote_ui(addr, None);
ui.refresh_status();
assert!(ui.status.loaded, "status error: {:?}", ui.status.error);
assert_eq!(ui.status.status, "ok");
assert!(!ui.status.version.is_empty());
ui.refresh_repos();
assert_eq!(ui.repos.repos.len(), 2);
let dev = ui
.repos
.repos
.iter()
.position(|r| r.name == "rust-dev")
.expect("rust-dev");
ui.select_repo(dev);
assert!(ui.repos.upload_enabled());
let id = ArtifactId {
namespace: None,
name: "serde".into(),
version: "1.0.0".into(),
};
ui.put_artifact("rust-dev", &id, b"CRATE_BYTES")
.expect("put over grpc");
ui.fetch_artifact("rust-dev", id.clone());
assert!(ui.artifact.found);
assert_eq!(ui.artifact.size_bytes, b"CRATE_BYTES".len() as u64);
assert_eq!(ui.artifact.data, b"CRATE_BYTES");
ui.refresh_browse("rust-dev", None);
assert!(ui.browse.error.is_none(), "browse error: {:?}", ui.browse.error);
let row = ui
.browse
.entries
.iter()
.find(|r| r.name == "serde" && r.version == "1.0.0")
.expect("put artifact present in browse listing");
assert_eq!(row.size_bytes, b"CRATE_BYTES".len() as i64);
#[cfg_attr(not(feature = "testmatrix"), allow(unused_variables))]
let browsed_size = row.size_bytes;
ui.refresh_archive("rust-dev", None);
assert!(ui.archive.error.is_none(), "archive error: {:?}", ui.archive.error);
assert_eq!(ui.archive.repository, "rust-dev");
assert_eq!(ui.archive.file_count, 2);
assert_eq!(ui.archive.total_uncompressed_bytes, 1234);
assert_eq!(ui.archive.archive_path, "rust-dev");
assert_eq!(ui.archive.files.len(), 2);
assert!(ui
.archive
.files
.iter()
.any(|f| f == "crates/serde-1.0.0.crate"));
let err = ui.put_artifact("rust-prod", &id, b"x").unwrap_err();
let msg = err.to_string().to_lowercase();
assert!(
msg.contains("read-only") || msg.contains("permission"),
"unexpected error: {err}"
);
let missing = ArtifactId {
namespace: None,
name: "does-not-exist".into(),
version: "0.0.0".into(),
};
ui.fetch_artifact("rust-dev", missing);
assert!(!ui.artifact.found);
assert!(ui.artifact.error.is_none());
#[cfg(feature = "testmatrix")]
fstatus(
"holger-ui",
"remote_grpc_round_trip",
ui.status.status == "ok"
&& ui.repos.repos.len() == 2
&& browsed_size == b"CRATE_BYTES".len() as i64
&& ui.archive.file_count == 2
&& (msg.contains("read-only") || msg.contains("permission"))
&& !ui.artifact.found,
&format!(
"over gRPC: status={} {} repos, put/fetch {} bytes, archive {} files, PERMISSION_DENIED mapped",
ui.status.status,
ui.repos.repos.len(),
browsed_size,
ui.archive.file_count,
),
);
}
#[test]
fn bearer_token_path_is_wire_correct() {
let addr = spawn_holger();
let mut ui = remote_ui(addr, Some("test-token-123"));
ui.refresh_status();
assert!(ui.status.loaded, "status error: {:?}", ui.status.error);
ui.refresh_repos();
assert_eq!(ui.repos.repos.len(), 2);
#[cfg(feature = "testmatrix")]
fstatus(
"holger-ui",
"bearer_token_wire_correct",
ui.status.loaded && ui.repos.repos.len() == 2,
&format!("bearer interceptor: status loaded={} repos={}", ui.status.loaded, ui.repos.repos.len()),
);
}