Skip to content

Commit a962e4c

Browse files
committed
Merge #424: fix(mysql): [#410] Fix multiple MySQL configuration issues
afabd0c chore(mysql): fix linting issues from Phase 5 (Jose Celano) d465611 feat(mysql): move DSN to env var override with percent-encoding (Bug 1) (Jose Celano) 81ed6d1 feat(mysql): make root_password configurable, generate at creation time (Jose Celano) 2ec8f1e fix: [#410] reject 'root' as MySQL app username (Bug 3) (Jose Celano) Pull request description: ## Summary Fixes three related MySQL configuration bugs discovered during the Hetzner demo deployment (#405). Closes #410 --- ## Bug 1 — MySQL DSN hardcoded in `tracker.toml` with no URL-encoding **Problem**: The tracker config template interpolated raw credentials directly into the DSN URL. Passwords with URL-reserved characters (`@`, `:`, `/`, `+`) produced malformed DSNs and broke MySQL connectivity. The plaintext password also ended up in a mounted config file instead of an env var. **Fix**: - Added `percent-encoding` crate; built a custom `USERINFO_ENCODE` `AsciiSet` that preserves RFC 3986 unreserved chars (so `tracker_user` is not over-encoded) - DSN is now percent-encoded in Rust (`EnvContext::new_with_mysql`) and stored as `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH` in `.env` - `docker-compose.yml.tera` injects the new env var into the tracker container - `tracker.toml.tera` no longer contains the DSN; a static comment explains the injection - `MysqlTemplateConfig` removed entirely --- ## Bug 2 — MySQL root password not configurable **Problem**: Root password was unconditionally derived as `{app_password}_root`, giving users no way to supply their own value and making it entirely predictable. **Fix**: - Added optional `root_password` field to the environment config JSON schema - Added `generate_random_password()` in `src/shared/secrets/random.rs` (32-char, mixed charset, satisfies MySQL MEDIUM policy) - Password is generated **once at environment creation time** (in `TryFrom<DatabaseSection>`), not at render time, so it stays stable across re-renders - `create_mysql_contexts` no longer contains any derivation logic --- ## Bug 3 — No validation that MySQL app username is not `"root"` **Problem**: The MySQL Docker image rejects `MYSQL_USER=root`, but the deployer only validated for an empty username, deferring the error to Docker startup time. **Fix**: - Added `ReservedUsername` variant to `MysqlConfigError` with an actionable `help()` message - `MysqlConfig::new()` rejects `"root"` immediately with a clear error --- ## Testing - Added unit tests for all three fixes - All 2314 tests pass (`cargo test`) - Full E2E LXD deployment verified with password `tracker_p@ss!word#1` — confirmed `@` → `%40`, `#` → `%23` in the rendered `.env` - All linters pass (`cargo run --bin linter all`) - `cargo machete` reports no unused dependencies - Pre-commit checks pass (`./scripts/pre-commit.sh`) --- ## Checklist - [x] Pre-commit checks pass - [x] `MysqlConfig::new()` rejects `"root"` with `ReservedUsername` error - [x] `root_password` is configurable and generated at creation time when omitted - [x] DSN is percent-encoded and injected via env var override - [x] `tracker.toml` no longer contains the database password - [x] `cargo machete` reports no unused dependencies ACKs for top commit: josecelano: ACK afabd0c Tree-SHA512: 9af00556e7be392f909978729f3f27715ea0d9112870db1cb6a6122505b1e1e689aefc02aa28db3bc26c7f7f729c1430c37b2c85dbe0fd6566421a708d521313
2 parents 8e42698 + afabd0c commit a962e4c

18 files changed

Lines changed: 499 additions & 166 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ clap = { version = "4.0", features = [ "derive" ] }
5050
derive_more = { version = "2.1", features = [ "display", "from" ] }
5151
figment = { version = "0.10", features = [ "json" ] }
5252
parking_lot = "0.12"
53+
percent-encoding = "2.0"
54+
rand = "0.9"
5355
reqwest = "0.12"
5456
rust-embed = "8.0"
5557
schemars = "1.1"

docs/issues/410-bug-multiple-mysql-configuration-issues.md

Lines changed: 102 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,28 @@ value.
3535

3636
### Bug 1 — DSN in `tracker.toml`
3737

38-
- [ ] Move the MySQL DSN out of `tracker.toml` and into an environment variable override,
38+
- [x] Move the MySQL DSN out of `tracker.toml` and into an environment variable override,
3939
consistent with `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER` and
4040
`TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`
41-
- [ ] Build the percent-encoded DSN in Rust and expose it in the `.env` file as
41+
- [x] Build the percent-encoded DSN in Rust and expose it in the `.env` file as
4242
`TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH`
43-
- [ ] Pass the new env var into the tracker container via `docker-compose.yml.tera`
44-
- [ ] Remove the raw DSN line from `tracker.toml.tera` for the MySQL case
45-
- [ ] Remove the now-unused `MysqlTemplateConfig` from `TrackerContext`
43+
- [x] Pass the new env var into the tracker container via `docker-compose.yml.tera`
44+
- [x] Remove the raw DSN line from `tracker.toml.tera` for the MySQL case
45+
- [x] Remove the now-unused `MysqlTemplateConfig` from `TrackerContext`
4646

4747
### Bug 2 — Root password not configurable
4848

49-
- [ ] Add an optional `root_password` field to the MySQL section of the environment
49+
- [x] Add an optional `root_password` field to the MySQL section of the environment
5050
configuration JSON schema
51-
- [ ] When a root password is provided by the user, use it; when omitted, generate a
52-
strong random password at render time rather than deriving it from the app password
53-
- [ ] Remove the `format!("{password}_root")` derivation from `create_mysql_contexts`
51+
- [x] When a root password is provided by the user, use it; when omitted, generate a
52+
strong random password at environment creation time (application layer) rather
53+
than deriving it from the app password
54+
- [x] Remove the `format!("{password}_root")` derivation from `create_mysql_contexts`
5455

5556
### Bug 3 — Reserved username not rejected
5657

57-
- [ ] Add a `ReservedUsername` variant to `MysqlConfigError`
58-
- [ ] Reject `"root"` as the app DB username in `MysqlConfig::new()` with a clear,
58+
- [x] Add a `ReservedUsername` variant to `MysqlConfigError`
59+
- [x] Reject `"root"` as the app DB username in `MysqlConfig::new()` with a clear,
5960
actionable error message
6061

6162
## Specifications
@@ -174,21 +175,44 @@ no `root_password` field for MySQL. The deployer therefore always sets
174175

175176
**Fix**: Add an optional `root_password` to the MySQL section of the environment
176177
configuration JSON. If the user provides it, use it. If they omit it, generate a
177-
cryptographically random password at render time instead of deriving it from the app
178-
password. Remove the `format!("{password}_root")` derivation.
178+
cryptographically random password at **environment creation time** (application layer)
179+
instead of deriving it from the app password. Remove the `format!("{password}_root")`
180+
derivation.
181+
182+
**Key design decision — generate at creation time, not render time**: The root password
183+
is a domain invariant that must remain stable across multiple renders (e.g. re-deploying
184+
without reprovisioning). Generating it at render time would produce a different
185+
`MYSQL_ROOT_PASSWORD` on each render, breaking MySQL container restarts. Instead,
186+
generation happens once in the application layer (`TryFrom<DatabaseSection>`) when the
187+
environment is first created, and the value is persisted alongside the rest of the
188+
environment config.
179189

180190
**Affected modules and types**:
181191

192+
- `Cargo.toml`: add `rand = "0.9"` dependency.
182193
- `schemas/environment-config.json`: add optional `root_password` string to the MySQL
183194
database object.
184-
- `src/domain/tracker/config/core/database/mysql.rs` (`MysqlConfig`): add optional
185-
`root_password` field; update constructor and accessors.
186-
- `src/presentation/cli/controllers/create/subcommands/environment/config_loader.rs`
187-
(or equivalent deserialization path): propagate the optional field through to the
188-
domain type.
195+
- `src/shared/secrets/random.rs` (new file): `generate_random_password() -> Password`
196+
using `rand::rng()` (ThreadRng seeded from OsRng), guaranteeing one character from each
197+
class (lower, upper, digit, symbol), filled to 32 characters, then shuffled. Satisfies
198+
MySQL `validate_password MEDIUM` policy.
199+
- `src/shared/secrets/mod.rs` and `src/shared/mod.rs`: re-export
200+
`generate_random_password`.
201+
- `src/domain/tracker/config/core/database/mysql.rs` (`MysqlConfig`): `root_password`
202+
field is `Password` (non-optional) — the domain type always has a value. Constructor
203+
`new()` takes `root_password: Password`. Accessor `root_password() -> &Password` added.
204+
`MysqlConfigRaw` (the serde deserialization intermediate) keeps
205+
`root_password: Option<Password>` with `#[serde(default)]` for backward compatibility
206+
with persisted environments that pre-date this field; missing values are filled by
207+
calling `generate_random_password()` during deserialization.
208+
- `src/application/command_handlers/create/config/tracker/tracker_core_section.rs`
209+
(`TryFrom<DatabaseSection>`): generation happens here — the optional user-supplied
210+
`root_password` is mapped to `Password` if present, or `generate_random_password()` is
211+
called if absent. This is the single point of generation for new environments.
189212
- `src/application/services/rendering/docker_compose.rs` (`create_mysql_contexts`):
190-
replace `format!("{password}_root")` with either the user-supplied root password or a
191-
freshly generated random password.
213+
`root_password` parameter is now `PlainPassword` (non-optional); call site passes
214+
`mysql_config.root_password().expose_secret().to_string()`. No generation logic
215+
remains here.
192216

193217
### Bug 3 — Reserved MySQL Username `"root"` Not Rejected
194218

@@ -237,57 +261,62 @@ Tasks are ordered from simplest to most complex.
237261

238262
### Phase 1: Reject reserved MySQL username (Bug 3)
239263

240-
- [ ] In `MysqlConfigError` (`mysql.rs`): add `ReservedUsername` variant
241-
- [ ] Add `help()` arm for `ReservedUsername` with actionable fix instructions
242-
- [ ] In `MysqlConfig::new()`: add `if username == "root"` guard returning
264+
- [x] In `MysqlConfigError` (`mysql.rs`): add `ReservedUsername` variant
265+
- [x] Add `help()` arm for `ReservedUsername` with actionable fix instructions
266+
- [x] In `MysqlConfig::new()`: add `if username == "root"` guard returning
243267
`Err(MysqlConfigError::ReservedUsername)`
244-
- [ ] Add unit test `it_should_reject_root_as_username`
268+
- [x] Add unit test `it_should_reject_root_as_username`
245269

246270
### Phase 2: Make root password configurable (Bug 2)
247271

248-
- [ ] `schemas/environment-config.json`: add optional `root_password` string to the
272+
- [x] `schemas/environment-config.json`: add optional `root_password` string to the
249273
MySQL database object
250-
- [ ] `MysqlConfig` (`mysql.rs`): add optional `root_password` field; update constructor
251-
and accessor
252-
- [ ] Deserialization/config-loading path: thread the optional field through to the
253-
application layer
254-
- [ ] `create_mysql_contexts` (`docker_compose.rs`): replace
255-
`format!("{password}_root")` with user-supplied value or a randomly generated
256-
password (use `rand` / `getrandom` — already in `Cargo.toml` — to produce a
257-
16+ character alphanumeric string)
274+
- [x] `MysqlConfig` (`mysql.rs`): `root_password` is `Password` (non-optional) in the
275+
domain — always has a value. `MysqlConfigRaw` uses `Option<Password>` for backward
276+
compat with persisted environments lacking the field.
277+
- [x] `src/shared/secrets/random.rs` (new): `generate_random_password() -> Password`
278+
using mixed charset (lower + upper + digit + symbol), length 32, satisfies MySQL
279+
MEDIUM password policy
280+
- [x] `TryFrom<DatabaseSection>` (`tracker_core_section.rs`): generates root password at
281+
environment creation time — not at render time — so it is stable across re-renders
282+
- [x] `create_mysql_contexts` (`docker_compose.rs`): replaced `format!("{password}_root")`
283+
with `mysql_config.root_password().expose_secret().to_string()`; no generation
284+
logic remains here
258285

259286
### Phase 3: Move DSN to env var override and add URL-encoding (Bug 1)
260287

261-
- [ ] Add `percent-encoding` to `Cargo.toml`
262-
- [ ] `TrackerServiceConfig` (`env/context.rs`): add optional database path field
263-
- [ ] `EnvContext::new_with_mysql`: percent-encode username and password with
264-
`utf8_percent_encode(..., NON_ALPHANUMERIC)`, build the full DSN string, store in
265-
the new field
266-
- [ ] `TrackerContext` (`tracker_config/context.rs`): remove `MysqlTemplateConfig` and
288+
- [x] Add `percent-encoding` to `Cargo.toml`
289+
- [x] `TrackerServiceConfig` (`env/context.rs`): add optional database path field
290+
- [x] `EnvContext::new_with_mysql`: percent-encode username and password with
291+
`utf8_percent_encode(..., USERINFO_ENCODE)` (custom AsciiSet preserving RFC 3986
292+
unreserved chars), build the full DSN string, store in the new field
293+
- [x] `TrackerContext` (`tracker_config/context.rs`): remove `MysqlTemplateConfig` and
267294
the MySQL branch that builds it
268-
- [ ] `templates/docker-compose/.env.tera`: add
269-
`TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH` inside `{%- if mysql %}`
270-
- [ ] `templates/docker-compose/docker-compose.yml.tera`: inject the new env var into
271-
the tracker service `environment:` section, conditionally on
272-
`{%- if database.mysql %}`
273-
- [ ] `templates/tracker/tracker.toml.tera`: remove the MySQL `path =` line; add a
295+
- [x] `templates/docker-compose/.env.tera`: add
296+
`TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH` inside `{%- if mysql %}`,
297+
placed in the Tracker Service Configuration section
298+
- [x] `templates/docker-compose/docker-compose.yml.tera`: inject the new env var into
299+
the tracker service `environment:` section, conditionally on `{%- if mysql %}`
300+
- [x] `templates/tracker/tracker.toml.tera`: remove the MySQL `path =` line; add a
274301
comment explaining that the connection path is injected via the env var override
275302

276303
### Phase 4: Tests
277304

278-
- [ ] `mysql.rs`: add `it_should_reject_root_as_username` unit test (Phase 1)
279-
- [ ] `env/context.rs`: add test that `new_with_mysql` produces a correctly
305+
- [x] `mysql.rs`: add `it_should_reject_root_as_username` unit test (Phase 1)
306+
- [x] `env/context.rs`: add test that `new_with_mysql` produces a correctly
280307
percent-encoded DSN for a password containing special characters
281-
- [ ] `env/context.rs`: add test that `new` (SQLite) leaves the database path field as
308+
- [x] `env/context.rs`: add test that `new` (SQLite) leaves the database path field as
282309
`None`
283-
- [ ] `tracker_config/context.rs`: remove or update the test that referenced
310+
- [x] `tracker_config/context.rs`: remove or update the test that referenced
284311
`mysql_password` on `MysqlTemplateConfig`
285-
- [ ] Run `cargo test` to verify all tests pass
312+
- [x] Run `cargo test` to verify all tests pass (2314 passed)
286313

287314
### Phase 5: Linting and pre-commit
288315

289-
- [ ] Run linters: `cargo run --bin linter all`
290-
- [ ] Run pre-commit: `./scripts/pre-commit.sh`
316+
- [x] Run linters: `cargo run --bin linter all`
317+
- [x] Run pre-commit: `./scripts/pre-commit.sh`
318+
319+
> **Status**: Phases 1–5 complete and committed.
291320
292321
## Acceptance Criteria
293322

@@ -297,42 +326,46 @@ Tasks are ordered from simplest to most complex.
297326
298327
**Quality Checks**:
299328

300-
- [ ] Pre-commit checks pass: `./scripts/pre-commit.sh`
329+
- [x] Pre-commit checks pass: `./scripts/pre-commit.sh`
301330

302331
**Task-Specific Criteria — Bug 3 (reserved username)**:
303332

304-
- [ ] `MysqlConfig::new()` returns `Err(MysqlConfigError::ReservedUsername)` when
333+
- [x] `MysqlConfig::new()` returns `Err(MysqlConfigError::ReservedUsername)` when
305334
username is `"root"`
306-
- [ ] `MysqlConfigError::ReservedUsername` has a `help()` message with an actionable fix
307-
- [ ] A unit test for the reserved username rejection exists and passes
335+
- [x] `MysqlConfigError::ReservedUsername` has a `help()` message with an actionable fix
336+
- [x] A unit test for the reserved username rejection exists and passes
308337

309338
**Task-Specific Criteria — Bug 2 (root password)**:
310339

311-
- [ ] The environment configuration JSON schema accepts an optional `root_password` field
340+
- [x] The environment configuration JSON schema accepts an optional `root_password` field
312341
in the MySQL database object
313-
- [ ] When `root_password` is provided in the env JSON it is used as `MYSQL_ROOT_PASSWORD`
342+
- [x] When `root_password` is provided in the env JSON it is used as `MYSQL_ROOT_PASSWORD`
314343
in the rendered `.env`
315-
- [ ] When `root_password` is omitted, a randomly generated password is used — it is
344+
- [x] When `root_password` is omitted, a randomly generated password is used — it is
316345
**not** derived from the app password
317-
- [ ] `create_mysql_contexts` no longer contains `format!("{password}_root")`
346+
- [x] `create_mysql_contexts` no longer contains `format!("{password}_root")`
347+
- [x] The random password is generated once at environment creation time (not at render
348+
time), ensuring stability across multiple renders
349+
- [x] The domain type `MysqlConfig.root_password` is always populated (`Password`,
350+
non-optional)
318351

319352
**Task-Specific Criteria — Bug 1 (DSN in tracker.toml)**:
320353

321-
- [ ] The rendered `tracker.toml` for a MySQL deployment does **not** contain the
354+
- [x] The rendered `tracker.toml` for a MySQL deployment does **not** contain the
322355
database password
323-
- [ ] The rendered `.env` file contains
356+
- [x] The rendered `.env` file contains
324357
`TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH` with a correctly
325358
percent-encoded DSN when MySQL is configured
326-
- [ ] The rendered `.env` file does **not** contain
359+
- [x] The rendered `.env` file does **not** contain
327360
`TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH` when SQLite is configured
328-
- [ ] The rendered `docker-compose.yml` injects
361+
- [x] The rendered `docker-compose.yml` injects
329362
`TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__PATH` into the tracker service
330363
environment when MySQL is configured
331-
- [ ] A MySQL password containing URL-reserved characters (e.g. `@`, `+`, `/`) produces
332-
a valid, correctly encoded DSN in the `.env` file
333-
- [ ] A MySQL password with only alphanumeric characters is rendered unchanged
334-
- [ ] `MysqlTemplateConfig` no longer exists in `tracker_config/context.rs`
335-
- [ ] `cargo machete` reports no unused dependencies
364+
- [x] A MySQL password containing URL-reserved characters (e.g. `@`, `+`, `/`) produces
365+
a valid, correctly encoded DSN in the `.env` file (verified with `tracker_p@ss!word#1`)
366+
- [x] A MySQL password with only alphanumeric characters is rendered unchanged
367+
- [x] `MysqlTemplateConfig` no longer exists in `tracker_config/context.rs`
368+
- [x] `cargo machete` reports no unused dependencies
336369

337370
## Manual E2E Verification Test
338371

project-words.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,10 @@ zcat
540540
zeroize
541541
zoneinfo
542542
zstd
543+
CSPRNG
544+
USERINFO
545+
plainpassword
546+
userinfo
543547
Émojis
544548
значение
545549
ключ

schemas/environment-config.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@
145145
"username": {
146146
"description": "Database username",
147147
"type": "string"
148+
},
149+
"root_password": {
150+
"description": "Optional `MySQL` root password\n\nWhen provided, used as `MYSQL_ROOT_PASSWORD` in the rendered `.env` file.\nWhen absent, a cryptographically random password is generated at render time.\nNever set this to the same value as `password` in production environments.",
151+
"type": [
152+
"string",
153+
"null"
154+
],
155+
"default": null
148156
}
149157
},
150158
"required": [
@@ -515,4 +523,4 @@
515523
]
516524
}
517525
}
518-
}
526+
}

