support root and path deployments explicitly

This commit is contained in:
2026-07-10 08:47:59 +00:00
parent a6e8be7553
commit 47795011b6
8 changed files with 237 additions and 26 deletions
+103 -14
View File
@@ -137,6 +137,7 @@ impl Config {
} else {
format!("/{trimmed_path}")
};
let base_url = base_url.trim_end_matches('/').to_string();
Ok(Self {
base_url,
@@ -1001,22 +1002,18 @@ mod tests {
}
fn config(db_path: PathBuf) -> Config {
Config {
base_url: "https://example.test/s".into(),
api_key: "secret".into(),
host: "127.0.0.1".into(),
port: 8080,
config_for_base(db_path, "https://example.test/s")
}
fn config_for_base(db_path: PathBuf, base_url: &str) -> Config {
Config::from_raw(RawConfig {
base_url: Some(base_url.into()),
api_key: Some("secret".into()),
db_path,
retention_days: 0,
min_short_length: 6,
max_short_length: 32,
max_url_length: 2048,
max_retention_days: 3650,
rate_limit_requests: 1_000,
rate_limit_window: 60,
production: false,
base_path: "/s".into(),
}
..RawConfig::default()
})
.unwrap()
}
fn json(response: &ResponseData) -> Value {
@@ -1032,6 +1029,98 @@ mod tests {
assert_eq!(local_path("/api/health/", ""), Some("/api/health".into()));
}
#[test]
fn every_endpoint_accepts_trailing_slashes_at_root_and_under_a_path() {
for (label, base_url, prefix) in [
("subdomain", "https://s.example.test/", ""),
("subpath", "https://example.test/s/", "/s"),
] {
let path = temp_path(label);
let app = App::new(config_for_base(path.clone(), base_url)).unwrap();
let target = |route: &str| format!("{prefix}{route}");
assert_eq!(app.config.base_path, prefix);
assert_eq!(app.handle(RequestData::new("GET", target("/"))).status, 200);
assert_eq!(
app.handle(RequestData::new("GET", target("///"))).status,
200
);
assert_eq!(
app.handle(RequestData::new("GET", target("/static/")))
.status,
200
);
assert_eq!(
app.handle(RequestData::new("GET", target("/static/app.js/")))
.status,
200
);
assert_eq!(
app.handle(RequestData::new("GET", target("/api/health/")))
.status,
200
);
assert_eq!(
app.handle(RequestData::new("OPTIONS", target("/api/health/")))
.status,
200
);
let mut create = RequestData::new("POST", target("/api/shorten/"));
create.body = br#"{"url":"https://destination.example/one"}"#.to_vec();
let created = app.handle(create);
assert_eq!(created.status, 201);
let short_url = String::from_utf8(created.body).unwrap();
let expected_base = base_url.trim_end_matches('/');
assert_eq!(app.config.base_url, expected_base);
assert!(short_url.starts_with(&format!("{expected_base}/")));
let code = short_url.rsplit('/').next().unwrap();
let get_create = app.handle(RequestData::new(
"GET",
target("/api/shorten/?url=https%3A%2F%2Fdestination.example%2Ftwo"),
));
assert_eq!(get_create.status, 201);
let lookup = app.handle(RequestData::new(
"GET",
target("/api/lookup/?url=https%3A%2F%2Fdestination.example%2Fone"),
));
assert_eq!(lookup.status, 200);
assert_eq!(json(&lookup)["short_code"], code);
assert_eq!(json(&lookup)["short_url"], short_url);
let metadata = app.handle(RequestData::new(
"GET",
target(&format!("/api/urls/{code}/")),
));
assert_eq!(metadata.status, 200);
let listing = app.handle(RequestData::new("GET", target("/api/urls/?api_key=secret")));
assert_eq!(listing.status, 200);
assert_eq!(json(&listing)["count"], 2);
let redirect = app.handle(RequestData::new("GET", target(&format!("/{code}/"))));
assert_eq!(redirect.status, 302);
assert!(redirect
.headers
.contains(&("Location".into(), "https://destination.example/one".into())));
let deleted = app.handle(RequestData::new(
"DELETE",
target(&format!("/api/urls/{code}/?api_key=secret")),
));
assert_eq!(deleted.status, 204);
let missing = app.handle(RequestData::new(
"GET",
target(&format!("/api/urls/{code}/")),
));
assert_eq!(missing.status, 404);
let _ = fs::remove_file(path);
}
}
#[test]
fn existing_database_without_retention_column_is_migrated() {
let path = temp_path("schema");