Rust implementation of OCI Distribution Spec with granular access control
1// Docker client integration tests
2// These tests require Docker to be installed and running
3// Enabled with --features docker-tests
4
5#![cfg(feature = "docker-tests")]
6
7mod common;
8
9use common::*;
10use serial_test::serial;
11use std::process::Command;
12
13fn docker_available() -> bool {
14 Command::new("docker")
15 .arg("version")
16 .output()
17 .map(|o| o.status.success())
18 .unwrap_or(false)
19}
20
21fn docker_login(registry: &str, username: &str, password: &str) -> bool {
22 let output = Command::new("docker")
23 .args(["login", registry, "-u", username, "-p", password])
24 .output()
25 .expect("Failed to run docker login");
26
27 output.status.success()
28}
29
30fn docker_logout(registry: &str) {
31 let _ = Command::new("docker").args(["logout", registry]).output();
32}
33
34fn docker_tag(source: &str, target: &str) -> bool {
35 let output = Command::new("docker")
36 .args(["tag", source, target])
37 .output()
38 .expect("Failed to run docker tag");
39
40 output.status.success()
41}
42
43fn docker_push(image: &str) -> bool {
44 let output = Command::new("docker")
45 .args(["push", image])
46 .output()
47 .expect("Failed to run docker push");
48
49 output.status.success()
50}
51
52fn docker_pull(image: &str) -> bool {
53 let output = Command::new("docker")
54 .args(["pull", image])
55 .output()
56 .expect("Failed to run docker pull");
57
58 output.status.success()
59}
60
61fn docker_rmi(image: &str) {
62 let _ = Command::new("docker").args(["rmi", "-f", image]).output();
63}
64
65#[test]
66#[serial]
67fn test_docker_login_valid_credentials() {
68 if !docker_available() {
69 println!("Docker not available, skipping test");
70 return;
71 }
72
73 let mut server = TestServer::new();
74 server.start();
75
76 let registry = format!("127.0.0.1:{}", server.port);
77
78 assert!(docker_login(®istry, "admin", "admin"));
79
80 docker_logout(®istry);
81}
82
83#[test]
84#[serial]
85fn test_docker_login_invalid_credentials() {
86 if !docker_available() {
87 println!("Docker not available, skipping test");
88 return;
89 }
90
91 let mut server = TestServer::new();
92 server.start();
93
94 let registry = format!("127.0.0.1:{}", server.port);
95
96 assert!(!docker_login(®istry, "invalid", "invalid"));
97}
98
99#[test]
100#[serial]
101fn test_docker_push_pull_image() {
102 if !docker_available() {
103 println!("Docker not available, skipping test");
104 return;
105 }
106
107 let mut server = TestServer::new();
108 server.start();
109
110 let registry = format!("127.0.0.1:{}", server.port);
111 let image_name = format!("{}/test/alpine:latest", registry);
112
113 // Login
114 assert!(docker_login(®istry, "admin", "admin"));
115
116 // Pull a small base image
117 assert!(docker_pull("alpine:latest"));
118
119 // Tag for local registry
120 assert!(docker_tag("alpine:latest", &image_name));
121
122 // Push to local registry
123 assert!(docker_push(&image_name));
124
125 // Remove local images
126 docker_rmi(&image_name);
127 docker_rmi("alpine:latest");
128
129 // Pull from local registry
130 assert!(docker_pull(&image_name));
131
132 // Cleanup
133 docker_rmi(&image_name);
134 docker_logout(®istry);
135}
136
137#[test]
138#[serial]
139fn test_docker_push_requires_authentication() {
140 if !docker_available() {
141 println!("Docker not available, skipping test");
142 return;
143 }
144
145 let mut server = TestServer::new();
146 server.start();
147
148 let registry = format!("127.0.0.1:{}", server.port);
149 let image_name = format!("{}/test/alpine:latest", registry);
150
151 // Don't login - push should fail
152 docker_pull("alpine:latest");
153 docker_tag("alpine:latest", &image_name);
154
155 assert!(!docker_push(&image_name));
156
157 // Cleanup
158 docker_rmi(&image_name);
159 docker_rmi("alpine:latest");
160}
161
162#[test]
163#[serial]
164fn test_docker_push_with_limited_permissions() {
165 if !docker_available() {
166 println!("Docker not available, skipping test");
167 return;
168 }
169
170 let mut server = TestServer::new();
171 server.start();
172
173 let registry = format!("127.0.0.1:{}", server.port);
174 let allowed_image = format!("{}/test/allowed:latest", registry);
175 let forbidden_image = format!("{}/forbidden/denied:latest", registry);
176
177 // Login as writer (has access to test/*)
178 assert!(docker_login(®istry, "writer", "writer"));
179
180 // Pull base image
181 assert!(docker_pull("alpine:latest"));
182
183 // Push to allowed repo should succeed
184 docker_tag("alpine:latest", &allowed_image);
185 assert!(docker_push(&allowed_image));
186
187 // Push to forbidden repo should fail
188 docker_tag("alpine:latest", &forbidden_image);
189 assert!(!docker_push(&forbidden_image));
190
191 // Cleanup
192 docker_rmi(&allowed_image);
193 docker_rmi(&forbidden_image);
194 docker_rmi("alpine:latest");
195 docker_logout(®istry);
196}
197
198#[test]
199#[serial]
200fn test_docker_multi_layer_image() {
201 if !docker_available() {
202 println!("Docker not available, skipping test");
203 return;
204 }
205
206 let mut server = TestServer::new();
207 server.start();
208
209 let registry = format!("127.0.0.1:{}", server.port);
210 let image_name = format!("{}/test/nginx:latest", registry);
211
212 // Login
213 assert!(docker_login(®istry, "admin", "admin"));
214
215 // Pull multi-layer image
216 assert!(docker_pull("nginx:alpine"));
217
218 // Tag for local registry
219 assert!(docker_tag("nginx:alpine", &image_name));
220
221 // Push to local registry
222 assert!(docker_push(&image_name));
223
224 // Remove local copy
225 docker_rmi(&image_name);
226 docker_rmi("nginx:alpine");
227
228 // Pull back from local registry
229 assert!(docker_pull(&image_name));
230
231 // Cleanup
232 docker_rmi(&image_name);
233 docker_logout(®istry);
234}
235
236#[test]
237#[serial]
238fn test_docker_manifest_inspect() {
239 if !docker_available() {
240 println!("Docker not available, skipping test");
241 return;
242 }
243
244 let mut server = TestServer::new();
245 server.start();
246
247 let registry = format!("127.0.0.1:{}", server.port);
248 let image_name = format!("{}/test/alpine:latest", registry);
249
250 // Login and push image
251 assert!(docker_login(®istry, "admin", "admin"));
252 assert!(docker_pull("alpine:latest"));
253 assert!(docker_tag("alpine:latest", &image_name));
254 assert!(docker_push(&image_name));
255
256 // Inspect manifest
257 let output = Command::new("docker")
258 .args(["manifest", "inspect", &image_name])
259 .output()
260 .expect("Failed to run docker manifest inspect");
261
262 assert!(output.status.success());
263 let manifest_json = String::from_utf8_lossy(&output.stdout);
264 assert!(manifest_json.contains("schemaVersion"));
265
266 // Cleanup
267 docker_rmi(&image_name);
268 docker_rmi("alpine:latest");
269 docker_logout(®istry);
270}
271
272#[test]
273#[serial]
274fn test_docker_concurrent_operations() {
275 if !docker_available() {
276 println!("Docker not available, skipping test");
277 return;
278 }
279
280 let mut server = TestServer::new();
281 server.start();
282
283 let registry = format!("127.0.0.1:{}", server.port);
284
285 // Login
286 assert!(docker_login(®istry, "admin", "admin"));
287
288 // Pull base images
289 assert!(docker_pull("alpine:latest"));
290 assert!(docker_pull("busybox:latest"));
291
292 // Tag both for local registry
293 let alpine_name = format!("{}/test/alpine:latest", registry);
294 let busybox_name = format!("{}/test/busybox:latest", registry);
295
296 assert!(docker_tag("alpine:latest", &alpine_name));
297 assert!(docker_tag("busybox:latest", &busybox_name));
298
299 // Push both (simulates concurrent operations)
300 assert!(docker_push(&alpine_name));
301 assert!(docker_push(&busybox_name));
302
303 // Verify both can be pulled
304 docker_rmi(&alpine_name);
305 docker_rmi(&busybox_name);
306
307 assert!(docker_pull(&alpine_name));
308 assert!(docker_pull(&busybox_name));
309
310 // Cleanup
311 docker_rmi(&alpine_name);
312 docker_rmi(&busybox_name);
313 docker_rmi("alpine:latest");
314 docker_rmi("busybox:latest");
315 docker_logout(®istry);
316}
317
318#[test]
319#[serial]
320fn test_docker_reader_can_pull_only() {
321 if !docker_available() {
322 println!("Docker not available, skipping test");
323 return;
324 }
325
326 let mut server = TestServer::new();
327 server.start();
328
329 let registry = format!("127.0.0.1:{}", server.port);
330 let image_name = format!("{}/test/alpine:latest", registry);
331
332 // First push as admin
333 assert!(docker_login(®istry, "admin", "admin"));
334 assert!(docker_pull("alpine:latest"));
335 assert!(docker_tag("alpine:latest", &image_name));
336 assert!(docker_push(&image_name));
337 docker_logout(®istry);
338
339 // Login as reader
340 assert!(docker_login(®istry, "reader", "reader"));
341
342 // Should be able to pull
343 docker_rmi(&image_name);
344 assert!(docker_pull(&image_name));
345
346 // Should NOT be able to push
347 let new_tag = format!("{}/test/alpine:newtag", registry);
348 docker_tag(&image_name, &new_tag);
349 assert!(!docker_push(&new_tag));
350
351 // Cleanup
352 docker_rmi(&image_name);
353 docker_rmi(&new_tag);
354 docker_rmi("alpine:latest");
355 docker_logout(®istry);
356}