testcontainers_modules/gitea/mod.rs
1/// Self-hosted git server with https/http/ssh access, uses [Gitea](https://docs.gitea.com/).
2use std::result::Result;
3
4use rcgen::{BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair};
5use testcontainers::{
6 core::{
7 wait::HttpWaitStrategy, CmdWaitFor, ContainerPort, ContainerState, ExecCommand, WaitFor,
8 },
9 CopyDataSource, CopyToContainer, Image, TestcontainersError,
10};
11
12/// Container port for SSH listener.
13pub const GITEA_SSH_PORT: ContainerPort = ContainerPort::Tcp(2222);
14/// Container port for HTTPS/HTTP listener.
15pub const GITEA_HTTP_PORT: ContainerPort = ContainerPort::Tcp(3000);
16/// Container port for HTTP listener to redirect call to HTTPS port.
17pub const GITEA_HTTP_REDIRECT_PORT: ContainerPort = ContainerPort::Tcp(3080);
18
19/// Default admin username.
20pub const GITEA_DEFAULT_ADMIN_USERNAME: &str = "git-admin";
21/// Default admin password.
22pub const GITEA_DEFAULT_ADMIN_PASSWORD: &str = "git-admin";
23
24/// Container folder where configuration and SSL certificates are stored to.
25pub const GITEA_CONFIG_FOLDER: &str = "/etc/gitea";
26/// Container folder with git data: repos, DB, etc.
27pub const GITEA_DATA_FOLDER: &str = "/var/lib/gitea";
28
29/// Docker hub registry with gitea image.
30const GITEA_IMAGE_NAME: &str = "gitea/gitea";
31/// Image tag to use.
32const GITEA_IMAGE_TAG: &str = "1.22.3-rootless";
33
34/// File name with SSL certificate.
35const TLS_CERT_FILE_NAME: &str = "cert.pem";
36/// File name with a private key for SSL certificate.
37const TLS_KEY_FILE_NAME: &str = "key.pem";
38/// File name with a Gitea config.
39const CONFIG_FILE_NAME: &str = "app.ini";
40
41/// Module to work with [Gitea](https://docs.gitea.com/) container.
42///
43/// Starts an instance of [`Gitea`](https://docs.gitea.com/), fully functional git server, with reasonable defaults
44/// and possibility to tune some configuration options.
45///
46/// From the `Gitea` documentation:
47/// _Gitea is a painless, self-hosted, all-in-one software development service.
48/// It includes Git hosting, code review, team collaboration, package registry,
49/// and CI/CD. It is similar to GitHub, Bitbucket and GitLab._
50///
51/// By default, `Gitea` server container starts with the following config:
52/// - accepts SSH (Git) protocol requests on port [GITEA_SSH_PORT];
53/// - accepts HTTP requests on port [GITEA_HTTP_PORT];
54/// - has a single configured user with admin privileges,
55/// with pre-defined [username](GITEA_DEFAULT_ADMIN_USERNAME) and [password](GITEA_DEFAULT_ADMIN_PASSWORD);
56/// - configured git server hostname is `localhost`; this is a name which `Gitea` uses in the links to repositories;
57/// - no repositories are created.
58///
59/// Additionally to defaults, it's possible to:
60/// - use HTTPS instead of HTTP with auto-generated self-signed certificate or provide your own certificate;
61/// - redirect HTTP calls to HTTPS listener, if HTTPS is enabled;
62/// - change git server hostname, which is used in various links to repos or web-server;
63/// - provide your own admin user credentials as well as its SSH public key to authorize git calls;
64/// - create any number of public or private repositories with provided names during server startup;
65/// - execute set of `gitea admin ...` commands during server startup to customize configuration;
66/// - add environment variables
67///
68/// # Examples
69///
70/// 1. Minimalistic server
71/// ```rust
72/// use testcontainers::{runners::AsyncRunner, ImageExt};
73/// use testcontainers_modules::gitea::{self, Gitea, GiteaRepo};
74///
75/// #[tokio::test]
76/// async fn default_gitea_server() {
77/// // Run default container
78/// let gitea = Gitea::default().start().await.unwrap();
79/// let port = gitea
80/// .get_host_port_ipv4(gitea::GITEA_HTTP_PORT)
81/// .await
82/// .unwrap();
83/// let url = format!(
84/// "http://localhost:{port}/api/v1/users/{}",
85/// gitea::GITEA_DEFAULT_ADMIN_USERNAME
86/// );
87///
88/// // Anonymous query Gitea API for user info
89/// let response = reqwest::Client::new().get(url).send().await.unwrap();
90/// assert_eq!(response.status(), 200);
91/// }
92/// ```
93///
94/// 2. Customized server
95/// ```rust
96/// use testcontainers::{runners::AsyncRunner, ImageExt};
97/// use testcontainers_modules::gitea::{self, Gitea, GiteaRepo};
98///
99/// #[tokio::test]
100/// async fn gitea_server_with_custom_config() {
101/// // Start server container with:
102/// // - custom admin credentials
103/// // - two repos: public and private
104/// // - TLS enabled
105/// // - port mapping for HTTP and SSH
106/// // - custom git hostname
107/// let gitea = Gitea::default()
108/// .with_git_hostname("gitea.example.com")
109/// .with_admin_account("custom-admin", "password", None)
110/// .with_repo(GiteaRepo::Public("public-test-repo".to_string()))
111/// .with_repo(GiteaRepo::Private("private-test-repo".to_string()))
112/// .with_tls(true)
113/// .with_mapped_port(443, gitea::GITEA_HTTP_PORT)
114/// .with_mapped_port(22, gitea::GITEA_SSH_PORT)
115/// .start()
116/// .await
117/// .unwrap();
118///
119/// // Obtain auto-created root CA certificate
120/// let ca = gitea.image().tls_ca().unwrap();
121/// let ca = reqwest::Certificate::from_pem(ca.as_bytes()).unwrap();
122/// // Attach custom CA to the client
123/// let client = reqwest::ClientBuilder::new()
124/// .add_root_certificate(ca)
125/// .build()
126/// .unwrap();
127///
128/// // Get list of repos of particular user.
129/// // This query should be authorized.
130/// let response = client
131/// .get("https://localhost/api/v1/user/repos")
132/// .basic_auth("custom-admin", Some("password"))
133/// .header("Host", "gitea.example.com")
134/// .send()
135/// .await
136/// .unwrap();
137/// assert_eq!(response.status(), 200);
138///
139/// let repos = response.json::<serde_json::Value>().await.unwrap();
140/// assert_eq!(repos.as_array().unwrap().len(), 2);
141/// }
142/// ```
143#[derive(Debug, Clone)]
144pub struct Gitea {
145 git_hostname: String,
146 admin_username: String,
147 admin_password: String,
148 admin_key: Option<String>,
149 admin_commands: Vec<Vec<String>>,
150 tls: Option<GiteaTlsCert>,
151 repos: Vec<GiteaRepo>,
152 copy_to_sources: Vec<CopyToContainer>,
153}
154
155impl Default for Gitea {
156 /// Returns default Gitea server setup with the following defaults:
157 /// - hostname is `localhost`;
158 /// - admin account username from [GITEA_DEFAULT_ADMIN_USERNAME];
159 /// - admin account password from [GITEA_DEFAULT_ADMIN_PASSWORD];
160 /// - without admins' account SSH public key;
161 /// - without additional startup admin commands;
162 /// - without TLS (SSH and HTTP protocols only);
163 /// - without repositories.
164 fn default() -> Self {
165 Self {
166 git_hostname: "localhost".to_string(),
167 admin_username: GITEA_DEFAULT_ADMIN_USERNAME.to_string(),
168 admin_password: GITEA_DEFAULT_ADMIN_PASSWORD.to_string(),
169 admin_key: None,
170 admin_commands: vec![],
171 tls: None,
172 repos: vec![],
173 copy_to_sources: vec![Self::render_app_ini("http", "localhost", false)],
174 }
175 }
176}
177
178impl Image for Gitea {
179 fn name(&self) -> &str {
180 GITEA_IMAGE_NAME
181 }
182
183 fn tag(&self) -> &str {
184 GITEA_IMAGE_TAG
185 }
186
187 fn ready_conditions(&self) -> Vec<WaitFor> {
188 let http_check = match self.tls {
189 Some(_) => WaitFor::seconds(5), // it's expensive to add reqwest dependency for the single health check only
190 None => WaitFor::http(
191 HttpWaitStrategy::new("/api/swagger")
192 .with_port(GITEA_HTTP_PORT)
193 .with_expected_status_code(200_u16),
194 ),
195 };
196
197 vec![
198 WaitFor::message_on_stdout(format!(
199 "Starting new Web server: tcp:0.0.0.0:{}",
200 GITEA_HTTP_PORT.as_u16()
201 )),
202 http_check,
203 ]
204 }
205
206 fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
207 &self.copy_to_sources
208 }
209
210 fn expose_ports(&self) -> &[ContainerPort] {
211 if self.tls.is_some() {
212 // additional port for HTTP with redirect to HTTPS
213 &[GITEA_SSH_PORT, GITEA_HTTP_PORT, GITEA_HTTP_REDIRECT_PORT]
214 } else {
215 &[GITEA_SSH_PORT, GITEA_HTTP_PORT]
216 }
217 }
218
219 fn exec_after_start(
220 &self,
221 _cs: ContainerState,
222 ) -> Result<Vec<ExecCommand>, TestcontainersError> {
223 // Create admin user
224 let mut start_commands = vec![self.create_admin_user_cmd()];
225 // Add admins' public key if needed
226 if let Some(key) = &self.admin_key {
227 start_commands.push(self.create_admin_key_cmd(key));
228 }
229 // create repos if they're defined
230 self.repos.iter().for_each(|r| {
231 start_commands.push(self.create_repo_cmd(r));
232 });
233
234 // and finally, add `gitea admin` commands, if defined
235 let admin_commands: Vec<Vec<String>> = self
236 .admin_commands
237 .clone()
238 .into_iter()
239 .map(|v| {
240 vec!["gitea".to_string(), "admin".to_string()]
241 .into_iter()
242 .chain(v)
243 .collect::<Vec<String>>()
244 })
245 .collect();
246
247 // glue everything togather
248 start_commands.extend(admin_commands);
249
250 // and convert to `ExecCommand`s
251 let commands: Vec<ExecCommand> = start_commands
252 .iter()
253 .map(|v| ExecCommand::new(v).with_cmd_ready_condition(CmdWaitFor::exit_code(0)))
254 .collect();
255
256 Ok(commands)
257 }
258}
259
260impl Gitea {
261 /// Change admin user credential to the custom provided `username` and `password` instead of using defaults.
262 ///
263 /// If `public_key` value is provided, it will be added to the admin account.
264 ///
265 /// # Example
266 /// ```rust,ignore
267 /// #[tokio::test]
268 /// async fn test() {
269 /// const PUB_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJRE5a67/cTbR6DpWqzBl6BTY0LE0Hg715ZI/FMK7iCH";
270 /// let gitea = Gitea::default()
271 /// .with_admin_account("git-admin", "nKz4SC7bkz4KSXbQ", Some(PUB_KEY))
272 /// .start()
273 /// .await
274 /// .unwrap();
275 /// // ...
276 /// }
277 /// ```
278 pub fn with_admin_account(
279 self,
280 username: impl Into<String>,
281 password: impl Into<String>,
282 public_key: Option<String>,
283 ) -> Self {
284 Self {
285 admin_username: username.into(),
286 admin_password: password.into(),
287 admin_key: public_key,
288 ..self
289 }
290 }
291
292 /// Set git server hostname instead of the default `localhost`.
293 ///
294 /// This is not a containers' hostname, but the name which git server uses in various links like repo URLs.
295 pub fn with_git_hostname(self, hostname: impl Into<String>) -> Self {
296 let new = Self {
297 git_hostname: hostname.into(),
298 ..self
299 };
300 Self {
301 // to update app.ini
302 copy_to_sources: new.generate_copy_to_sources(),
303 ..new
304 }
305 }
306
307 /// Create a repository during startup.
308 ///
309 /// It's possible to call this method more than once to create several repositories.
310 ///
311 /// # Example
312 /// ```rust,ignore
313 /// #[tokio::test]
314 /// async fn test() {
315 /// let gitea = Gitea::default()
316 /// .with_repo(GiteaRepo::Public("example-public-repo"))
317 /// .with_repo(GiteaRepo::Private("example-private-repo"))
318 /// .start()
319 /// .await
320 /// .unwrap();
321 /// // ...
322 /// }
323 /// ```
324 pub fn with_repo(self, repo: GiteaRepo) -> Self {
325 let mut repos = self.repos;
326 repos.push(repo);
327 Self { repos, ..self }
328 }
329
330 /// Add `gitea admin ...` command with parameters to execute after server startup.
331 ///
332 /// This method is useful, for example, to create additional users or to do other admin stuff.
333 ///
334 /// It's possible to call this method more than once to add several consecutive commands.
335 ///
336 /// # Example
337 /// ```rust,ignore
338 /// #[tokio::test]
339 /// async fn test() {
340 /// let cmd = vec![
341 /// "user",
342 /// "create",
343 /// "--username",
344 /// "test-user",
345 /// "--password",
346 /// "test-password",
347 /// "--email",
348 /// "test@localhost",
349 /// "--must-change-password=true",
350 /// ]
351 /// .into_iter()
352 /// .map(String::from)
353 /// .collect::<Vec<String>>();
354 ///
355 /// let gitea = Gitea::default()
356 /// .with_admin_command(command)
357 /// .start()
358 /// .await
359 /// .unwrap();
360 /// // ...
361 /// }
362 /// ```
363 pub fn with_admin_command(self, command: impl IntoIterator<Item = impl Into<String>>) -> Self {
364 let command = command
365 .into_iter()
366 .map(|s| s.into())
367 .collect::<Vec<String>>();
368 let mut admin_commands = self.admin_commands;
369
370 admin_commands.push(command);
371 Self {
372 admin_commands,
373 ..self
374 }
375 }
376
377 /// `Gitea` web server will start with HTTPS listener (with auto-generated certificate),
378 /// instead of the default HTTP.
379 ///
380 /// If `enabled` is `true,` web server will be started with TLS listener with auto-generated self-signed certificate.
381 /// If Root CA certificate is needed to ensure fully protected communications,
382 /// it can be obtained by [Gitea::tls_ca()] method call.
383 ///
384 /// Note: _If TLS is enabled, additional HTTP listener will be started on port [GITEA_HTTP_REDIRECT_PORT]
385 /// to redirect all HTTP calls to the HTTPS listener._
386 pub fn with_tls(self, enabled: bool) -> Self {
387 let new = Self {
388 tls: if enabled {
389 Some(GiteaTlsCert::default())
390 } else {
391 None
392 },
393 ..self
394 };
395
396 Self {
397 // to update app.ini and certificates
398 copy_to_sources: new.generate_copy_to_sources(),
399 ..new
400 }
401 }
402
403 /// `Gitea` web server will start with HTTPS listener (with provided certificate), instead of the default HTTP.
404 ///
405 /// `cert` and `key` are strings with PEM encoded certificate and its key.
406 /// This method is similar to [Gitea::with_tls()] but use provided certificate instead of generating self-signed one.
407 ///
408 /// Note: _If TLS is enabled, additional HTTP listener will be started on port [GITEA_HTTP_REDIRECT_PORT]
409 /// to redirect all HTTP calls to the HTTPS listener._
410 pub fn with_tls_certs(self, cert: impl Into<String>, key: impl Into<String>) -> Self {
411 let new = Self {
412 tls: Some(GiteaTlsCert::from_pem(cert.into(), key.into())),
413 ..self
414 };
415
416 Self {
417 // to update app.ini and certificates
418 copy_to_sources: new.generate_copy_to_sources(),
419 ..new
420 }
421 }
422
423 /// Return PEM encoded Root CA certificate of the Gitea servers' certificate issuer.
424 ///
425 /// If TLS has been enabled using [Gitea::with_tls_certs()] method (with auto-generated self-signed certificate),
426 /// then this method returns `Some` option with issuer root CA certificate to verify servers' certificate
427 /// and ensure fully protected communications.
428 ///
429 /// If TLS isn't enabled or TLS is enabled with external certificate,
430 /// provided using [Gitea::with_tls_certs] method,
431 /// this method returns `None` since there is no known CA certificate.
432 pub fn tls_ca(&self) -> Option<&str> {
433 self.tls.as_ref().and_then(|t| t.ca())
434 }
435
436 /// Gather app.ini and certificates (if needed) into one vector to store into modules' structure.
437 fn generate_copy_to_sources(&self) -> Vec<CopyToContainer> {
438 let mut to_copy = vec![];
439
440 // Prepare app.ini from template
441 let app_ini = Self::render_app_ini(
442 self.protocol(),
443 self.git_hostname.as_str(),
444 self.tls.is_some(),
445 );
446 to_copy.push(app_ini);
447
448 // Add certificates if TLS is enabled
449 if let Some(tls_config) = &self.tls {
450 let cert = CopyToContainer::new(
451 CopyDataSource::Data(tls_config.cert.clone().into_bytes()),
452 format!("{GITEA_CONFIG_FOLDER}/{TLS_CERT_FILE_NAME}"),
453 );
454 let key = CopyToContainer::new(
455 CopyDataSource::Data(tls_config.key.clone().into_bytes()),
456 format!("{GITEA_CONFIG_FOLDER}/{TLS_KEY_FILE_NAME}"),
457 );
458 to_copy.push(cert);
459 to_copy.push(key);
460 }
461
462 to_copy
463 }
464
465 /// Render app.ini content from the template using current config values.
466 fn render_app_ini(protocol: &str, hostname: &str, is_tls: bool) -> CopyToContainer {
467 let redirect_port = GITEA_HTTP_REDIRECT_PORT.as_u16();
468 // load template of the app.ini,
469 // `[server]` section should be at the bottom to add variable part
470 // and TLS-related variables is needed
471 let mut app_ini_template = include_str!("app.ini").to_string();
472 let host_template_part = format!(
473 r#"
474DOMAIN = {hostname}
475SSH_DOMAIN = {hostname}
476ROOT_URL = {protocol}://{hostname}/
477PROTOCOL = {protocol}
478"#,
479 );
480 app_ini_template.push_str(&host_template_part);
481
482 // If TLS is enabled, add TLS-related config to app.ini
483 if is_tls {
484 let tls_config = format!(
485 r#"
486CERT_FILE = {GITEA_CONFIG_FOLDER}/{TLS_CERT_FILE_NAME}
487KEY_FILE = {GITEA_CONFIG_FOLDER}/{TLS_KEY_FILE_NAME}
488REDIRECT_OTHER_PORT = true
489PORT_TO_REDIRECT = {redirect_port}
490"#
491 );
492 app_ini_template.push_str(&tls_config);
493 }
494
495 CopyToContainer::new(
496 CopyDataSource::Data(app_ini_template.into_bytes()),
497 format!("{GITEA_CONFIG_FOLDER}/{CONFIG_FILE_NAME}"),
498 )
499 }
500
501 /// Generate command to create admin user with actual parameters.
502 fn create_admin_user_cmd(&self) -> Vec<String> {
503 vec![
504 "gitea",
505 "admin",
506 "user",
507 "create",
508 "--username",
509 self.admin_username.as_str(),
510 "--password",
511 self.admin_password.as_str(),
512 "--email",
513 format!("{}@localhost", self.admin_username).as_str(),
514 "--admin",
515 ]
516 .into_iter()
517 .map(String::from)
518 .collect::<Vec<String>>()
519 }
520
521 /// Generate curl command with API call to add public key for admin user.
522 fn create_admin_key_cmd(&self, key: &String) -> Vec<String> {
523 let body = format!(r#"{{"title":"default","key":"{key}","read_only":false}}"#);
524 self.create_gitea_api_curl_cmd("POST", "/user/keys", Some(body))
525 }
526
527 /// Generate curl command with API call to create repository with minimal parameters.
528 fn create_repo_cmd(&self, repo: &GiteaRepo) -> Vec<String> {
529 let (repo, private) = match repo {
530 GiteaRepo::Private(name) => (name, "true"),
531 GiteaRepo::Public(name) => (name, "false"),
532 };
533
534 let body = format!(
535 r#"{{"name":"{repo}","readme":"Default","auto_init":true,"private":{private}}}"#,
536 );
537
538 self.create_gitea_api_curl_cmd("POST", "/user/repos", Some(body))
539 }
540
541 /// Helper to generate curl commands with API call.
542 fn create_gitea_api_curl_cmd(
543 &self,
544 method: &str,
545 api_path: &str,
546 body: Option<String>,
547 ) -> Vec<String> {
548 let mut curl = vec![
549 "curl",
550 "-sk",
551 "-X",
552 method,
553 "-H",
554 "accept: application/json",
555 "-H",
556 "Content-Type: application/json",
557 "-u",
558 format!("{}:{}", self.admin_username, self.admin_password).as_str(),
559 ]
560 .into_iter()
561 .map(String::from)
562 .collect::<Vec<String>>();
563
564 // add body if present
565 if let Some(body) = body {
566 curl.push("-d".to_string());
567 curl.push(body);
568 }
569
570 // and finally, add url to API with a requested path
571 curl.push(self.api_url(api_path));
572
573 curl
574 }
575
576 /// Return configured protocol string.
577 fn protocol(&self) -> &str {
578 if self.tls.is_some() {
579 "https"
580 } else {
581 "http"
582 }
583 }
584
585 /// Return container-internal base URL to the API.
586 fn api_url(&self, api: &str) -> String {
587 let api = api.strip_prefix('/').unwrap_or(api);
588 format!(
589 "{}://localhost:{}/api/v1/{api}",
590 self.protocol(),
591 GITEA_HTTP_PORT.as_u16()
592 )
593 }
594}
595
596/// Defines repository to create during container startup.
597///
598/// Each option includes repository name in the enum value.
599///
600/// [`Gitea::with_repo`] documentation provides more details and usage examples.
601#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
602pub enum GiteaRepo {
603 /// Create a private repository which is accessible with authorization only.
604 Private(String),
605 /// Create a public repository accessible without authorization.
606 Public(String),
607}
608
609/// Helper struct to store TLS certificates.
610#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
611struct GiteaTlsCert {
612 cert: String,
613 key: String,
614 ca: Option<String>,
615}
616
617impl Default for GiteaTlsCert {
618 fn default() -> Self {
619 Self::new("localhost")
620 }
621}
622
623impl GiteaTlsCert {
624 /// Generate new self-signed Root CA certificate,
625 /// and generate new server certificate signed by CA.
626 ///
627 /// SAN list includes "localhost", "127.0.0.1", "::1"
628 /// and provided hostname (if it's different form localhost).
629 fn new(hostname: impl Into<String>) -> Self {
630 // generate root CA key and cert
631 let ca_key = KeyPair::generate().unwrap();
632 let mut ca_cert = CertificateParams::new(vec!["Gitea root CA".to_string()]).unwrap();
633 ca_cert.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
634
635 // prepare SANs
636 let mut hostnames = vec![
637 "localhost".to_string(),
638 "127.0.0.1".to_string(),
639 "::1".to_string(),
640 ];
641 let hostname = hostname.into();
642 if hostname != "localhost" {
643 hostnames.insert(0, hostname);
644 }
645
646 // and generate server key and cert
647 let key = KeyPair::generate().unwrap();
648 let issuer = Issuer::from_params(&ca_cert, &ca_key);
649 let cert = CertificateParams::new(hostnames)
650 .unwrap()
651 .signed_by(&key, &issuer)
652 .unwrap();
653
654 let ca_cert = ca_cert.self_signed(&ca_key).unwrap();
655 Self {
656 cert: cert.pem(),
657 key: key.serialize_pem(),
658 ca: Some(ca_cert.pem()),
659 }
660 }
661
662 /// Construct from externally provided certificate and key, without CA.
663 fn from_pem(cert: impl Into<String>, key: impl Into<String>) -> Self {
664 Self {
665 cert: cert.into(),
666 key: key.into(),
667 ca: None,
668 }
669 }
670
671 /// Return self-signed Root CA is it was generated.
672 fn ca(&self) -> Option<&str> {
673 self.ca.as_deref()
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use reqwest::Certificate;
680 use serde_json::Value;
681 use testcontainers::{runners::AsyncRunner, ContainerAsync};
682
683 use super::*;
684
685 const TEST_PUBLIC_KEY: &str =
686 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJRE5a67/cTbR6DpWqzBl6BTY0LE0Hg715ZI/FMK7iCH";
687 const TEST_ADMIN_USERNAME: &str = "non-default-user";
688 const TEST_ADMIN_PASSWORD: &str = "some-dummy-password";
689 const TEST_PUBLIC_REPO: &str = "test-public-repo";
690 const TEST_PRIVATE_REPO: &str = "test-private-repo";
691
692 async fn api_url(container: &ContainerAsync<Gitea>, api: &str) -> String {
693 let api = api.strip_prefix('/').unwrap_or(api);
694 let host = container.get_host().await.unwrap();
695 let port = container.get_host_port_ipv4(GITEA_HTTP_PORT).await.unwrap();
696
697 format!(
698 "{}://{host}:{port}/api/v1/{api}",
699 container.image().protocol(),
700 )
701 }
702
703 #[tokio::test]
704 async fn gitea_defaults() {
705 let gitea = Gitea::default().start().await.unwrap();
706
707 // Check for admin user
708 let response = reqwest::Client::new()
709 .get(api_url(&gitea, &format!("/users/{GITEA_DEFAULT_ADMIN_USERNAME}")).await)
710 .basic_auth(
711 GITEA_DEFAULT_ADMIN_USERNAME,
712 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
713 )
714 .send()
715 .await
716 .unwrap();
717
718 assert_eq!(response.status(), 200);
719
720 // Check for an admin user public key
721 let keys_list = reqwest::Client::new()
722 .get(api_url(&gitea, "/user/keys").await)
723 .basic_auth(
724 GITEA_DEFAULT_ADMIN_USERNAME,
725 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
726 )
727 .send()
728 .await
729 .unwrap()
730 .json::<Value>()
731 .await
732 .unwrap();
733
734 let keys_list = keys_list.as_array().unwrap();
735 assert!(keys_list.is_empty());
736 }
737
738 #[tokio::test]
739 async fn gitea_with_tls() {
740 let gitea = Gitea::default().with_tls(true).start().await.unwrap();
741
742 // Check w/o CA, should fail
743 let response = reqwest::Client::new()
744 .get(api_url(&gitea, &format!("/users/{GITEA_DEFAULT_ADMIN_USERNAME}")).await)
745 .basic_auth(
746 GITEA_DEFAULT_ADMIN_USERNAME,
747 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
748 )
749 .send()
750 .await;
751 assert!(response.is_err());
752
753 // Check with CA, should pass
754 let ca = gitea.image().tls_ca().unwrap();
755 let ca = Certificate::from_pem(ca.as_bytes()).unwrap();
756 let client = reqwest::ClientBuilder::new()
757 .use_rustls_tls()
758 .add_root_certificate(ca)
759 .build()
760 .unwrap();
761
762 let response = client
763 .get(api_url(&gitea, &format!("/users/{GITEA_DEFAULT_ADMIN_USERNAME}")).await)
764 .basic_auth(
765 GITEA_DEFAULT_ADMIN_USERNAME,
766 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
767 )
768 .send()
769 .await
770 .unwrap();
771 assert_eq!(response.status(), 200);
772 }
773
774 #[tokio::test]
775 async fn gitea_custom_admin_credentials() {
776 let gitea = Gitea::default()
777 .with_admin_account(
778 TEST_ADMIN_USERNAME,
779 TEST_ADMIN_PASSWORD,
780 Some(TEST_PUBLIC_KEY.to_string()),
781 )
782 .start()
783 .await
784 .unwrap();
785
786 // Check for an admin user public key with default credentials,
787 // fails since user doesn't exist
788 let response = reqwest::Client::new()
789 .get(api_url(&gitea, "/user/keys").await)
790 .basic_auth(
791 GITEA_DEFAULT_ADMIN_USERNAME,
792 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
793 )
794 .send()
795 .await
796 .unwrap();
797
798 assert_eq!(response.status(), 401);
799
800 // The same check with custom credentials should pass
801 let keys_list = reqwest::Client::new()
802 .get(api_url(&gitea, "/user/keys").await)
803 .basic_auth(TEST_ADMIN_USERNAME, Some(TEST_ADMIN_PASSWORD))
804 .send()
805 .await
806 .unwrap()
807 .json::<Value>()
808 .await
809 .unwrap();
810
811 let keys_list = keys_list.as_array().unwrap();
812 assert_eq!(keys_list.len(), 1);
813 }
814
815 #[tokio::test]
816 async fn gitea_create_repos() {
817 let gitea = Gitea::default()
818 .with_repo(GiteaRepo::Public(TEST_PUBLIC_REPO.to_string()))
819 .with_repo(GiteaRepo::Private(TEST_PRIVATE_REPO.to_string()))
820 .start()
821 .await
822 .unwrap();
823
824 // Check access to the public repo w/o auth
825 let response = reqwest::Client::new()
826 .get(
827 api_url(
828 &gitea,
829 &format!("/repos/{GITEA_DEFAULT_ADMIN_USERNAME}/{TEST_PUBLIC_REPO}"),
830 )
831 .await,
832 )
833 .send()
834 .await
835 .unwrap();
836
837 assert_eq!(response.status(), 200);
838
839 // Check access to the private repo w/o auth,
840 // should be 404
841 let response = reqwest::Client::new()
842 .get(
843 api_url(
844 &gitea,
845 &format!("/repos/{GITEA_DEFAULT_ADMIN_USERNAME}/{TEST_PRIVATE_REPO}"),
846 )
847 .await,
848 )
849 .send()
850 .await
851 .unwrap();
852
853 assert_eq!(response.status(), 404);
854
855 // Check access to the private repo with auth,
856 // should be 200
857 let response = reqwest::Client::new()
858 .get(
859 api_url(
860 &gitea,
861 &format!("/repos/{GITEA_DEFAULT_ADMIN_USERNAME}/{TEST_PRIVATE_REPO}"),
862 )
863 .await,
864 )
865 .basic_auth(
866 GITEA_DEFAULT_ADMIN_USERNAME,
867 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
868 )
869 .send()
870 .await
871 .unwrap();
872
873 assert_eq!(response.status(), 200);
874 }
875
876 #[tokio::test]
877 async fn gitea_admin_commands() {
878 let command = vec![
879 "user",
880 "create",
881 "--username",
882 TEST_ADMIN_USERNAME,
883 "--password",
884 TEST_ADMIN_PASSWORD,
885 "--email",
886 format!("{}@localhost", TEST_ADMIN_USERNAME).as_str(),
887 "--must-change-password=false",
888 ]
889 .into_iter()
890 .map(String::from)
891 .collect::<Vec<String>>();
892
893 let gitea = Gitea::default()
894 .with_admin_command(command)
895 .start()
896 .await
897 .unwrap();
898
899 // Check for new custom user
900 let response = reqwest::Client::new()
901 .get(api_url(&gitea, &format!("/users/{TEST_ADMIN_USERNAME}")).await)
902 .basic_auth(
903 GITEA_DEFAULT_ADMIN_USERNAME,
904 Some(GITEA_DEFAULT_ADMIN_PASSWORD),
905 )
906 .send()
907 .await
908 .unwrap();
909 assert_eq!(response.status(), 200);
910
911 // Check with users' credentials
912 let response = reqwest::Client::new()
913 .get(api_url(&gitea, "/user/emails").await)
914 .basic_auth(TEST_ADMIN_USERNAME, Some(TEST_ADMIN_PASSWORD))
915 .send()
916 .await
917 .unwrap()
918 .json::<Value>()
919 .await
920 .unwrap();
921
922 let response = response.as_array().unwrap();
923 assert_eq!(response.len(), 1);
924 }
925}