use super::{
build_context_tar, build_context_tar_with_inline, glob_match, is_ignored,
map_additional_context, read_dockerignore,
};
use std::fs;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn additional_context_prefix_mapping() {
let base = Path::new("/proj");
assert_eq!(
map_additional_context(base, "docker-image://alpine"),
"image:alpine"
);
assert_eq!(
map_additional_context(base, "https://example.org/ctx.tar"),
"url:https://example.org/ctx.tar"
);
assert_eq!(
map_additional_context(base, "git://example.org/r.git"),
"url:git://example.org/r.git"
);
let local = map_additional_context(base, "sub/dir");
assert!(local.starts_with("localpath:"));
assert!(local.ends_with(&base.join("sub/dir").display().to_string()));
}
#[test]
fn extra_files_are_added_to_context_tar() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\n").unwrap();
let extra = vec![(".podup-build-secret-tok".to_string(), b"hunter2".to_vec())];
let bytes = build_context_tar(dir.path(), "Dockerfile", &extra).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let mut archive = tar::Archive::new(raw.as_slice());
let names: Vec<String> = archive
.entries()
.unwrap()
.filter_map(|e| e.ok())
.filter_map(|e| e.path().ok().map(|p| p.to_string_lossy().into_owned()))
.collect();
assert!(
names.iter().any(|n| n.contains(".podup-build-secret-tok")),
"secret entry must be present: {names:?}"
);
}
#[test]
fn build_secret_entries_excluded_from_copy_via_dockerignore() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\nCOPY . /app\n").unwrap();
fs::write(dir.path().join(".dockerignore"), b"*.log\n").unwrap();
let extra = vec![(".podup-build-secret-tok".to_string(), b"hunter2".to_vec())];
let bytes = build_context_tar(dir.path(), "Dockerfile", &extra).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let mut archive = tar::Archive::new(raw.as_slice());
let mut di_entries = 0;
let mut di = String::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
if entry.path().unwrap().to_string_lossy() == ".dockerignore" {
di_entries += 1;
entry.read_to_string(&mut di).unwrap();
}
}
assert_eq!(di_entries, 1, "exactly one .dockerignore entry");
assert!(di.lines().any(|l| l == "*.log"), "user rule kept: {di:?}");
assert!(
di.lines().any(|l| l == ".podup-build-secret-tok"),
"secret entry must be COPY-excluded via .dockerignore: {di:?}"
);
}
#[test]
fn inline_tar_excludes_build_secrets_via_dockerignore() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("app.txt"), b"x").unwrap();
let extra = vec![(".podup-build-secret-db".to_string(), b"s3cret".to_vec())];
let (bytes, _) = build_context_tar_with_inline(dir.path(), "FROM alpine\n", &extra).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let mut archive = tar::Archive::new(raw.as_slice());
let mut di = String::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
if entry.path().unwrap().to_string_lossy() == ".dockerignore" {
entry.read_to_string(&mut di).unwrap();
}
}
assert!(
di.lines().any(|l| l == ".dockerfile-inline"),
"inline exclusion kept: {di:?}"
);
assert!(
di.lines().any(|l| l == ".podup-build-secret-db"),
"secret entry must be COPY-excluded via .dockerignore: {di:?}"
);
}
#[test]
fn dockerfile_is_force_included_despite_dockerignore() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\n").unwrap();
fs::write(dir.path().join(".dockerignore"), b"*\n").unwrap();
let bytes = build_context_tar(dir.path(), "Dockerfile", &[]).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let names: Vec<String> = tar::Archive::new(raw.as_slice())
.entries()
.unwrap()
.filter_map(|e| e.ok())
.filter_map(|e| e.path().ok().map(|p| p.to_string_lossy().into_owned()))
.collect();
assert!(
names.iter().any(|n| n == "Dockerfile"),
"Dockerfile must survive a `*` .dockerignore: {names:?}"
);
}
#[test]
fn build_ignored_exact() {
let patterns = vec!["secret.txt".to_string()];
assert!(is_ignored("secret.txt", &patterns));
assert!(!is_ignored("secret.txt.bak", &patterns));
}
#[test]
fn build_ignored_dir() {
let patterns = vec!["node_modules/".to_string()];
assert!(is_ignored("node_modules/foo.js", &patterns));
assert!(!is_ignored("other/foo.js", &patterns));
}
#[test]
fn build_ignored_path_separator() {
let patterns = vec!["vendor".to_string()];
assert!(is_ignored("vendor/lib.rs", &patterns));
assert!(!is_ignored("notvendor/lib.rs", &patterns));
}
#[test]
fn build_ignored_glob_extension() {
let patterns = vec!["*.key".to_string()];
assert!(is_ignored("secret.key", &patterns));
assert!(is_ignored("certs/ca.key", &patterns));
assert!(!is_ignored("key.txt", &patterns));
}
#[test]
fn build_ignored_glob_in_subdir() {
let patterns = vec!["logs/*.log".to_string()];
assert!(is_ignored("logs/error.log", &patterns));
assert!(!is_ignored("other/error.log", &patterns));
}
#[test]
fn glob_match_star_extension() {
assert!(glob_match("*.env", "production.env"));
assert!(glob_match("*.env", "config/.env"));
assert!(!glob_match("*.env", "env.txt"));
}
#[test]
fn glob_match_star_prefix() {
assert!(glob_match("id_*", "id_rsa"));
assert!(glob_match("id_*", "id_ed25519"));
assert!(!glob_match("id_*", "not_id_rsa"));
}
#[test]
fn glob_match_double_star_any_depth() {
assert!(glob_match("**/*.key", "secret.key"));
assert!(glob_match("**/*.key", "a/b/c/secret.key"));
assert!(glob_match("a/**/b", "a/b"));
assert!(glob_match("a/**/b", "a/x/y/b"));
assert!(!glob_match("a/**/b", "z/b"));
}
#[test]
fn glob_match_question_mark() {
assert!(glob_match("file?.txt", "file1.txt"));
assert!(!glob_match("file?.txt", "file.txt"));
assert!(!glob_match("file?.txt", "file12.txt"));
}
#[test]
fn dockerignore_negation_reincludes() {
let patterns = vec!["*.log".to_string(), "!keep.log".to_string()];
assert!(is_ignored("error.log", &patterns));
assert!(!is_ignored("keep.log", &patterns));
}
#[test]
fn dockerignore_negation_order_matters() {
let patterns = vec![
"logs/".to_string(),
"!logs/keep/".to_string(),
"logs/keep/secret.txt".to_string(),
];
assert!(is_ignored("logs/a.log", &patterns));
assert!(!is_ignored("logs/keep/b.log", &patterns));
assert!(is_ignored("logs/keep/secret.txt", &patterns));
}
#[test]
fn dockerignore_parsed_correctly() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join(".dockerignore"),
b"# comment\n\ntarget/\n*.log\n",
)
.unwrap();
let patterns = read_dockerignore(dir.path());
assert_eq!(patterns, vec!["target/", "*.log"]);
}
#[test]
fn dockerignore_missing_returns_empty() {
let dir = tempdir().unwrap();
let patterns = read_dockerignore(dir.path());
assert!(patterns.is_empty());
}
#[test]
fn context_tar_produces_valid_gzip() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\n").unwrap();
fs::write(dir.path().join("app.rs"), b"fn main() {}").unwrap();
let bytes = build_context_tar(dir.path(), "Dockerfile", &[]).unwrap();
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
}
#[test]
fn context_tar_excludes_dockerignore_glob() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\n").unwrap();
fs::write(dir.path().join("secret.key"), b"top secret").unwrap();
fs::write(dir.path().join(".dockerignore"), b"*.key\n").unwrap();
let bytes = build_context_tar(dir.path(), "Dockerfile", &[]).unwrap();
let mut gz_content = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut gz_content)
.unwrap();
let mut archive = tar::Archive::new(gz_content.as_slice());
let names: Vec<String> = archive
.entries()
.unwrap()
.filter_map(|e| e.ok())
.filter_map(|e| e.path().ok().map(|p| p.to_string_lossy().into_owned()))
.collect();
assert!(
!names.iter().any(|n| n.contains("secret.key")),
"secret.key must be excluded: {names:?}"
);
}
#[test]
fn context_tar_with_subdirectory() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\n").unwrap();
fs::create_dir(dir.path().join("src")).unwrap();
fs::write(dir.path().join("src/main.rs"), b"fn main() {}").unwrap();
let bytes = build_context_tar(dir.path(), "Dockerfile", &[]).unwrap();
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
}
#[test]
fn inline_tar_produces_valid_gzip() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("app.txt"), b"content").unwrap();
let inline = "FROM alpine\nRUN echo hello\n";
let (bytes, df_name) = build_context_tar_with_inline(dir.path(), inline, &[]).unwrap();
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
assert!(!df_name.is_empty());
}
#[test]
fn inline_dockerfile_excluded_via_dockerignore() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("app.txt"), b"x").unwrap();
let (bytes, _) = build_context_tar_with_inline(dir.path(), "FROM alpine\n", &[]).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let mut archive = tar::Archive::new(raw.as_slice());
let mut di = String::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
if entry.path().unwrap().to_string_lossy() == ".dockerignore" {
entry.read_to_string(&mut di).unwrap();
}
}
assert!(
di.lines().any(|l| l == ".dockerfile-inline"),
"inline Dockerfile must be excluded from COPY via .dockerignore: {di:?}"
);
}
#[test]
fn inline_dockerignore_preserves_user_rules() {
use flate2::read::GzDecoder;
use std::io::Read;
let dir = tempdir().unwrap();
fs::write(dir.path().join("app.txt"), b"x").unwrap();
fs::write(dir.path().join(".dockerignore"), b"*.log\n").unwrap();
let (bytes, _) = build_context_tar_with_inline(dir.path(), "FROM alpine\n", &[]).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let mut archive = tar::Archive::new(raw.as_slice());
let mut di_entries = 0;
let mut di = String::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
if entry.path().unwrap().to_string_lossy() == ".dockerignore" {
di_entries += 1;
entry.read_to_string(&mut di).unwrap();
}
}
assert_eq!(di_entries, 1, "exactly one .dockerignore entry");
assert!(di.lines().any(|l| l == "*.log"), "user rule kept: {di:?}");
assert!(
di.lines().any(|l| l == ".dockerfile-inline"),
"inline exclusion added: {di:?}"
);
}
#[cfg(unix)]
#[test]
fn context_tar_packs_symlink_as_link_not_target() {
use flate2::read::GzDecoder;
use std::io::Read;
let outside = tempdir().unwrap();
fs::write(outside.path().join("secret"), b"TOPSECRET").unwrap();
let dir = tempdir().unwrap();
fs::write(dir.path().join("Dockerfile"), b"FROM alpine\n").unwrap();
std::os::unix::fs::symlink(outside.path().join("secret"), dir.path().join("leak")).unwrap();
let bytes = build_context_tar(dir.path(), "Dockerfile", &[]).unwrap();
let mut raw = Vec::new();
GzDecoder::new(bytes.as_slice())
.read_to_end(&mut raw)
.unwrap();
let mut archive = tar::Archive::new(raw.as_slice());
let mut found = false;
for entry in archive.entries().unwrap() {
let entry = entry.unwrap();
if entry.path().unwrap().to_string_lossy() == "leak" {
found = true;
assert_eq!(
entry.header().entry_type(),
tar::EntryType::Symlink,
"symlink must be packed as a link"
);
assert_eq!(
entry.header().size().unwrap(),
0,
"link entry must not carry the target's bytes"
);
}
}
assert!(found, "symlink entry must be present in the context tar");
}
#[test]
fn build_ignored_empty_pattern_matches_nothing() {
let patterns = vec![String::new()];
assert!(!is_ignored("anything.txt", &patterns));
assert!(!is_ignored("a/b/c", &patterns));
}
#[test]
fn glob_match_double_star_suffix_spans_subtree() {
assert!(glob_match("build/**", "build/out.o"));
assert!(glob_match("build/**", "build/a/b/out.o"));
assert!(!glob_match("build/**", "src/out.o"));
}
#[test]
fn glob_match_double_star_middle_with_no_match_fails() {
assert!(glob_match("a/**/z", "a/b/c/z"));
assert!(!glob_match("a/**/z", "a/b/c/y"));
}
#[test]
fn glob_match_question_mark_matches_single_non_slash_char() {
assert!(glob_match("file?.txt", "file1.txt"));
assert!(!glob_match("file?.txt", "file.txt"));
assert!(!glob_match("a?b", "a/b"));
}