src/application/command_handlers/create/config/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ impl EnvironmentCreationConfigBuilder {
203203
database_name: database_name.into(),
204204
username: username.into(),
205205
password: password.into(),
206+
root_password: None,
206207
});
207208
self
208209
}

src/application/command_handlers/create/config/tracker/tracker_core_section.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize};
1616

1717
use crate::application::command_handlers::create::config::errors::CreateConfigError;
1818
use crate::domain::tracker::{DatabaseConfig, MysqlConfig, SqliteConfig, TrackerCoreConfig};
19-
use crate::shared::{Password, PlainPassword};
19+
use crate::shared::{generate_random_password, Password, PlainPassword};
2020

2121
/// Database configuration section (application DTO)
2222
///
@@ -67,6 +67,12 @@ pub enum DatabaseSection {
6767
/// Uses `PlainPassword` type alias to explicitly mark this as a temporarily visible secret.
6868
/// Converted to secure `Password` type in `to_database_config()` at the DTO-to-domain boundary.
6969
password: PlainPassword,
70+
/// Optional `MySQL` root password
71+
///
72+
/// When provided, used as `MYSQL_ROOT_PASSWORD` in the rendered `.env` file.
73+
/// When absent, a cryptographically random password is generated at environment creation time.
74+
#[serde(default)]
75+
root_password: Option<PlainPassword>,
7076
},
7177
}
7278

