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        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}