Initial
This commit is contained in:
commit
3f60b72c3b
48 changed files with 6599 additions and 0 deletions
509
tests/test_compose_operations.rs
Normal file
509
tests/test_compose_operations.rs
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
use docker_compose_updater::compose::updater::ComposeUpdater;
|
||||
use docker_compose_updater::config::{Config, RegistryConfig, UpdateStrategy};
|
||||
use docker_compose_updater::registry::ImageRef;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_image_ref_comprehensive_parsing() {
|
||||
// Test comprehensive image reference parsing scenarios
|
||||
let test_cases = vec![
|
||||
// (input, expected_registry, expected_namespace, expected_name, expected_tag)
|
||||
|
||||
// Standard Docker Hub images
|
||||
("nginx", "docker.io", None, "nginx", "latest"),
|
||||
("nginx:1.21", "docker.io", None, "nginx", "1.21"),
|
||||
("nginx:latest", "docker.io", None, "nginx", "latest"),
|
||||
// Docker Hub with namespace
|
||||
(
|
||||
"library/nginx:1.21",
|
||||
"docker.io",
|
||||
Some("library"),
|
||||
"nginx",
|
||||
"1.21",
|
||||
),
|
||||
(
|
||||
"bitnami/nginx:1.21",
|
||||
"docker.io",
|
||||
Some("bitnami"),
|
||||
"nginx",
|
||||
"1.21",
|
||||
),
|
||||
// Custom registries
|
||||
("quay.io/nginx:1.21", "quay.io", None, "nginx", "1.21"),
|
||||
(
|
||||
"gcr.io/project/nginx:1.21",
|
||||
"gcr.io",
|
||||
Some("project"),
|
||||
"nginx",
|
||||
"1.21",
|
||||
),
|
||||
(
|
||||
"registry.k8s.io/pause:3.9",
|
||||
"registry.k8s.io",
|
||||
None,
|
||||
"pause",
|
||||
"3.9",
|
||||
),
|
||||
// GitHub Container Registry
|
||||
(
|
||||
"ghcr.io/user/app:v1.0.0",
|
||||
"ghcr.io",
|
||||
Some("user"),
|
||||
"app",
|
||||
"v1.0.0",
|
||||
),
|
||||
(
|
||||
"ghcr.io/org/project:latest",
|
||||
"ghcr.io",
|
||||
Some("org"),
|
||||
"project",
|
||||
"latest",
|
||||
),
|
||||
// Local registries with ports
|
||||
(
|
||||
"localhost:5000/app:dev",
|
||||
"localhost:5000",
|
||||
None,
|
||||
"app",
|
||||
"dev",
|
||||
),
|
||||
(
|
||||
"127.0.0.1:8080/ns/app:test",
|
||||
"127.0.0.1:8080",
|
||||
Some("ns"),
|
||||
"app",
|
||||
"test",
|
||||
),
|
||||
// Complex tags with versions and suffixes
|
||||
(
|
||||
"nginx:1.21.6-alpine",
|
||||
"docker.io",
|
||||
None,
|
||||
"nginx",
|
||||
"1.21.6-alpine",
|
||||
),
|
||||
(
|
||||
"postgres:13.7-bullseye",
|
||||
"docker.io",
|
||||
None,
|
||||
"postgres",
|
||||
"13.7-bullseye",
|
||||
),
|
||||
(
|
||||
"redis:7.0.0-alpine3.16",
|
||||
"docker.io",
|
||||
None,
|
||||
"redis",
|
||||
"7.0.0-alpine3.16",
|
||||
),
|
||||
// Enterprise/custom registries
|
||||
(
|
||||
"my-registry.company.com/team/app:v2.1.3",
|
||||
"my-registry.company.com",
|
||||
Some("team"),
|
||||
"app",
|
||||
"v2.1.3",
|
||||
),
|
||||
(
|
||||
"harbor.example.org/project/nginx:stable",
|
||||
"harbor.example.org",
|
||||
Some("project"),
|
||||
"nginx",
|
||||
"stable",
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected_registry, expected_namespace, expected_name, expected_tag) in test_cases {
|
||||
let result = ImageRef::parse(input);
|
||||
assert!(result.is_ok(), "Failed to parse: {input}");
|
||||
|
||||
let image_ref = result.unwrap();
|
||||
assert_eq!(
|
||||
image_ref.registry, expected_registry,
|
||||
"Registry mismatch for: {input}"
|
||||
);
|
||||
assert_eq!(
|
||||
image_ref.namespace,
|
||||
expected_namespace.map(String::from),
|
||||
"Namespace mismatch for: {input}"
|
||||
);
|
||||
assert_eq!(image_ref.name, expected_name, "Name mismatch for: {input}");
|
||||
assert_eq!(image_ref.tag, expected_tag, "Tag mismatch for: {input}");
|
||||
|
||||
// Test that the image ref can be converted back to string representation
|
||||
let display_string = image_ref.to_string();
|
||||
assert!(
|
||||
!display_string.is_empty(),
|
||||
"Display string should not be empty for: {input}"
|
||||
);
|
||||
|
||||
// For Docker Hub images, the display should omit the registry
|
||||
if expected_registry == "docker.io" {
|
||||
assert!(
|
||||
!display_string.starts_with("docker.io"),
|
||||
"Docker Hub images should not show registry in display: {input}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compose_file_parsing_edge_cases() {
|
||||
let config = create_test_config();
|
||||
let updater = ComposeUpdater::new(config);
|
||||
|
||||
// Test various compose file formats and edge cases
|
||||
let test_cases = vec![
|
||||
// Standard compose file
|
||||
(
|
||||
"standard_compose",
|
||||
r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.21.0
|
||||
ports:
|
||||
- "80:80"
|
||||
db:
|
||||
image: postgres:13.7
|
||||
"#,
|
||||
2, // Expected number of services
|
||||
),
|
||||
// Compose file with comments and whitespace
|
||||
(
|
||||
"with_comments",
|
||||
r#"
|
||||
# This is a comment
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Web service comment
|
||||
web:
|
||||
image: nginx:1.21.0 # Inline comment
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
# Database service
|
||||
db:
|
||||
image: postgres:13.7
|
||||
# More comments
|
||||
environment:
|
||||
POSTGRES_DB: test
|
||||
"#,
|
||||
2,
|
||||
),
|
||||
// Compose file with complex image references
|
||||
(
|
||||
"complex_images",
|
||||
r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/company/frontend:v2.1.0
|
||||
backend:
|
||||
image: my-registry.com:5000/team/backend:latest
|
||||
cache:
|
||||
image: redis:7.0.0-alpine3.16
|
||||
database:
|
||||
image: postgres:14.5-bullseye
|
||||
"#,
|
||||
4,
|
||||
),
|
||||
// Minimal compose file
|
||||
(
|
||||
"minimal",
|
||||
r#"
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
image: hello-world
|
||||
"#,
|
||||
1,
|
||||
),
|
||||
// Compose file with extends and other features
|
||||
(
|
||||
"with_extends",
|
||||
r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
base:
|
||||
image: ubuntu:20.04
|
||||
environment:
|
||||
- ENV=production
|
||||
|
||||
app:
|
||||
extends:
|
||||
service: base
|
||||
image: myapp:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
"#,
|
||||
2,
|
||||
),
|
||||
];
|
||||
|
||||
for (test_name, compose_content, expected_services) in test_cases {
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(compose_content.as_bytes()).unwrap();
|
||||
|
||||
let result = updater.parse_compose_file(temp_file.path().to_str().unwrap());
|
||||
assert!(result.is_ok(), "Failed to parse compose file: {test_name}");
|
||||
|
||||
let compose_file = result.unwrap();
|
||||
assert_eq!(
|
||||
compose_file.services.len(),
|
||||
expected_services,
|
||||
"Service count mismatch for: {test_name}"
|
||||
);
|
||||
|
||||
// Verify that all services have valid image references
|
||||
for service in &compose_file.services {
|
||||
assert!(
|
||||
!service.service_name.is_empty(),
|
||||
"Service name should not be empty in: {test_name}"
|
||||
);
|
||||
assert!(
|
||||
!service.image_ref.name.is_empty(),
|
||||
"Image name should not be empty in: {test_name}"
|
||||
);
|
||||
assert!(
|
||||
!service.image_ref.tag.is_empty(),
|
||||
"Image tag should not be empty in: {test_name}"
|
||||
);
|
||||
assert!(
|
||||
!service.image_ref.registry.is_empty(),
|
||||
"Image registry should not be empty in: {test_name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compose_content_replacement() {
|
||||
let config = create_test_config();
|
||||
let updater = ComposeUpdater::new(config);
|
||||
|
||||
let original_content = r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.20.0 # Current version
|
||||
ports:
|
||||
- "80:80"
|
||||
db:
|
||||
image: postgres:13.6
|
||||
"#;
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(original_content.as_bytes()).unwrap();
|
||||
|
||||
let compose_file = updater
|
||||
.parse_compose_file(temp_file.path().to_str().unwrap())
|
||||
.unwrap();
|
||||
|
||||
// Test replacing nginx image
|
||||
let nginx_service = compose_file
|
||||
.services
|
||||
.iter()
|
||||
.find(|s| s.service_name == "web")
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
updater.replace_image_in_content(&compose_file.content, nginx_service, "nginx:1.21.0");
|
||||
assert!(result.is_ok(), "Should successfully replace image");
|
||||
|
||||
let new_content = result.unwrap();
|
||||
assert!(
|
||||
new_content.contains("nginx:1.21.0"),
|
||||
"Should contain new image version"
|
||||
);
|
||||
assert!(
|
||||
!new_content.contains("nginx:1.20.0"),
|
||||
"Should not contain old image version"
|
||||
);
|
||||
assert!(
|
||||
new_content.contains("# Current version"),
|
||||
"Should preserve comments"
|
||||
);
|
||||
assert!(
|
||||
new_content.contains("postgres:13.6"),
|
||||
"Should preserve other services unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_service_image_replacement_edge_cases() {
|
||||
let config = create_test_config();
|
||||
let updater = ComposeUpdater::new(config);
|
||||
|
||||
// Test cases with tricky image replacement scenarios
|
||||
let test_cases = vec![
|
||||
// Multiple occurrences of similar image names
|
||||
(
|
||||
r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
nginx-proxy:
|
||||
image: nginx:1.20.0
|
||||
nginx-cache:
|
||||
image: nginx:1.21.0
|
||||
web:
|
||||
image: nginx:1.20.0
|
||||
"#,
|
||||
"nginx-proxy",
|
||||
"nginx:1.20.0",
|
||||
"nginx:1.22.0",
|
||||
),
|
||||
// Images with similar names but different registries
|
||||
(
|
||||
r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
public-nginx:
|
||||
image: nginx:1.20.0
|
||||
private-nginx:
|
||||
image: my-registry.com/nginx:1.20.0
|
||||
"#,
|
||||
"public-nginx",
|
||||
"nginx:1.20.0",
|
||||
"nginx:1.21.0",
|
||||
),
|
||||
// Complex image with registry and namespace
|
||||
(
|
||||
r#"
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/company/app:v1.0.0
|
||||
environment:
|
||||
- IMAGE_VERSION=v1.0.0
|
||||
"#,
|
||||
"app",
|
||||
"ghcr.io/company/app:v1.0.0",
|
||||
"ghcr.io/company/app:v1.1.0",
|
||||
),
|
||||
];
|
||||
|
||||
for (compose_content, service_name, old_image, new_image) in test_cases {
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(compose_content.as_bytes()).unwrap();
|
||||
|
||||
let compose_file = updater
|
||||
.parse_compose_file(temp_file.path().to_str().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let target_service = compose_file
|
||||
.services
|
||||
.iter()
|
||||
.find(|s| s.service_name == service_name)
|
||||
.unwrap();
|
||||
|
||||
let result =
|
||||
updater.replace_image_in_content(&compose_file.content, target_service, new_image);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Should replace image for service: {service_name}"
|
||||
);
|
||||
|
||||
let new_content = result.unwrap();
|
||||
assert!(
|
||||
new_content.contains(new_image),
|
||||
"Should contain new image: {new_image}"
|
||||
);
|
||||
|
||||
// Count occurrences to ensure only the right image was replaced
|
||||
let old_count = compose_content.matches(old_image).count();
|
||||
let new_old_count = new_content.matches(old_image).count();
|
||||
assert_eq!(
|
||||
new_old_count,
|
||||
old_count - 1,
|
||||
"Should replace exactly one occurrence for: {service_name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_ignore_patterns_comprehensive() {
|
||||
// Test comprehensive ignore pattern scenarios
|
||||
let test_cases = vec![
|
||||
// Basic pattern matching
|
||||
(vec!["localhost"], "localhost:5000/app:latest", true),
|
||||
(vec!["localhost"], "nginx:latest", false),
|
||||
// Multiple patterns
|
||||
(
|
||||
vec!["localhost", "127.0.0.1"],
|
||||
"127.0.0.1:5000/app:latest",
|
||||
true,
|
||||
),
|
||||
(
|
||||
vec!["localhost", "127.0.0.1"],
|
||||
"192.168.1.1:5000/app:latest",
|
||||
false,
|
||||
),
|
||||
// Registry-based patterns
|
||||
(vec!["ghcr.io"], "ghcr.io/user/app:latest", true),
|
||||
(vec!["ghcr.io"], "docker.io/user/app:latest", false),
|
||||
// Prefix patterns
|
||||
(vec!["test-"], "test-app:latest", true),
|
||||
(vec!["test-"], "app-test:latest", false),
|
||||
(vec!["test-"], "testing:latest", false),
|
||||
// Complex registry patterns
|
||||
(
|
||||
vec!["internal.company.com"],
|
||||
"internal.company.com/team/app:v1.0",
|
||||
true,
|
||||
),
|
||||
(
|
||||
vec!["internal.company.com"],
|
||||
"external.company.com/team/app:v1.0",
|
||||
false,
|
||||
),
|
||||
// Partial matches
|
||||
(vec!["redis"], "redis:latest", true),
|
||||
(vec!["redis"], "valkey:latest", false),
|
||||
(vec!["redis"], "my-redis:latest", true), // This should match as substring
|
||||
];
|
||||
|
||||
for (ignore_patterns, image, should_be_ignored) in test_cases {
|
||||
let config = Config {
|
||||
ignore_images: ignore_patterns.iter().map(|s| s.to_string()).collect(),
|
||||
..create_test_config()
|
||||
};
|
||||
|
||||
let result = config.is_image_ignored(image);
|
||||
assert_eq!(
|
||||
result, should_be_ignored,
|
||||
"Pattern {ignore_patterns:?} with image '{image}' should be ignored: {should_be_ignored}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_config() -> Config {
|
||||
let mut registries = HashMap::new();
|
||||
registries.insert(
|
||||
"docker.io".to_string(),
|
||||
RegistryConfig {
|
||||
url: "https://registry-1.docker.io".to_string(),
|
||||
auth_token: None,
|
||||
},
|
||||
);
|
||||
registries.insert(
|
||||
"ghcr.io".to_string(),
|
||||
RegistryConfig {
|
||||
url: "https://ghcr.io".to_string(),
|
||||
auth_token: None,
|
||||
},
|
||||
);
|
||||
|
||||
Config {
|
||||
compose_paths: vec![PathBuf::from("./test")],
|
||||
schedule: "0 0 2 * * *".to_string(),
|
||||
registries,
|
||||
update_strategy: UpdateStrategy::Latest,
|
||||
ignore_images: vec![],
|
||||
dry_run: true,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue