fabro/lib/crates/fabro-devcontainer/tests/it/integration.rs
Bryan Helmkamp 5eeacd7864 fmt
2026-04-11 11:27:46 -04:00

209 lines
6.3 KiB
Rust

use fabro_devcontainer::{Command, DevcontainerResolver};
use super::helpers::fixture_path;
#[tokio::test]
async fn resolve_image_only() {
let config = DevcontainerResolver::resolve(&fixture_path("image-only"))
.await
.unwrap();
assert!(
config
.dockerfile
.contains("FROM mcr.microsoft.com/devcontainers/base:ubuntu")
);
assert_eq!(config.remote_user.as_deref(), Some("vscode"));
assert_eq!(config.forwarded_ports, vec![3000, 80, 9090]);
assert_eq!(
config.environment.get("EDITOR").map(String::as_str),
Some("code")
);
assert_eq!(config.workspace_folder, "/workspaces/image-only");
assert!(config.compose_files.is_empty());
assert!(config.compose_service.is_none());
assert_eq!(config.post_create_commands.len(), 1);
assert!(matches!(&config.post_create_commands[0], Command::Shell(s) if s == "echo hello"));
// onCreateCommand
assert_eq!(config.on_create_commands.len(), 1);
assert!(matches!(&config.on_create_commands[0], Command::Shell(s) if s == "setup.sh"));
// containerEnv baked into Dockerfile
assert!(
config
.dockerfile
.contains("ENV DEBIAN_FRONTEND=noninteractive")
);
assert_eq!(
config
.container_env
.get("DEBIAN_FRONTEND")
.map(String::as_str),
Some("noninteractive")
);
}
#[tokio::test]
async fn resolve_dockerfile_mode() {
let config = DevcontainerResolver::resolve(&fixture_path("dockerfile-mode"))
.await
.unwrap();
// Should read the actual Dockerfile content
assert!(config.dockerfile.contains("FROM node:20"));
assert!(config.dockerfile.contains("apt-get update"));
assert_eq!(config.remote_user.as_deref(), Some("developer"));
assert_eq!(config.forwarded_ports, vec![4000]);
assert_eq!(config.post_create_commands.len(), 1);
assert!(matches!(&config.post_create_commands[0], Command::Shell(s) if s == "npm install"));
// build.args
assert_eq!(
config.build_args.get("NODE_VERSION").map(String::as_str),
Some("20")
);
// build.target
assert_eq!(config.build_target.as_deref(), Some("dev"));
}
#[tokio::test]
async fn resolve_compose_mode() {
let config = DevcontainerResolver::resolve(&fixture_path("compose-mode"))
.await
.unwrap();
// In compose mode, the dockerfile is derived from the compose service's image
assert!(config.dockerfile.contains("FROM node:20"));
assert_eq!(config.workspace_folder, "/workspace");
assert_eq!(config.remote_user.as_deref(), Some("node"));
assert_eq!(config.compose_files.len(), 1);
assert_eq!(config.compose_service.as_deref(), Some("app"));
// Ports come from compose + forwardPorts merged
assert_eq!(config.forwarded_ports, vec![3000, 9229, 5173]);
// Environment merged from compose + remoteEnv
assert_eq!(
config.environment.get("NODE_ENV").map(String::as_str),
Some("development")
);
assert_eq!(
config.environment.get("DEBUG").map(String::as_str),
Some("true")
);
}
#[tokio::test]
async fn resolve_variables() {
let config = DevcontainerResolver::resolve(&fixture_path("variables"))
.await
.unwrap();
assert_eq!(config.workspace_folder, "/workspaces/variables");
assert_eq!(
config.environment.get("PROJECT_ROOT").map(String::as_str),
Some("/workspaces/variables")
);
assert_eq!(
config.environment.get("PROJECT_NAME").map(String::as_str),
Some("variables")
);
}
#[tokio::test]
async fn resolve_compose_multi() {
let config = DevcontainerResolver::resolve(&fixture_path("compose-multi"))
.await
.unwrap();
// Override file wins for image
assert!(config.dockerfile.contains("FROM node:22"));
assert_eq!(config.workspace_folder, "/workspace");
assert_eq!(config.compose_files.len(), 2);
assert_eq!(config.compose_service.as_deref(), Some("app"));
// Port from base.yml
assert_eq!(config.forwarded_ports, vec![3000]);
// Environment from override.yml
assert_eq!(
config.environment.get("OVERRIDE_VAR").map(String::as_str),
Some("true")
);
}
#[tokio::test]
async fn resolve_not_found() {
let result = DevcontainerResolver::resolve(&fixture_path("nonexistent")).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("no devcontainer.json found"));
}
#[tokio::test]
async fn resolve_subdirectory_mode() {
let config = DevcontainerResolver::resolve(&fixture_path("subdirectory-mode"))
.await
.unwrap();
assert!(
config
.dockerfile
.contains("FROM mcr.microsoft.com/devcontainers/python:3.12")
);
assert_eq!(config.remote_user.as_deref(), Some("vscode"));
assert_eq!(config.workspace_folder, "/workspaces/subdirectory-mode");
}
#[tokio::test]
async fn resolve_subdirectory_multiple_picks_alphabetical_first() {
let config = DevcontainerResolver::resolve(&fixture_path("subdirectory-multiple"))
.await
.unwrap();
// "alpha" sorts before "beta", so alpha's config is used
assert!(
config
.dockerfile
.contains("FROM mcr.microsoft.com/devcontainers/base:ubuntu")
);
assert_eq!(config.remote_user.as_deref(), Some("alpha-user"));
}
#[tokio::test]
async fn resolve_subdirectory_standard_wins_over_subdirs() {
let config = DevcontainerResolver::resolve(&fixture_path("subdirectory-with-standard"))
.await
.unwrap();
// Standard .devcontainer/devcontainer.json takes priority over subdirectory
// format
assert!(
config
.dockerfile
.contains("FROM mcr.microsoft.com/devcontainers/base:ubuntu")
);
assert_eq!(config.remote_user.as_deref(), Some("standard-user"));
}
#[tokio::test]
async fn generated_dockerfile_is_well_formed() {
let config = DevcontainerResolver::resolve(&fixture_path("image-only"))
.await
.unwrap();
// Should start with the generated header
assert!(
config
.dockerfile
.contains("# Generated by fabro-devcontainer")
);
// Should have the base image
assert!(config.dockerfile.contains("FROM"));
// Should end with a newline
assert!(config.dockerfile.ends_with('\n'));
}