@@ -85,13 +91,17 @@ impl TryFrom<DatabaseSection> for DatabaseConfig {
8591
database_name,
8692
username,
8793
password,
94+
root_password,
8895
} => {
96+
let root_password = root_password
97+
.map_or_else(generate_random_password, |p| Password::from(p.as_str()));
8998
let config = MysqlConfig::new(
9099
host,
91100
port,
92101
database_name,
93102
username,
94103
Password::from(password.as_str()),
104+
root_password,
95105
)?;
96106
Ok(Self::Mysql(config))
97107
}
@@ -212,25 +222,23 @@ mod tests {
212222
database_name: "tracker".to_string(),
213223
username: "tracker_user".to_string(),
214224
password: "secure_password".to_string(),
225+
root_password: None,
215226
},
216227
private: false,
217228
};
218229

219230
let config: TrackerCoreConfig = section.try_into().unwrap();
220231

221-
assert_eq!(
222-
*config.database(),
223-
DatabaseConfig::Mysql(
224-
MysqlConfig::new(
225-
"localhost",
226-
3306,
227-
"tracker",
228-
"tracker_user",
229-
Password::from("secure_password"),
230-
)
231-
.unwrap()
232-
)
233-
);
232+
let DatabaseConfig::Mysql(mysql) = config.database() else {
233+
panic!("expected MySQL config");
234+
};
235+
assert_eq!(mysql.host(), "localhost");
236+
assert_eq!(mysql.port(), 3306);
237+
assert_eq!(mysql.database_name(), "tracker");
238+
assert_eq!(mysql.username(), "tracker_user");
239+
assert_eq!(mysql.password().expose_secret(), "secure_password");
240+
// root_password is generated randomly — just verify it is non-empty
241+
assert!(!mysql.root_password().expose_secret().is_empty());
234242
assert!(!config.private());
235243
}
236244

@@ -243,6 +251,7 @@ mod tests {
243251
database_name: "tracker".to_string(),
244252
username: "tracker_user".to_string(),
245253
password: "pass123".to_string(),
254+
root_password: None,
246255
},
247256
private: false,
248257
};
@@ -280,6 +289,7 @@ mod tests {
280289
database_name: "tracker".to_string(),
281290
username: "tracker_user".to_string(),
282291
password: "secure_password".to_string(),
292+
root_password: None,
283293
}
284294
);
285295
assert!(!section.private);

0 commit comments

Comments
 (0)