1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! A single-node ArangoDB container. Auth is disabled by default; see
//! [`ArangoContainer::with_root_password`] to enable it.
use rightsize::{Container, ContainerGuard, Result, Wait};
/// A single-node ArangoDB container.
pub struct ArangoContainer(Container);
impl ArangoContainer {
/// The guest port ArangoDB's HTTP API listens on.
const PORT: u16 = 8529;
/// Builds a container from the pinned default image (`arangodb:3.11`), with auth
/// disabled (`ARANGO_NO_AUTH=1`).
pub fn new() -> Self {
Self::with_image("arangodb:3.11")
}
/// Builds a container from a caller-chosen image, with auth disabled.
pub fn with_image(image: &str) -> Self {
Self(
Container::new(image)
.with_exposed_ports(&[Self::PORT])
.with_env("ARANGO_NO_AUTH", "1")
.waiting_for(
Wait::for_http("/_api/version")
.for_port(Self::PORT)
.for_status_code(200),
),
)
}
/// Enables auth with the given root password, instead of the default no-auth
/// setup. `ARANGO_NO_AUTH` was already set by `new`/`with_image`; this call
/// removes it before adding `ARANGO_ROOT_PASSWORD`.
///
/// This removal is required, not cosmetic: the official ArangoDB entrypoint
/// checks `ARANGO_NO_AUTH` for mere *presence* (`if [ ! -z "$ARANGO_NO_AUTH" ];
/// then AUTHENTICATION="false"; fi`), unconditionally, near the very end of the
/// script, right before it execs `arangod --server.authentication=$AUTHENTICATION`
/// — this check does not care whether `ARANGO_ROOT_PASSWORD` is also set.
/// Verified directly against the real `arangodb:3.11` entrypoint (`docker run
/// --entrypoint /bin/cat arangodb:3.11 /entrypoint.sh`): with both vars set, the
/// root password gets initialized (the `+x`-guarded init block cares only that
/// `ARANGO_ROOT_PASSWORD` is set), but `AUTHENTICATION` still ends up `"false"`
/// because `ARANGO_NO_AUTH` is still present — auth stays off regardless of the
/// password, so leaving `ARANGO_NO_AUTH` in the spec made the password a no-op.
pub fn with_root_password(mut self, password: &str) -> Self {
self.0 = self
.0
.remove_env("ARANGO_NO_AUTH")
.with_env("ARANGO_ROOT_PASSWORD", password);
self
}
/// Boots the container.
pub async fn start(self) -> Result<ArangoGuard> {
crate::register_default_backends();
Ok(ArangoGuard(self.0.start().await?))
}
}
impl Default for ArangoContainer {
fn default() -> Self {
Self::new()
}
}
/// The running guard for an [`ArangoContainer`].
pub struct ArangoGuard(ContainerGuard);
impl ArangoGuard {
/// The HTTP API endpoint for the running container.
pub fn endpoint(&self) -> String {
format!(
"http://{}:{}",
self.0.host(),
self.0.get_mapped_port(ArangoContainer::PORT).unwrap()
)
}
/// Stops and removes the container, releasing its host port.
pub async fn stop(self) -> Result<()> {
self.0.stop().await
}
}
impl std::ops::Deref for ArangoGuard {
type Target = ContainerGuard;
fn deref(&self) -> &ContainerGuard {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_image_and_root_password_smoke() {
let _ = ArangoContainer::new();
let _ = ArangoContainer::with_image("arangodb:3.11").with_root_password("s3cret");
}
/// Fix 3 (arango override-path assertion): `with_root_password` must remove
/// `ARANGO_NO_AUTH` entirely — not merely blank its value — and add
/// `ARANGO_ROOT_PASSWORD` with the given password, in the final builder state.
///
/// Mutation-style evidence: reverting `with_root_password` to the old
/// `self.0.with_env("ARANGO_ROOT_PASSWORD", password)` (no `remove_env` call)
/// makes this test fail — `ARANGO_NO_AUTH` would still be present (`entries`
/// would contain `("ARANGO_NO_AUTH", "1")`), which is exactly the bug: the real
/// entrypoint checks `ARANGO_NO_AUTH` for presence regardless of
/// `ARANGO_ROOT_PASSWORD` (see the module doc), so the old code's password was a
/// no-op against a live container.
#[test]
fn with_root_password_removes_no_auth_and_sets_the_password() {
let container = ArangoContainer::with_image("arangodb:3.11").with_root_password("s3cret");
let entries = container.0.env();
assert!(
entries.iter().all(|(k, _)| k != "ARANGO_NO_AUTH"),
"ARANGO_NO_AUTH must be entirely absent once a root password is set, not just \
blanked — the entrypoint checks for presence, not value; found: {entries:?}"
);
assert!(
entries
.iter()
.any(|(k, v)| k == "ARANGO_ROOT_PASSWORD" && v == "s3cret"),
"ARANGO_ROOT_PASSWORD must be set to the given password; found: {entries:?}"
);
}
/// The no-auth default path (no `with_root_password` call) must be unaffected:
/// `ARANGO_NO_AUTH=1` stays present and no root password entry is added.
#[test]
fn without_root_password_no_auth_stays_set() {
let container = ArangoContainer::with_image("arangodb:3.11");
let entries = container.0.env();
assert!(
entries
.iter()
.any(|(k, v)| k == "ARANGO_NO_AUTH" && v == "1"),
"the no-auth default must be untouched when with_root_password is never called; \
found: {entries:?}"
);
assert!(
entries.iter().all(|(k, _)| k != "ARANGO_ROOT_PASSWORD"),
"no root password should be set by default; found: {entries:?}"
);
}
}