Skip to main content

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        Ok(ArangoGuard(self.0.start().await?))
59    }
60}
61
62impl Default for ArangoContainer {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68/// The running guard for an [`ArangoContainer`].
69pub struct ArangoGuard(ContainerGuard);
70
71impl ArangoGuard {
72    /// The HTTP API endpoint for the running container.
73    pub fn endpoint(&self) -> String {
74        format!(
75            "http://{}:{}",
76            self.0.host(),
77            self.0.get_mapped_port(ArangoContainer::PORT).unwrap()
78        )
79    }
80
81    /// Stops and removes the container, releasing its host port.
82    pub async fn stop(self) -> Result<()> {
83        self.0.stop().await
84    }
85}
86
87impl std::ops::Deref for ArangoGuard {
88    type Target = ContainerGuard;
89    fn deref(&self) -> &ContainerGuard {
90        &self.0
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn with_image_and_root_password_smoke() {
100        let _ = ArangoContainer::new();
101        let _ = ArangoContainer::with_image("arangodb:3.11").with_root_password("s3cret");
102    }
103
104    /// Fix 3 (arango override-path assertion): `with_root_password` must remove
105    /// `ARANGO_NO_AUTH` entirely — not merely blank its value — and add
106    /// `ARANGO_ROOT_PASSWORD` with the given password, in the final builder state.
107    ///
108    /// Mutation-style evidence: reverting `with_root_password` to the old
109    /// `self.0.with_env("ARANGO_ROOT_PASSWORD", password)` (no `remove_env` call)
110    /// makes this test fail — `ARANGO_NO_AUTH` would still be present (`entries`
111    /// would contain `("ARANGO_NO_AUTH", "1")`), which is exactly the bug: the real
112    /// entrypoint checks `ARANGO_NO_AUTH` for presence regardless of
113    /// `ARANGO_ROOT_PASSWORD` (see the module doc), so the old code's password was a
114    /// no-op against a live container.
115    #[test]
116    fn with_root_password_removes_no_auth_and_sets_the_password() {
117        let container = ArangoContainer::with_image("arangodb:3.11").with_root_password("s3cret");
118        let entries = container.0.env();
119
120        assert!(
121            entries.iter().all(|(k, _)| k != "ARANGO_NO_AUTH"),
122            "ARANGO_NO_AUTH must be entirely absent once a root password is set, not just \
123             blanked — the entrypoint checks for presence, not value; found: {entries:?}"
124        );
125        assert!(
126            entries
127                .iter()
128                .any(|(k, v)| k == "ARANGO_ROOT_PASSWORD" && v == "s3cret"),
129            "ARANGO_ROOT_PASSWORD must be set to the given password; found: {entries:?}"
130        );
131    }
132
133    /// The no-auth default path (no `with_root_password` call) must be unaffected:
134    /// `ARANGO_NO_AUTH=1` stays present and no root password entry is added.
135    #[test]
136    fn without_root_password_no_auth_stays_set() {
137        let container = ArangoContainer::with_image("arangodb:3.11");
138        let entries = container.0.env();
139
140        assert!(
141            entries
142                .iter()
143                .any(|(k, v)| k == "ARANGO_NO_AUTH" && v == "1"),
144            "the no-auth default must be untouched when with_root_password is never called; \
145             found: {entries:?}"
146        );
147        assert!(
148            entries.iter().all(|(k, _)| k != "ARANGO_ROOT_PASSWORD"),
149            "no root password should be set by default; found: {entries:?}"
150        );
151    }
152}