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