testcontainers_modules/dex/
mod.rs1mod config;
2
3use std::borrow::Cow;
4
5pub use config::{PrivateClient, User};
6use testcontainers::{
7 core::{
8 error::Result, wait::HttpWaitStrategy, ContainerPort, ContainerState, ExecCommand, WaitFor,
9 },
10 Image,
11};
12
13use crate::dex::config::OAuth2;
14
15const NAME: &str = "dexidp/dex";
16const TAG: &str = "v2.41.1";
17const HTTP_PORT: ContainerPort = ContainerPort::Tcp(5556);
18
19const CONFIG_FILE: &str = "/etc/dex/config.docker.json";
20
21pub struct Dex {
45 tag: String,
46 clients: Vec<PrivateClient>,
47 users: Vec<User>,
48 allow_password_grants: bool,
49}
50
51impl Default for Dex {
52 fn default() -> Self {
53 Self {
54 tag: TAG.to_string(),
55 clients: vec![],
56 users: vec![],
57 allow_password_grants: false,
58 }
59 }
60}
61
62impl Dex {
63 pub fn with_tag(self, tag: String) -> Self {
66 Self { tag, ..self }
67 }
68
69 pub fn with_simple_user(self) -> Self {
77 self.with_user(User::simple_user())
78 }
79
80 pub fn with_user(self, user: User) -> Self {
84 Self {
85 users: self.users.into_iter().chain(vec![user]).collect(),
86 ..self
87 }
88 }
89
90 pub fn with_simple_client(self) -> Self {
97 self.with_client(PrivateClient::simple_client())
98 }
99
100 pub fn with_client(self, client: PrivateClient) -> Self {
103 Self {
104 clients: self.clients.into_iter().chain(vec![client]).collect(),
105 ..self
106 }
107 }
108
109 pub fn with_allow_password_grants(self) -> Self {
111 Self {
112 allow_password_grants: true,
113 ..self
114 }
115 }
116}
117
118impl Dex {
119 fn generate_config(&self, host: &str, host_port: u16) -> ExecCommand {
120 let config = config::Config {
121 issuer: format!("http://{host}:{host_port}"),
122 enable_password_db: true,
123 storage: config::Storage::sqlite(),
124 web: config::Web::http(),
125 static_clients: self.clients.clone(),
126 static_passwords: self.users.clone(),
127 oauth2: if !self.allow_password_grants {
128 None
129 } else {
130 Some(OAuth2::allow_password_grant())
131 },
132 };
133
134 let config = serde_json::to_string(&config)
135 .expect("Parsing should only fail if structs were defined incorrectly.");
136
137 ExecCommand::new(vec![
138 "/bin/sh",
139 "-c",
140 &format!("echo '{config}' > {CONFIG_FILE}"),
141 ])
142 }
143}
144
145impl Image for Dex {
146 fn name(&self) -> &str {
147 NAME
148 }
149
150 fn tag(&self) -> &str {
151 &self.tag
152 }
153
154 fn ready_conditions(&self) -> Vec<WaitFor> {
155 vec![WaitFor::http(
156 HttpWaitStrategy::new("/.well-known/openid-configuration")
157 .with_port(HTTP_PORT)
158 .with_expected_status_code(200u16),
159 )]
160 }
161
162 fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
163 let command = format!(
166 r#"while [[ ! -f {CONFIG_FILE} ]]; do sleep 1; echo "Waiting for configuration file..."; done;
167 dex serve {CONFIG_FILE}"#,
168 );
169 vec![String::from("/bin/sh"), String::from("-c"), command]
170 }
171
172 fn expose_ports(&self) -> &[ContainerPort] {
173 &[HTTP_PORT]
174 }
175
176 fn exec_before_ready(&self, cs: ContainerState) -> Result<Vec<ExecCommand>> {
177 let host = cs.host();
178 let port = cs.host_port_ipv4(HTTP_PORT)?;
179 Ok(vec![self.generate_config(&host.to_string(), port)])
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::Dex;
186
187 #[tokio::test]
188 async fn starts_with_async_runner() {
189 use testcontainers::runners::AsyncRunner;
190 Dex::default().with_simple_user().start().await.unwrap();
191 }
192
193 #[test]
194 fn starts_with_sync_runner() {
195 use testcontainers::runners::SyncRunner;
196 Dex::default().with_simple_user().start().unwrap();
197 }
198
199 #[tokio::test]
200 async fn starts_without_users_and_client() {
201 use testcontainers::runners::AsyncRunner;
202 Dex::default().start().await.unwrap();
203 }
204
205 #[tokio::test]
206 async fn can_authenticate() {
207 use testcontainers::runners::AsyncRunner;
208 let dex = Dex::default()
209 .with_simple_user()
210 .with_simple_client()
211 .with_allow_password_grants()
212 .start()
213 .await
214 .unwrap();
215 let request = reqwest::Client::new();
216 let url = format!(
217 "http://{}:{}/token",
218 dex.get_host().await.unwrap(),
219 dex.get_host_port_ipv4(5556).await.unwrap()
220 );
221 let token = request
222 .post(url)
223 .header("Authorization", "Basic Y2xpZW50OnNlY3JldA==")
224 .form(&[
225 ("grant_type", "password"),
226 ("scope", "openid"),
227 ("username", "user@example.org"),
228 ("password", "user"),
229 ])
230 .send()
231 .await
232 .unwrap();
233 assert!(token.status().is_success());
234 assert!(token
235 .text()
236 .await
237 .unwrap()
238 .starts_with("{\"access_token\":"));
239 }
240}