use std::io::{Read as _, Write as _};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use super::{http, start_nginx, test_fixture};
use crate::config::{IpPrefix, ServeLimits, ValidatedConfig};
use crate::manifest::GenerationManifest;
use crate::plan::repo_token;
const REPO: &str = "did:plc:repo";
const FULL_PACK: &[u8] = b"0008NAK\nFULLPACK";
const BLOB_BYTES: &[u8] = b"\x89PNG\r\n\x1a\nblob";
const BLOB_CID: &str = "bafkreib6ntpxmgkcwtwzvxbpcmqfuzbqtcqxqkqbxqfzqkqkqkqkqkqkqk";
fn nginx_binary() -> PathBuf {
PathBuf::from(
std::env::var_os("NGINX_BINARY")
.expect("NGINX_BINARY must name the real nginx binary for this ignored test"),
)
}
fn smart_dir(generation: &Path) -> PathBuf {
generation
.join("www/_git")
.join(repo_token(REPO))
.join("smart")
}
fn crawled_fixture(temp: &Path) -> (ValidatedConfig, GenerationManifest, PathBuf) {
let (config, mut manifest, generation) = test_fixture(temp);
let tip = |n: usize, m: usize| format!("{:040x}", (n * 1000 + m + 0x1000) as u64);
let repo = &mut manifest.repos[0];
repo.fetch_bases = (0..crate::git_export::FETCH_BASES)
.map(|n| (0..8).map(|m| tip(n, m)).collect())
.collect();
repo.shallow_branches = (0..8).map(|n| tip(100 + n, 0)).collect();
repo.shallow_head = Some(tip(200, 0));
let smart = smart_dir(&generation);
std::fs::create_dir_all(&smart).unwrap();
std::fs::write(generation.join("www/_health.json"), b"{}").unwrap();
std::fs::write(generation.join("www/_describe.json"), b"{}").unwrap();
std::fs::write(generation.join("www/index.html"), b"<!doctype html>").unwrap();
std::fs::write(
generation
.join("www")
.join(crate::index_page::FAVICON_SVG_FILE),
crate::index_page::FAVICON_SVG,
)
.unwrap();
std::fs::write(
generation
.join("www")
.join(crate::index_page::FAVICON_PNG_FILE),
crate::index_page::FAVICON_PNG,
)
.unwrap();
std::fs::write(
generation
.join("www")
.join(crate::index_page::APPLE_TOUCH_ICON_FILE),
crate::index_page::APPLE_TOUCH_ICON,
)
.unwrap();
std::fs::write(
generation
.join("www")
.join(crate::index_page::OG_IMAGE_FILE),
crate::index_page::FAVICON_PNG,
)
.unwrap();
std::fs::write(smart.join("advertisement"), b"0000ADVERTISED").unwrap();
std::fs::write(smart.join("adv-v2"), b"0000ADVERTISEDV2").unwrap();
for name in [
"fetch-nodone",
"v0-nodone",
"ls-refs",
"ls-refs-head",
"ls-refs-head-cobs",
] {
std::fs::write(smart.join(name), b"0008NAK\n").unwrap();
}
for name in ["fetch-done", "upload-pack"] {
std::fs::write(smart.join(name), FULL_PACK).unwrap();
}
crate::atproto::emit(&generation, &[]).unwrap();
crate::blobs::emit(temp, &generation, &[], &Default::default()).unwrap();
let blobs = generation.join("www/_blobs").join(repo_token(REPO));
std::fs::create_dir_all(&blobs).unwrap();
std::fs::write(blobs.join(format!("{BLOB_CID}.png")), BLOB_BYTES).unwrap();
let row = |map: &str, row: String| {
let path = generation.join("nginx/maps").join(map);
let routes = std::fs::read_to_string(&path)
.unwrap()
.replace("default \"\";", &format!("default \"\";\n {row};"));
std::fs::write(&path, routes).unwrap();
};
row(
"61_blobs.conf",
format!("{REPO}|{BLOB_CID} {}/{BLOB_CID}.png", repo_token(REPO)),
);
row("60_atproto.conf", format!("{REPO} {}", repo_token(REPO)));
(config, manifest, generation)
}
/// the last tip of each base is left out. every have row scans the whole body before failing.
fn fetch_of_haves(manifest: &GenerationManifest, size: usize, done: bool) -> Vec<u8> {
let mut body = String::from("0011command=fetch\n0001");
for base in &manifest.repos[0].fetch_bases {
for tip in &base[..base.len() - 1] {
body.push_str(&format!("0032have {tip}\n"));
}
}
let mut filler = 0u64;
while body.len() < size {
body.push_str(&format!("0032have {:040x}\n", 0xdead_0000 + filler));
filler += 1;
}
if done {
body.push_str("0009done\n");
}
body.into_bytes()
}
fn send(port: u16, body: &[u8], forwarded: Option<&str>) -> TcpStream {
let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
let mut head = format!(
"POST /{REPO}/git-upload-pack HTTP/1.1\r\nHost: knot.test\r\nConnection: close\r\n\
Content-Length: {}\r\n",
body.len()
);
if let Some(client) = forwarded {
head.push_str(&format!("X-Forwarded-For: {client}\r\n"));
}
head.push_str("\r\n");
stream.write_all(head.as_bytes()).unwrap();
stream.write_all(body).unwrap();
stream
}
fn ask(port: u16, body: &[u8], forwarded: Option<&str>) -> (u16, String) {
let mut stream = send(port, body, forwarded);
let mut answer = Vec::new();
stream.read_to_end(&mut answer).unwrap();
let head = String::from_utf8_lossy(&answer[..answer.len().min(1024)]).into_owned();
let status = head.split(' ').nth(1).unwrap().parse().unwrap();
(status, head)
}
/// pcre hits its match limit (error -47) on a body of a few MiB, and the map answers 400.
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_fetch_body_past_pcres_interpreted_match_limit_still_gets_its_pack() {
let temp = tempfile::tempdir().unwrap();
let (config, manifest, generation) = crawled_fixture(temp.path());
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
let body = fetch_of_haves(&manifest, 8 << 20, true);
let (status, _, reply) = http(
port,
"POST",
&format!("/{REPO}/git-upload-pack"),
&[],
&body,
);
assert_eq!(status, 200, "a big fetch body must still answer a pack");
assert_eq!(reply, FULL_PACK);
}
/// the hop has `proxy_buffering off`. it only sends what the client reads.
fn loopback_bytes_for_a_stalled_reader(pack: &[u8]) -> u64 {
let temp = tempfile::tempdir().unwrap();
let (config, manifest, generation) = crawled_fixture(temp.path());
std::fs::write(smart_dir(&generation).join("fetch-done"), pack).unwrap();
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, nginx) = start_nginx(&nginx_binary(), &generation);
let mut stream = send(
port,
&fetch_of_haves(&manifest, 0, true),
Some("203.0.113.9"),
);
let mut head = [0u8; 256];
stream.read(&mut head).unwrap();
thread::sleep(Duration::from_millis(500));
drop(stream);
drop(nginx);
let log = std::fs::read_to_string(
config
.output_root
.join("runtime")
.join(generation.file_name().unwrap())
.join("access.log"),
)
.unwrap();
log.lines()
.filter(|line| line.contains(" /_ks_git/"))
.map(|line| {
let after_status = line
.split('"')
.nth(2)
.expect("log fields after the request");
after_status
.split_whitespace()
.nth(1)
.and_then(|bytes| bytes.parse().ok())
.unwrap_or(0)
})
.max()
.expect("the loopback request is logged")
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_stalled_reader_does_not_spill_a_pack_into_a_temp_file() {
// the client's kernel buffers soak up a few MiB before anything stalls
let pack = vec![b'P'; 64 << 20];
let sent = loopback_bytes_for_a_stalled_reader(&pack);
assert!(
sent < 24 << 20,
"the loopback hop sent {sent} of {} bytes to a client that read none",
pack.len()
);
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_trusted_client_pulls_a_pack_at_its_own_rate_and_everyone_else_is_slowed() {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.serve.trusted = ["198.51.100.7", "2001:db8:ff::/48"]
.map(|client| IpPrefix::parse(client, "test").unwrap())
.into();
config.serve.trusted_rate = None;
// 8 MiB past PACK_RATE_AFTER is four seconds at 2 MB/s
std::fs::write(
smart_dir(&generation).join("fetch-done"),
vec![b'P'; 40 << 20],
)
.unwrap();
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
let pack = fetch_of_haves(&manifest, 0, true);
let pull = |client: Option<&str>| {
let started = Instant::now();
let mut stream = send(port, &pack, client);
let mut answer = Vec::new();
stream.read_to_end(&mut answer).unwrap();
assert!(
answer.len() > 40 << 20,
"{client:?} got {} bytes",
answer.len()
);
started.elapsed()
};
let untrusted = pull(Some("2001:db8:1:2::9"));
let trusted = pull(Some("2001:db8:ff::9"));
let loopback = pull(None);
assert!(
untrusted > Duration::from_secs(3),
"an untrusted client took {untrusted:?} for 8 MiB past the cap"
);
assert!(
trusted < Duration::from_secs(2) && loopback < Duration::from_secs(2),
"trusted {trusted:?}, loopback {loopback:?} must not be slowed"
);
let ls_refs = b"0014command=ls-refs\n0001".to_vec();
let started = Instant::now();
let (status, head) = ask(port, &ls_refs, Some("2001:db8:1:2::9"));
assert_eq!(status, 200, "a cheap reply is not a pack: {head}");
assert!(started.elapsed() < Duration::from_secs(2), "{head}");
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn the_pack_after_the_knot_wide_cap_is_told_to_retry() {
let temp = tempfile::tempdir().unwrap();
let (config, manifest, generation) = crawled_fixture(temp.path());
std::fs::write(
smart_dir(&generation).join("fetch-done"),
vec![b'P'; 64 << 20],
)
.unwrap();
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
let pack = fetch_of_haves(&manifest, 0, true);
let in_flight: u32 = config
.serve
.limits
.packs_in_flight
.expect("a cap by default")
.to_string()
.parse()
.unwrap();
let held: Vec<TcpStream> = (0..in_flight)
.map(|n| {
let mut stream = send(port, &pack, Some(&format!("203.0.113.{n}")));
let mut head = [0u8; 256];
stream.read(&mut head).unwrap();
stream
})
.collect();
let (status, head) = ask(port, &pack, Some("203.0.113.200"));
assert_eq!(status, 429, "one pack over the knot-wide cap: {head}");
assert!(
head.to_ascii_lowercase().contains("retry-after: 30"),
"a refused client is told when to come back: {head}"
);
drop(held);
}
#[test]
fn the_default_limits_render_the_numbers_this_knot_ships_with() {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.writes.enabled = true;
config.writes.fastcgi_socket = Some("unix:/dev/null".into());
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let conf = std::fs::read_to_string(generation.join("nginx/knot-static.conf")).unwrap();
for directive in [
"limit_req_zone $ks_client zone=ks_reads:10m rate=20r/s;",
"limit_req zone=ks_reads burst=50;",
"limit_conn ks_reads_conn 32;",
"limit_conn ks_packs_all 16;",
"limit_rate_after 32m;",
"limit_rate $ks_pack_rate;",
"default 2m;",
"limit_conn ks_blobs 2;",
"limit_rate_after 8m;",
"limit_rate 10m;",
"limit_req_zone $server_name zone=ks_inflate_all:1m rate=50r/s;",
"limit_req zone=ks_inflate_all burst=1500;",
"limit_rate $ks_handed_rate;",
"max_ranges 0;",
] {
assert!(conf.contains(directive), "{directive} missing from\n{conf}");
}
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_limit_turned_off_leaves_no_directive() {
let off: [(&str, fn(&mut ServeLimits), &str); 8] = [
(
"pack_rate",
|limits| limits.pack_rate = None,
"limit_rate $ks_pack_rate;",
),
(
"pack_rate_after",
|limits| limits.pack_rate_after = None,
"limit_rate_after 32m;",
),
(
"packs_in_flight",
|limits| limits.packs_in_flight = None,
"ks_packs_all",
),
("reads", |limits| limits.reads = None, "zone=ks_reads:10m"),
(
"read_connections",
|limits| limits.read_connections = None,
"ks_reads_conn",
),
(
"blob_connections",
|limits| limits.blob_connections = None,
"ks_blobs",
),
(
"blob_rate",
|limits| limits.blob_rate = None,
"limit_rate 10m;",
),
(
"inflate_rate_all",
|limits| limits.inflate_rate_all = None,
"zone=ks_inflate_all",
),
];
for (name, turn_off, directive) in off {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.writes.enabled = true;
config.writes.fastcgi_socket = Some("unix:/dev/null".into());
turn_off(&mut config.serve.limits);
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let conf = std::fs::read_to_string(generation.join("nginx/knot-static.conf")).unwrap();
assert!(
!conf.contains(directive),
"{name} off still emits {directive}"
);
let (_port, _nginx) = start_nginx(&nginx_binary(), &generation);
}
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_generations_files_are_revalidated_and_answer_304_unchanged() {
let temp = tempfile::tempdir().unwrap();
let (config, manifest, generation) = crawled_fixture(temp.path());
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
for target in [
"/",
"/favicon.svg",
"/favicon.ico",
"/apple-touch-icon.png",
"/og.png",
&format!("/{REPO}/info/refs?service=git-upload-pack"),
"/xrpc/com.atproto.sync.listRepos",
"/xrpc/org.tangled.knot.describeKnot",
"/xrpc/_health",
] {
let (status, head, _) = http(port, "GET", target, &[], b"");
assert_eq!(status, 200, "{target}: {head}");
let lower = head.to_ascii_lowercase();
assert!(
lower.contains("cache-control: no-cache"),
"{target} must be revalidated, not unstorable: {head}"
);
let etag = head
.lines()
.find(|line| line.to_ascii_lowercase().starts_with("etag: "))
.map(|line| line["etag: ".len()..].trim().to_string())
.unwrap_or_else(|| panic!("{target} has no etag: {head}"));
let (status, head, body) = http(port, "GET", target, &[("If-None-Match", &etag)], b"");
assert_eq!(status, 304, "{target} unchanged: {head}");
assert!(body.is_empty(), "{target}: {} bytes", body.len());
}
// blobs are content-addressed. they are the one thing cached for good
let (status, head, blob) = http(
port,
"GET",
&format!("/xrpc/com.atproto.sync.getBlob?did={REPO}&cid={BLOB_CID}"),
&[],
b"",
);
assert_eq!(status, 200, "the blob is served: {head}");
assert_eq!(blob, BLOB_BYTES);
let lower = head.to_ascii_lowercase();
assert!(
lower.contains("cache-control: public, max-age=31536000, immutable"),
"a blob never changes under its cid: {head}"
);
}
fn nginx_takes_the_config(generation: &Path) -> Result<(), String> {
let checked = generation.join("nginx/checked.conf");
let conf = format!(
"pid {};\nerror_log {};\n",
generation.join("nginx.pid").display(),
generation.join("nginx/error.log").display()
) + &std::fs::read_to_string(generation.join("nginx/knot-static.conf")).unwrap();
std::fs::write(&checked, conf).unwrap();
let output = std::process::Command::new(nginx_binary())
.args(["-t", "-c"])
.arg(&checked)
.output()
.unwrap();
if output.status.success() {
return Ok(());
}
Err(String::from_utf8_lossy(&output.stderr).into_owned())
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn nginx_takes_every_limit_at_its_boundaries() {
let size = |text: &str| crate::config::ByteSize::parse(text, "test").unwrap();
let rate = |text: &str| Some(crate::config::RequestRate::parse(text, "test").unwrap());
let count = |count: u32| crate::config::Connections::parse(count, "test").unwrap();
let boundaries: [(&str, ServeLimits); 4] = [
(
"the smallest of everything",
ServeLimits {
pack_rate: size("1"),
pack_rate_after: size("1"),
packs_in_flight: count(1),
reads: rate("1r/m"),
reads_burst: 0,
read_connections: count(1),
blob_connections: count(1),
blob_rate: size("1"),
blob_rate_after: size("1"),
inflate_rate_all: rate("1r/m"),
},
),
(
"the largest of everything",
ServeLimits {
pack_rate: size("8796093022207m"),
pack_rate_after: size("8796093022207m"),
packs_in_flight: count(65535),
reads: rate("1000000r/s"),
reads_burst: u32::MAX,
read_connections: count(65535),
blob_connections: count(65535),
blob_rate: size("8796093022207m"),
blob_rate_after: size("8796093022207m"),
inflate_rate_all: rate("1000000r/s"),
},
),
(
"kilobytes and a minute rate",
ServeLimits {
pack_rate: size("512k"),
pack_rate_after: size("1024k"),
reads: rate("4r/m"),
blob_rate: size("64K"),
..ServeLimits::default()
},
),
(
"everything off",
ServeLimits {
pack_rate: None,
pack_rate_after: None,
packs_in_flight: None,
reads: None,
reads_burst: 0,
read_connections: None,
blob_connections: None,
blob_rate: None,
blob_rate_after: None,
inflate_rate_all: None,
},
),
];
for (name, limits) in boundaries {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.writes.enabled = true;
config.writes.fastcgi_socket = Some("unix:/dev/null".into());
config.serve.limits = limits;
// a trusted rate reaches nginx through a map, which quietly reads an unparseable size as no limit
config.serve.trusted = [IpPrefix::parse("198.51.100.7", "test").unwrap()].into();
config.serve.trusted_rate = size("64k");
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
if let Err(refused) = nginx_takes_the_config(&generation) {
panic!("{name}: {refused}");
}
}
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn one_ipv6_64_is_one_client_and_a_read_is_not_a_pack() {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.serve.limits.reads = Some(crate::config::RequestRate::parse("1r/m", "test").unwrap());
config.serve.limits.reads_burst = 0;
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
let read = |client: &str| {
http(
port,
"GET",
"/xrpc/_health",
&[("X-Forwarded-For", client)],
b"",
)
.0
};
assert_eq!(read("2001:db8::1"), 200, "the first read of a /64");
assert_eq!(
read("2001:db8:0:0:1:2:3:4"),
429,
"the same /64 written out in full is the same client"
);
assert_eq!(read("2001:db8:1::1"), 200, "another /64 is another client");
let pack = fetch_of_haves(&manifest, 0, true);
let (status, head) = ask(port, &pack, Some("2001:db8::9"));
assert_eq!(status, 200, "a pack is not held to the read limit: {head}");
}
/// a range would let each slice run at line speed. the whole pack has to come back.
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_ranged_fetch_post_gets_the_whole_pack() {
let temp = tempfile::tempdir().unwrap();
let (config, manifest, generation) = crawled_fixture(temp.path());
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
let body = fetch_of_haves(&manifest, 0, true);
let (status, head, reply) = http(
port,
"POST",
&format!("/{REPO}/git-upload-pack"),
&[("Range", "bytes=0-3")],
&body,
);
assert_eq!(status, 200, "a ranged fetch is answered whole: {head}");
assert_eq!(reply, FULL_PACK);
}
#[test]
#[ignore = "requires nginx; set NGINX_BINARY"]
fn a_class_turned_off_is_not_limited_at_all() {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.serve.limits.reads = None;
config.serve.limits.read_connections = None;
config.serve.limits.blob_connections = None;
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let (port, _nginx) = start_nginx(&nginx_binary(), &generation);
for n in 0..30 {
let (status, head, _) = http(
port,
"GET",
"/xrpc/_health",
&[("X-Forwarded-For", "2001:db8::1")],
b"",
);
assert_eq!(status, 200, "read {n} of thirty in a row: {head}");
}
let (status, head, blob) = http(
port,
"GET",
&format!("/xrpc/com.atproto.sync.getBlob?did={REPO}&cid={BLOB_CID}"),
&[("X-Forwarded-For", "2001:db8::1")],
b"",
);
assert_eq!(status, 200, "{head}");
assert_eq!(blob, BLOB_BYTES);
}
/// the tls edge in ark answers 413 on its own past 1 MiB.
/// a route listed here over that must be raised there too.
#[test]
fn the_routes_that_take_a_body_say_how_much_the_edge_has_to_let_through() {
let temp = tempfile::tempdir().unwrap();
let (mut config, manifest, generation) = crawled_fixture(temp.path());
config.writes.enabled = true;
config.writes.fastcgi_socket = Some("unix:/dev/null".into());
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let conf = std::fs::read_to_string(generation.join("nginx/knot-static.conf")).unwrap();
assert_eq!(
body_limits(&conf),
[
(format!("location = /{REPO}/git-upload-pack"), "16m"),
(format!("location = /{REPO}/git-receive-pack"), "8g"),
// internal. the edge never sends this one
("location = /_ks_inflate".into(), "16m"),
// under 1 MiB. the edge refuses these first
(
"location = /xrpc/com.atproto.repo.createRecord".into(),
"64k"
),
("location = /xrpc/com.atproto.repo.putRecord".into(), "64k"),
(
"location = /xrpc/com.atproto.repo.deleteRecord".into(),
"64k"
),
(
"location = /xrpc/org.tangled.review.submitPatch".into(),
"64k"
),
("location = /xrpc/org.tangled.repo.createRepo".into(), "64k"),
("location = /xrpc/org.tangled.repo.deleteRepo".into(), "64k"),
(
"location = /xrpc/org.tangled.git.setDefaultBranch".into(),
"64k"
),
(
"location = /xrpc/org.tangled.git.deleteBranch".into(),
"64k"
),
(
"location = /xrpc/org.tangled.temp.git.deleteBranch".into(),
"64k"
),
// the edge allows 200m, a bit more than this. an oversize upload gets this 413
(
"location = /xrpc/com.atproto.repo.uploadBlob".into(),
"200000000",
),
("location = /_ks_push".into(), "8g"),
]
.map(|(location, limit)| (location, limit.to_string()))
);
assert!(
!conf.contains("\n client_max_body_size"),
"a server-wide limit would hide what a route takes:\n{conf}"
);
}
fn body_limits(conf: &str) -> Vec<(String, String)> {
let mut location = String::new();
let mut limits = Vec::new();
for line in conf.lines() {
let line = line.trim();
if let Some(head) = line.strip_suffix(" {")
&& head.starts_with("location ")
{
location = head.to_string();
}
if let Some(limit) = line.strip_prefix("client_max_body_size ") {
limits.push((location.clone(), limit.trim_end_matches(';').to_string()));
}
}
limits
}
#[test]
fn the_generation_is_served_with_sendfile_by_a_worker_per_core() {
let temp = tempfile::tempdir().unwrap();
let (config, manifest, generation) = crawled_fixture(temp.path());
crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();
let conf = std::fs::read_to_string(generation.join("nginx/knot-static.conf")).unwrap();
for directive in [
"worker_processes auto;",
"sendfile on;",
"tcp_nopush on;",
"send_timeout 30s;",
"buffer=64k flush=5s;",
] {
assert!(conf.contains(directive), "{directive} missing from\n{conf}");
}
}