All checks were successful
PR Checks / rust-gates (pull_request) Successful in 7m52s
Phase 0.2 of the installer plan (docs/installer/PLAN.md §5). The installer has to drive this binary non-interactively, and today it cannot: the auth token is only readable by scraping the startup log, the config path can only be named through an environment variable, and a relative db path follows the process working directory — which a service manager, not the operator, chooses. - Add a four-flag CLI (cli.rs): --print-config, --config <PATH>, --version, --help. Hand-rolled; an argument-parsing dependency would be larger than the code it replaced. An unrecognized flag exits 2 rather than starting a sidecar that is not the one that was asked for. - --print-config resolves the configuration exactly as a normal start does — including writing a missing config file and generating a blank auth token — and prints it as JSON on stdout: versions, protocol, both bind addresses, ws path, resolved db path, and the token. config_created / token_generated let a re-run tell "read an existing install" from "provisioned a new one". The log subscriber is deliberately not started in this mode, so the document is the whole output. - Anchor a relative [store].path to the config file's directory instead of the CWD, and report resolved absolute paths. A unit pinning UOLINK_CONFIG now keeps its database beside its config rather than in %SystemRoot%\System32 or a VirtualStore redirect. Development is unaffected: under cargo run the two directories are the same. :memory: and file: URIs are left alone. - Hand the db path to sqlx as a filesystem path instead of formatting it into a sqlite:// URL, which percent-decodes it and splits it on '?'. An installed path containing %20 previously opened a different file; verified it now does not. - Create the config's and the database's parent directories when missing, so a service can name /var/lib/runicgateway on a host where nothing made it yet. - 22 unit tests covering argument parsing, path anchoring, token persistence, the generated config template, and the --print-config document. No protocol change: PROTOCOL_VERSION stays 3. Co-Authored-By: Claude <noreply@anthropic.com>
616 lines
21 KiB
Rust
616 lines
21 KiB
Rust
//! Runtime configuration, loaded from an external file — nothing here is compiled into the binary.
|
|
//!
|
|
//! Precedence: environment variables override the file, the file overrides built-in defaults. On
|
|
//! first run, if the file is absent, a default one is written with a freshly generated auth token,
|
|
//! so the sidecar is secured out of the box and the operator just copies the token to the website.
|
|
//!
|
|
//! File path: `--config <PATH>`, else `$UOLINK_CONFIG`, else `sidecar.toml` in the working
|
|
//! directory.
|
|
//!
|
|
//! **Paths are anchored to the config file, not the working directory.** A relative
|
|
//! `[store].path` resolves against the directory holding `sidecar.toml`. A service started with
|
|
//! `UOLINK_CONFIG=/etc/runicgateway/sidecar.toml` therefore keeps its database beside its config
|
|
//! instead of wherever the service manager happened to set the working directory — which on
|
|
//! Windows can be `%SystemRoot%\System32` or, under `C:\Program Files\`, a silently redirected
|
|
//! VirtualStore copy. The values reported by `--print-config` are the resolved absolute ones.
|
|
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
use serde::Deserialize;
|
|
use serde_json::json;
|
|
use tracing::info;
|
|
|
|
use crate::PROTOCOL_VERSION;
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
pub struct Config {
|
|
#[serde(default)]
|
|
pub shard: ShardCfg,
|
|
#[serde(default)]
|
|
pub web: WebCfg,
|
|
#[serde(default)]
|
|
pub store: StoreCfg,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShardCfg {
|
|
#[serde(default = "default_shard_bind")]
|
|
pub bind: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct WebCfg {
|
|
#[serde(default = "default_web_bind")]
|
|
pub bind: String,
|
|
/// Shared secret the website must present. Never empty in practice — `Config::load` generates
|
|
/// and persists one when it finds none, so the web surface is authenticated from first boot.
|
|
#[serde(default)]
|
|
pub auth_token: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct StoreCfg {
|
|
#[serde(default = "default_db_path")]
|
|
pub path: String,
|
|
}
|
|
|
|
/// A loaded configuration plus what loading it *did* — an installer re-running the binary needs to
|
|
/// distinguish "read an existing install" from "provisioned a new one", and it cannot tell from the
|
|
/// values alone.
|
|
#[derive(Debug)]
|
|
pub struct Loaded {
|
|
pub cfg: Config,
|
|
/// Absolute path of the config file that was read or written.
|
|
pub path: PathBuf,
|
|
/// The config file did not exist and was created by this run.
|
|
pub config_created: bool,
|
|
/// No usable token was configured, so one was generated and saved.
|
|
pub token_generated: bool,
|
|
}
|
|
|
|
fn default_shard_bind() -> String {
|
|
"127.0.0.1:7788".into()
|
|
}
|
|
fn default_web_bind() -> String {
|
|
"127.0.0.1:8080".into()
|
|
}
|
|
fn default_db_path() -> String {
|
|
"uo-link.db".into()
|
|
}
|
|
|
|
impl Default for ShardCfg {
|
|
fn default() -> Self {
|
|
Self {
|
|
bind: default_shard_bind(),
|
|
}
|
|
}
|
|
}
|
|
impl Default for WebCfg {
|
|
fn default() -> Self {
|
|
Self {
|
|
bind: default_web_bind(),
|
|
auth_token: String::new(),
|
|
}
|
|
}
|
|
}
|
|
impl Default for StoreCfg {
|
|
fn default() -> Self {
|
|
Self {
|
|
path: default_db_path(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
/// Which config file this invocation will use: `--config`, else `$UOLINK_CONFIG`, else
|
|
/// `sidecar.toml` beside the working directory. Always returned absolute, so every later
|
|
/// message names a path the operator can act on.
|
|
pub fn resolve_path(cli_override: Option<&str>) -> PathBuf {
|
|
let raw = cli_override
|
|
.map(str::to_string)
|
|
.or_else(|| env::var("UOLINK_CONFIG").ok())
|
|
.unwrap_or_else(|| "sidecar.toml".into());
|
|
absolutize(PathBuf::from(raw))
|
|
}
|
|
|
|
pub fn load(cli_override: Option<&str>) -> anyhow::Result<Loaded> {
|
|
let path = Self::resolve_path(cli_override);
|
|
let existed = path.exists();
|
|
|
|
let mut cfg: Config = if existed {
|
|
let text = fs::read_to_string(&path)?;
|
|
toml::from_str(&text)?
|
|
} else {
|
|
Config::default()
|
|
};
|
|
|
|
cfg.apply_env();
|
|
|
|
// Authentication is always on. A blank token is never allowed — if none is set (fresh
|
|
// install, or someone cleared it), generate one, save it, and continue. This keeps setup
|
|
// effortless while making it impossible to accidentally run with auth off.
|
|
let token_generated = cfg.web.auth_token.trim().is_empty();
|
|
if token_generated {
|
|
let token = generate_token();
|
|
|
|
if existed {
|
|
persist_token(&path, &token)?;
|
|
} else {
|
|
// The parent may not exist yet when an installer points at a fresh
|
|
// /etc/runicgateway; failing here would mean "run me again after mkdir".
|
|
create_parent_dir(&path)?;
|
|
fs::write(&path, default_file(&token))?;
|
|
}
|
|
|
|
cfg.web.auth_token = token.clone();
|
|
|
|
info!("No auth token configured.");
|
|
info!("Generated new token: {}", token);
|
|
info!("Saved to {}. Authentication is on.", path.display());
|
|
}
|
|
|
|
cfg.anchor_store_path(&path);
|
|
|
|
Ok(Loaded {
|
|
cfg,
|
|
path,
|
|
config_created: !existed,
|
|
token_generated,
|
|
})
|
|
}
|
|
|
|
/// Environment overrides, so a deployment can set secrets without editing the file.
|
|
fn apply_env(&mut self) {
|
|
if let Ok(v) = env::var("UOLINK_SHARD_BIND") {
|
|
self.shard.bind = v;
|
|
}
|
|
if let Ok(v) = env::var("UOLINK_WEB_BIND") {
|
|
self.web.bind = v;
|
|
}
|
|
if let Ok(v) = env::var("UOLINK_WEB_TOKEN") {
|
|
self.web.auth_token = v;
|
|
}
|
|
if let Ok(v) = env::var("UOLINK_DB_PATH") {
|
|
self.store.path = v;
|
|
}
|
|
}
|
|
|
|
/// Resolves `[store].path` against the config file's directory (see the module docs). Absolute
|
|
/// paths and SQLite's non-filesystem spellings are left exactly as written.
|
|
fn anchor_store_path(&mut self, config_path: &Path) {
|
|
if is_sqlite_special(&self.store.path) {
|
|
return;
|
|
}
|
|
let raw = PathBuf::from(&self.store.path);
|
|
let anchored = if raw.is_absolute() {
|
|
raw
|
|
} else {
|
|
config_dir(config_path).join(raw)
|
|
};
|
|
self.store.path = absolutize(anchored).to_string_lossy().into_owned();
|
|
}
|
|
|
|
pub fn auth_required(&self) -> bool {
|
|
// Always true now — load() guarantees a non-empty token.
|
|
!self.web.auth_token.is_empty()
|
|
}
|
|
}
|
|
|
|
/// The `--print-config` document: everything an installer needs to register this sidecar with a
|
|
/// website, in one non-interactive read.
|
|
///
|
|
/// **This includes the auth token in clear text**, which is the point — §2.4 of the installer plan
|
|
/// calls the manual token hunt the largest "I installed it and nothing happened" failure mode. The
|
|
/// caller prints it to stdout and starts no log subscriber, so the document is the whole output.
|
|
pub fn describe(loaded: &Loaded) -> serde_json::Value {
|
|
json!({
|
|
"component": "uo-link-sidecar",
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
"protocol": PROTOCOL_VERSION,
|
|
"config_path": loaded.path.to_string_lossy(),
|
|
"config_created": loaded.config_created,
|
|
"token_generated": loaded.token_generated,
|
|
"shard": { "bind": loaded.cfg.shard.bind },
|
|
"web": {
|
|
"bind": loaded.cfg.web.bind,
|
|
"ws_path": crate::web::WS_PATH,
|
|
"auth_required": loaded.cfg.auth_required(),
|
|
"auth_token": loaded.cfg.web.auth_token,
|
|
},
|
|
"store": { "path": loaded.cfg.store.path },
|
|
})
|
|
}
|
|
|
|
/// Directory holding the config file. A bare `sidecar.toml` has no parent component, which would
|
|
/// join into an empty base — treat it as the current directory.
|
|
fn config_dir(config_path: &Path) -> PathBuf {
|
|
match config_path.parent() {
|
|
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
|
|
_ => PathBuf::from("."),
|
|
}
|
|
}
|
|
|
|
/// Prefixes the working directory onto a relative path, then drops the `.` components that
|
|
/// joining leaves behind — cosmetic, but these paths are printed and pasted into service units.
|
|
fn absolutize(p: PathBuf) -> PathBuf {
|
|
let joined = if p.is_absolute() {
|
|
p
|
|
} else {
|
|
match env::current_dir() {
|
|
Ok(cwd) => cwd.join(p),
|
|
Err(_) => p,
|
|
}
|
|
};
|
|
let cleaned: PathBuf = joined
|
|
.components()
|
|
.filter(|c| !matches!(c, Component::CurDir))
|
|
.collect();
|
|
if cleaned.as_os_str().is_empty() {
|
|
joined
|
|
} else {
|
|
cleaned
|
|
}
|
|
}
|
|
|
|
/// `:memory:` and `file:` URIs are instructions to SQLite, not paths on disk. Anchoring them to a
|
|
/// directory would turn a working in-memory store into an attempt to create a file called
|
|
/// `:memory:` — which Windows cannot even name.
|
|
fn is_sqlite_special(path: &str) -> bool {
|
|
path == ":memory:" || path.starts_with("file:")
|
|
}
|
|
|
|
fn create_parent_dir(path: &Path) -> anyhow::Result<()> {
|
|
if let Some(dir) = path.parent() {
|
|
if !dir.as_os_str().is_empty() && !dir.exists() {
|
|
fs::create_dir_all(dir)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Rewrites the `auth_token` line in an existing config file, preserving everything else. Falls
|
|
/// back to inserting it under `[web]`, or appending a `[web]` section, if the key is absent.
|
|
fn persist_token(path: &Path, token: &str) -> anyhow::Result<()> {
|
|
let text = fs::read_to_string(path)?;
|
|
let line = format!("auth_token = \"{token}\"");
|
|
|
|
if text
|
|
.lines()
|
|
.any(|l| l.trim_start().starts_with("auth_token"))
|
|
{
|
|
let out: String = text
|
|
.lines()
|
|
.map(|l| {
|
|
if l.trim_start().starts_with("auth_token") {
|
|
line.clone()
|
|
} else {
|
|
l.to_string()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
fs::write(path, out + "\n")?;
|
|
} else if text.lines().any(|l| l.trim() == "[web]") {
|
|
let out: String = text
|
|
.lines()
|
|
.flat_map(|l| {
|
|
if l.trim() == "[web]" {
|
|
vec![l.to_string(), line.clone()]
|
|
} else {
|
|
vec![l.to_string()]
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
fs::write(path, out + "\n")?;
|
|
} else {
|
|
fs::write(path, format!("{text}\n[web]\n{line}\n"))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_token() -> String {
|
|
let mut buf = [0u8; 24];
|
|
// OS randomness; falls back to a time-seeded token only if the OS RNG is unavailable.
|
|
if getrandom::getrandom(&mut buf).is_err() {
|
|
let nanos = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_nanos())
|
|
.unwrap_or(0);
|
|
return format!("insecure-fallback-{nanos:x}");
|
|
}
|
|
buf.iter().map(|b| format!("{b:02x}")).collect()
|
|
}
|
|
|
|
fn default_file(token: &str) -> String {
|
|
format!(
|
|
r#"# uo-link sidecar configuration.
|
|
# Read at startup. Nothing here is compiled into the binary. Environment variables
|
|
# (UOLINK_SHARD_BIND, UOLINK_WEB_BIND, UOLINK_WEB_TOKEN, UOLINK_DB_PATH) override these.
|
|
|
|
[shard]
|
|
# Loopback address the shard dials out to. Keep this on localhost — the game must
|
|
# not be reachable from anywhere else.
|
|
bind = "127.0.0.1:7788"
|
|
|
|
[web]
|
|
# Address the website connects to (WebSocket + REST).
|
|
# 127.0.0.1:8080 -> same host only
|
|
# 0.0.0.0:8080 -> accept remote clients (then auth_token is mandatory)
|
|
bind = "127.0.0.1:8080"
|
|
|
|
# Shared secret the website must present on every request:
|
|
# REST: Authorization: Bearer <token> (or X-Api-Key: <token>)
|
|
# WebSocket: add ?token=<token> to the connect URL
|
|
# Authentication is always on: if this is left blank, the sidecar generates a new
|
|
# token here on startup. Rotate by changing this value and restarting.
|
|
# Read it back without starting the sidecar: uo-link-sidecar --print-config
|
|
auth_token = "{token}"
|
|
|
|
[store]
|
|
# Relative paths resolve against the directory holding THIS FILE, not the working
|
|
# directory of the process.
|
|
path = "uo-link.db"
|
|
"#
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A unique scratch directory. `std::env::temp_dir()` plus the test name keeps the cases
|
|
/// independent under the default parallel test runner.
|
|
fn scratch(name: &str) -> PathBuf {
|
|
let dir = env::temp_dir().join(format!("uo-link-cfg-test-{name}"));
|
|
let _ = fs::remove_dir_all(&dir);
|
|
fs::create_dir_all(&dir).expect("create scratch dir");
|
|
dir
|
|
}
|
|
|
|
/// `Config::load` consults the process environment, and a developer may have `UOLINK_*` set
|
|
/// for a local shard. Mutating shared env state from a test thread is worse than skipping, so
|
|
/// the two cases that exercise the full load path bail out instead of failing spuriously.
|
|
fn env_overrides_present() -> bool {
|
|
[
|
|
"UOLINK_SHARD_BIND",
|
|
"UOLINK_WEB_BIND",
|
|
"UOLINK_WEB_TOKEN",
|
|
"UOLINK_DB_PATH",
|
|
]
|
|
.iter()
|
|
.any(|k| env::var_os(k).is_some())
|
|
}
|
|
|
|
fn cfg_with_store(path: &str) -> Config {
|
|
Config {
|
|
store: StoreCfg { path: path.into() },
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn relative_store_path_anchors_to_the_config_directory() {
|
|
// The working-directory trap: the service pins UOLINK_CONFIG but the service manager
|
|
// decides the CWD, so a relative db path must not follow the CWD.
|
|
let mut cfg = cfg_with_store("uo-link.db");
|
|
let config_path = if cfg!(windows) {
|
|
PathBuf::from(r"C:\ProgramData\RunicGateway\sidecar.toml")
|
|
} else {
|
|
PathBuf::from("/etc/runicgateway/sidecar.toml")
|
|
};
|
|
cfg.anchor_store_path(&config_path);
|
|
|
|
let expected = config_path.parent().unwrap().join("uo-link.db");
|
|
assert_eq!(Path::new(&cfg.store.path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn absolute_store_path_is_left_alone() {
|
|
let absolute = if cfg!(windows) {
|
|
r"C:\ProgramData\RunicGateway\uo-link.db"
|
|
} else {
|
|
"/var/lib/runicgateway/uo-link.db"
|
|
};
|
|
let mut cfg = cfg_with_store(absolute);
|
|
cfg.anchor_store_path(Path::new("/etc/runicgateway/sidecar.toml"));
|
|
assert_eq!(cfg.store.path, absolute);
|
|
}
|
|
|
|
#[test]
|
|
fn a_bare_config_filename_anchors_to_the_working_directory() {
|
|
// `cargo run` in the crate root: config dir and CWD are the same, so the historical
|
|
// behavior (db beside the binary's CWD) is preserved exactly.
|
|
let mut cfg = cfg_with_store("uo-link.db");
|
|
cfg.anchor_store_path(Path::new("sidecar.toml"));
|
|
assert_eq!(
|
|
Path::new(&cfg.store.path),
|
|
env::current_dir().unwrap().join("uo-link.db")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlite_special_paths_are_not_anchored() {
|
|
for special in [":memory:", "file:cache?mode=memory"] {
|
|
let mut cfg = cfg_with_store(special);
|
|
cfg.anchor_store_path(Path::new("/etc/runicgateway/sidecar.toml"));
|
|
assert_eq!(cfg.store.path, special);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_path_prefers_the_cli_override() {
|
|
// Absolute in, absolute out — and unchanged, so the operator sees the path they passed.
|
|
let explicit = if cfg!(windows) {
|
|
r"C:\tmp\custom.toml"
|
|
} else {
|
|
"/tmp/custom.toml"
|
|
};
|
|
assert_eq!(
|
|
Config::resolve_path(Some(explicit)),
|
|
PathBuf::from(explicit)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_path_makes_a_relative_override_absolute() {
|
|
let resolved = Config::resolve_path(Some("./conf/sidecar.toml"));
|
|
assert!(resolved.is_absolute(), "{}", resolved.display());
|
|
assert_eq!(
|
|
resolved,
|
|
env::current_dir()
|
|
.unwrap()
|
|
.join("conf")
|
|
.join("sidecar.toml")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn persist_token_replaces_an_existing_key() {
|
|
let dir = scratch("replace");
|
|
let path = dir.join("sidecar.toml");
|
|
fs::write(
|
|
&path,
|
|
"[web]\nbind = \"127.0.0.1:8080\"\nauth_token = \"\"\n\n[store]\npath = \"x.db\"\n",
|
|
)
|
|
.unwrap();
|
|
|
|
persist_token(&path, "deadbeef").unwrap();
|
|
|
|
let out = fs::read_to_string(&path).unwrap();
|
|
assert!(out.contains("auth_token = \"deadbeef\""), "{out}");
|
|
assert_eq!(out.matches("auth_token").count(), 1, "{out}");
|
|
// Everything else survives — the file is the operator's, not ours to rewrite.
|
|
assert!(out.contains("bind = \"127.0.0.1:8080\""), "{out}");
|
|
assert!(out.contains("path = \"x.db\""), "{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn persist_token_inserts_under_an_existing_web_section() {
|
|
let dir = scratch("insert");
|
|
let path = dir.join("sidecar.toml");
|
|
fs::write(&path, "[web]\nbind = \"127.0.0.1:8080\"\n").unwrap();
|
|
|
|
persist_token(&path, "deadbeef").unwrap();
|
|
|
|
let out = fs::read_to_string(&path).unwrap();
|
|
let web = out.find("[web]").unwrap();
|
|
let token = out.find("auth_token").unwrap();
|
|
assert!(token > web, "token must land inside [web]: {out}");
|
|
assert!(out.contains("auth_token = \"deadbeef\""), "{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn persist_token_appends_a_web_section_when_there_is_none() {
|
|
let dir = scratch("append");
|
|
let path = dir.join("sidecar.toml");
|
|
fs::write(&path, "[shard]\nbind = \"127.0.0.1:7788\"\n").unwrap();
|
|
|
|
persist_token(&path, "deadbeef").unwrap();
|
|
|
|
let out = fs::read_to_string(&path).unwrap();
|
|
assert!(out.contains("[shard]"), "{out}");
|
|
assert!(out.contains("[web]\nauth_token = \"deadbeef\""), "{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_generated_config_round_trips_through_the_parser() {
|
|
// The template is a format! string, so a stray brace or a bad key would only ever surface
|
|
// on someone's first run.
|
|
let cfg: Config = toml::from_str(&default_file("deadbeef")).expect("template parses");
|
|
assert_eq!(cfg.web.auth_token, "deadbeef");
|
|
assert_eq!(cfg.shard.bind, "127.0.0.1:7788");
|
|
assert_eq!(cfg.web.bind, "127.0.0.1:8080");
|
|
assert_eq!(cfg.store.path, "uo-link.db");
|
|
}
|
|
|
|
#[test]
|
|
fn generated_tokens_are_random_and_hex() {
|
|
let (a, b) = (generate_token(), generate_token());
|
|
assert_ne!(a, b);
|
|
assert_eq!(a.len(), 48);
|
|
assert!(a.chars().all(|c| c.is_ascii_hexdigit()), "{a}");
|
|
}
|
|
|
|
#[test]
|
|
fn describe_reports_the_resolved_configuration() {
|
|
let loaded = Loaded {
|
|
cfg: Config {
|
|
shard: ShardCfg {
|
|
bind: "127.0.0.1:7788".into(),
|
|
},
|
|
web: WebCfg {
|
|
bind: "0.0.0.0:8080".into(),
|
|
auth_token: "deadbeef".into(),
|
|
},
|
|
store: StoreCfg {
|
|
path: "/var/lib/runicgateway/uo-link.db".into(),
|
|
},
|
|
},
|
|
path: PathBuf::from("/etc/runicgateway/sidecar.toml"),
|
|
config_created: true,
|
|
token_generated: true,
|
|
};
|
|
|
|
let doc = describe(&loaded);
|
|
|
|
assert_eq!(doc["component"], "uo-link-sidecar");
|
|
assert_eq!(doc["version"], env!("CARGO_PKG_VERSION"));
|
|
assert_eq!(doc["protocol"], PROTOCOL_VERSION);
|
|
assert_eq!(doc["config_path"], "/etc/runicgateway/sidecar.toml");
|
|
assert_eq!(doc["config_created"], true);
|
|
assert_eq!(doc["token_generated"], true);
|
|
assert_eq!(doc["shard"]["bind"], "127.0.0.1:7788");
|
|
assert_eq!(doc["web"]["bind"], "0.0.0.0:8080");
|
|
assert_eq!(doc["web"]["ws_path"], "/ws");
|
|
assert_eq!(doc["web"]["auth_required"], true);
|
|
assert_eq!(doc["web"]["auth_token"], "deadbeef");
|
|
assert_eq!(doc["store"]["path"], "/var/lib/runicgateway/uo-link.db");
|
|
}
|
|
|
|
#[test]
|
|
fn load_provisions_a_missing_config_and_reports_it() {
|
|
if env_overrides_present() {
|
|
return;
|
|
}
|
|
let dir = scratch("provision");
|
|
let path = dir.join("sidecar.toml");
|
|
|
|
let loaded = Config::load(Some(path.to_str().unwrap())).unwrap();
|
|
|
|
assert!(loaded.config_created);
|
|
assert!(loaded.token_generated);
|
|
assert!(
|
|
path.exists(),
|
|
"the config file must be written, not just held in memory"
|
|
);
|
|
assert!(!loaded.cfg.web.auth_token.is_empty());
|
|
// The db lands beside the config, whatever the working directory is.
|
|
assert_eq!(Path::new(&loaded.cfg.store.path), dir.join("uo-link.db"));
|
|
|
|
// Second run: same token, and nothing reported as new.
|
|
let again = Config::load(Some(path.to_str().unwrap())).unwrap();
|
|
assert!(!again.config_created);
|
|
assert!(!again.token_generated);
|
|
assert_eq!(again.cfg.web.auth_token, loaded.cfg.web.auth_token);
|
|
}
|
|
|
|
#[test]
|
|
fn load_creates_the_config_directory() {
|
|
if env_overrides_present() {
|
|
return;
|
|
}
|
|
// An installer pointing at a fresh /etc/runicgateway should not have to mkdir first.
|
|
let dir = scratch("mkdir").join("nested").join("deeper");
|
|
let path = dir.join("sidecar.toml");
|
|
|
|
let loaded = Config::load(Some(path.to_str().unwrap())).unwrap();
|
|
|
|
assert!(path.exists(), "{}", path.display());
|
|
assert!(loaded.config_created);
|
|
}
|
|
}
|