challange: 9

This commit is contained in:
itsscb 2024-12-10 00:19:45 +01:00
parent ef5b3820e2
commit c8f3af6cf9
9 changed files with 483 additions and 341 deletions

View File

@ -5,14 +5,18 @@ edition = "2021"
[dependencies] [dependencies]
axum = { version = "0.7.4", features = ["query"] } axum = { version = "0.7.4", features = ["query"] }
cargo-manifest = "0.17.0"
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133" serde_json = "1.0.133"
serde_yml = "0.0.12"
shuttle-axum = "0.49.0" shuttle-axum = "0.49.0"
shuttle-runtime = "0.49.0" shuttle-runtime = "0.49.0"
tokio = "1.28.2" tokio = "1.28.2"
toml = "0.8.19" cargo-manifest = { version = "0.17.0", optional = true }
serde_yml = { version = "0.0.12", optional = true }
toml = { version = "0.8.19", optional = true }
[dev-dependencies] [dev-dependencies]
axum-test = "16.4.0" axum-test = "16.4.0"
[features]
default = []
task1-9 = ["cargo-manifest", "serde_yml", "toml"]

View File

@ -1,14 +1,23 @@
mod routes; mod routes;
use axum::routing::post; #[cfg(feature = "task1-9")]
pub use routes::hello_world; use routes::{
use routes::{hello_bird, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, minus_one}; hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one,
refill, MilkFactory,
};
pub fn router() -> axum::Router { pub fn router() -> axum::Router {
#[cfg(feature = "task1-9")]
use axum::routing::get; use axum::routing::get;
#[cfg(feature = "task1-9")]
use axum::routing::post;
use axum::Router; use axum::Router;
Router::new() #[cfg(feature = "task1-9")]
let milk_factory = MilkFactory::new();
#[cfg(feature = "task1-9")]
return Router::new()
.route("/hello_world", get(hello_world)) .route("/hello_world", get(hello_world))
.route("/-1/seek", get(minus_one)) .route("/-1/seek", get(minus_one))
.route("/2/dest", get(ipv4_dest)) .route("/2/dest", get(ipv4_dest))
@ -16,338 +25,11 @@ pub fn router() -> axum::Router {
.route("/2/v6/dest", get(ipv6_dest)) .route("/2/v6/dest", get(ipv6_dest))
.route("/2/v6/key", get(ipv6_key)) .route("/2/v6/key", get(ipv6_key))
.route("/5/manifest", post(manifest)) .route("/5/manifest", post(manifest))
.route("/", get(hello_bird)) .route("/9/milk", post(milk))
} .route("/9/refill", post(refill))
.with_state(milk_factory)
#[cfg(test)] .route("/", get(hello_bird));
mod test {
use super::*; // #[cfg(feature="task12")]
use axum::http::StatusCode; Router::new()
use axum_test::TestServer;
fn test_server() -> TestServer {
TestServer::new(router()).unwrap()
}
#[tokio::test]
async fn test_hello_world() {
let server = test_server();
let response = server.get("/hello_world").await;
response.assert_status_ok();
response.assert_text("Hello, world!");
}
#[tokio::test]
async fn test_hello_bird() {
let server = test_server();
let response = server.get("/").await;
response.assert_status_ok();
response.assert_text("Hello, bird!");
}
#[tokio::test]
async fn test_minus_one() {
let server = test_server();
let response = server.get("/-1/seek").await;
response.assert_header("location", "https://www.youtube.com/watch?v=9Gc4QTqslN4");
response.assert_status(StatusCode::FOUND);
}
#[tokio::test]
async fn test_ipv4_dest() {
let server = test_server();
let response = server.get("/2/dest?from=10.0.0.0&key=1.2.3.255").await;
response.assert_status_ok();
response.assert_text("11.2.3.255");
let response = server.get("/2/dest?from=invalid-from&key=1.2.3.255").await;
response.assert_status_bad_request();
let response = server.get("/2/dest?from=10.0.0.0&key=invalid-key").await;
response.assert_status_bad_request();
let response = server
.get("/2/dest?from=128.128.33.0&key=255.0.255.33")
.await;
response.assert_status_ok();
response.assert_text("127.128.32.33");
}
#[tokio::test]
async fn test_ipv4_key() {
let server = test_server();
let response = server.get("/2/key?from=10.0.0.0&to=11.2.3.255").await;
response.assert_status_ok();
response.assert_text("1.2.3.255");
let response = server.get("/2/key?from=invalid-from&to=1.2.3.255").await;
response.assert_status_bad_request();
let response = server.get("/2/key?from=10.0.0.0&to=invalid-to").await;
response.assert_status_bad_request();
let response = server
.get("/2/key?from=128.128.33.0&to=127.128.32.33")
.await;
response.assert_status_ok();
response.assert_text("255.0.255.33");
}
#[tokio::test]
async fn test_ipv6_dest() {
let server = test_server();
let response = server.get("/2/v6/dest?from=fe80::1&key=5:6:7::3333").await;
response.assert_status_ok();
response.assert_text("fe85:6:7::3332");
let response = server
.get("/2/v6/dest?from=invalid-from&key=5:6:7::3333")
.await;
response.assert_status_bad_request();
let response = server.get("/2/v6/dest?from=fe80::1&key=invalid-key").await;
response.assert_status_bad_request();
}
#[tokio::test]
async fn test_ipv6_key() {
let server = test_server();
let response = server
.get("/2/v6/key?from=aaaa::aaaa&to=5555:ffff:c:0:0:c:1234:5555")
.await;
response.assert_status_ok();
response.assert_text("ffff:ffff:c::c:1234:ffff");
let response = server
.get("/2/v6/dest?from=invalid-from&to=5:6:7::3333")
.await;
response.assert_status_bad_request();
let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await;
response.assert_status_bad_request();
}
#[tokio::test]
async fn test_manifest_1() {
let server = test_server();
let want = "Toy car: 2\nLego brick: 230";
let payload = r#"[package]
name = "not-a-gift-order"
authors = ["Not Santa"]
keywords = ["Christmas 2024"]
[[package.metadata.orders]]
item = "Toy car"
quantity = 2
[[package.metadata.orders]]
item = "Lego brick"
quantity = 230"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status_ok();
response.assert_text(want);
let payload = r#"[package]
name = "coal-in-a-bowl"
authors = ["H4CK3R_13E7"]
keywords = ["Christmas 2024"]
[[package.metadata.orders]]
item = "Coal"
quantity = "Hahaha get rekt""#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status(StatusCode::NO_CONTENT);
}
#[tokio::test]
async fn test_manifest_2() {
let server = test_server();
let payload = r#"[package]
name = false
authors = ["Not Santa"]
keywords = ["Christmas 2024"]"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status(StatusCode::BAD_REQUEST);
let payload = r#"[package]
name = "not-a-gift-order"
authors = ["Not Santa"]
keywords = ["Christmas 2024"]
[profile.release]
incremental = "stonks""#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status(StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_manifest_3() {
let server = test_server();
let want = "Toy car: 2\nLego brick: 230";
let payload = r#"[package]
name = "not-a-gift-order"
authors = ["Not Santa"]
keywords = ["Christmas 2024"]
[[package.metadata.orders]]
item = "Toy car"
quantity = 2
[[package.metadata.orders]]
item = "Lego brick"
quantity = 230"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status_ok();
response.assert_text(want);
let payload = r#"[package]
name = "not-a-gift-order"
authors = ["Not Santa"]
[[package.metadata.orders]]
item = "Toy car"
quantity = 2
[[package.metadata.orders]]
item = "Lego brick"
quantity = 230"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status(StatusCode::BAD_REQUEST);
response.assert_text("Magic keyword not provided");
let payload = r#"[package]
name = "grass"
authors = ["A vegan cow"]
keywords = ["Moooooo"]"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status(StatusCode::BAD_REQUEST);
response.assert_text("Magic keyword not provided");
let payload = r#"[package]
name = "grass"
authors = ["A vegan cow"]
keywords = ["Christmas 2024"]"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/toml")
.await;
response.assert_status(StatusCode::NO_CONTENT);
}
#[tokio::test]
async fn test_manifest_4() {
let server = test_server();
let payload = r#"[package]
name = "grass"
authors = ["A vegan cow"]
keywords = ["Christmas 2024"]"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("text/html")
.await;
response.assert_status(StatusCode::UNSUPPORTED_MEDIA_TYPE);
let want = "Toy train: 5";
let payload = r#"package:
name: big-chungus-sleigh
version: "2.0.24"
metadata:
orders:
- item: "Toy train"
quantity: 5
rust-version: "1.69"
keywords:
- "Christmas 2024""#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/yaml")
.await;
response.assert_status_ok();
response.assert_text(want);
let want = "Toy train: 5";
let payload = r#"{
"package": {
"name": "big-chungus-sleigh",
"version": "2.0.24",
"metadata": {
"orders": [
{
"item": "Toy train",
"quantity": 5
}
]
},
"rust-version": "1.69",
"keywords": [
"Christmas 2024"
]
}
}"#;
let response = server
.post("/5/manifest")
.text(payload)
.content_type("application/json")
.await;
response.assert_status_ok();
response.assert_text(want);
}
} }

View File

@ -1,7 +1,9 @@
#![cfg(feature = "task1-9")]
mod hello_bird; mod hello_bird;
mod hello_world; mod hello_world;
mod minus_one; mod minus_one;
mod task_five; mod task_five;
mod task_nine;
mod task_two; mod task_two;
pub use hello_bird::hello_bird; pub use hello_bird::hello_bird;
@ -12,3 +14,5 @@ pub use task_two::ipv4_dest;
pub use task_two::ipv4_key; pub use task_two::ipv4_key;
pub use task_two::ipv6_dest; pub use task_two::ipv6_dest;
pub use task_two::ipv6_key; pub use task_two::ipv6_key;
pub use task_nine::{milk, refill, MilkFactory};

View File

@ -0,0 +1,47 @@
use axum::{
extract::State,
http::{header::CONTENT_TYPE, HeaderMap, StatusCode},
response::IntoResponse,
};
use super::{MilkFactory, Unit};
#[allow(clippy::unused_async)]
pub async fn refill(State(milk_factory): State<MilkFactory>) -> impl IntoResponse {
milk_factory.magic_refill();
String::new().into_response()
}
#[allow(clippy::unused_async)]
pub async fn milk(
State(milk_factory): State<MilkFactory>,
headers: HeaderMap,
payload: String,
) -> Result<String, impl IntoResponse> {
headers.get(CONTENT_TYPE).map_or_else(
|| match milk_factory.withdraw() {
Ok(message) => Ok(message.to_string()),
Err(message) => Err(message.into_response()),
},
|content_type| match content_type.to_str().unwrap() {
"application/json" => Unit::from_json(&payload).map_or_else(
|_| {
let _ = milk_factory.withdraw();
Err((StatusCode::BAD_REQUEST, "").into_response())
},
|unit| match unit {
Unit::Liters(_) | Unit::Gallons(_) | Unit::Litres(_) | Unit::Pints(_) => {
match milk_factory.withdraw() {
Ok(_) => Ok(unit.json().unwrap()),
Err(message) => Err(message.into_response()),
}
}
},
),
_ => match milk_factory.withdraw() {
Ok(message) => Ok(message.to_string()),
Err(message) => Err(message.into_response()),
},
},
)
}

View File

@ -0,0 +1,153 @@
use std::{
fmt::{Display, Formatter},
sync::{atomic::AtomicU32, Arc},
thread,
};
use axum::{http::StatusCode, response::IntoResponse};
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
#[derive(Debug, Clone)]
pub struct MilkFactory {
stock: Arc<AtomicU32>,
}
impl MilkFactory {
pub fn new() -> Self {
let mf = Self {
stock: Arc::new(AtomicU32::new(5)),
};
mf.run();
mf
}
pub fn run(&self) {
thread::spawn({
let stock = self.stock.clone();
move || {
let mut start = std::time::Instant::now();
loop {
if start.elapsed().as_millis() >= 990 {
let current_stock = stock.load(std::sync::atomic::Ordering::Relaxed);
if current_stock < 5 {
stock.store(current_stock + 1, std::sync::atomic::Ordering::Relaxed);
}
start = std::time::Instant::now();
}
}
}
});
}
pub fn magic_refill(&self) {
self.stock.store(5, std::sync::atomic::Ordering::Relaxed);
}
pub fn withdraw(&self) -> Result<MilkMessage, MilkMessage> {
let current_stock = self.stock.load(std::sync::atomic::Ordering::Relaxed);
if current_stock < 1 {
return Err(MilkMessage::NoMilkAvailable);
}
self.stock
.store(current_stock - 1, std::sync::atomic::Ordering::Relaxed);
Ok(MilkMessage::Withdraw)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum MilkMessage {
Withdraw,
Refill,
NoMilkAvailable,
WithdrawUnit(Unit),
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq)]
pub enum Unit {
#[serde(rename(deserialize = "liters", serialize = "gallons"))]
Liters(f32),
#[serde(rename(deserialize = "gallons", serialize = "liters"))]
Gallons(f32),
#[serde(rename(deserialize = "litres", serialize = "pints"))]
Litres(f32),
#[serde(rename(deserialize = "pints", serialize = "litres"))]
Pints(f32),
}
impl Unit {
pub fn from_json(json: &str) -> Result<Self, serde_json::error::Error> {
serde_json::from_str(json)
}
pub fn json(self) -> Result<String, serde_json::error::Error> {
serde_json::to_string(&self)
}
}
impl Serialize for Unit {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Unit", 1)?;
match self {
Self::Liters(value) => {
state.serialize_field::<f32>("gallons", &(*value * 0.264_172_06))?;
}
Self::Gallons(value) => {
state.serialize_field::<f32>("liters", &(*value * 3.785_412))?;
}
Self::Litres(value) => {
state.serialize_field::<f32>("pints", &(*value * 1.759_754))?;
}
Self::Pints(value) => {
state.serialize_field::<f32>("litres", &(*value * 0.568_261))?;
}
}
state.end()
}
}
#[test]
fn test_unit() {
let unit = Unit::Liters(5.0);
assert_eq!(
serde_json::to_string(&unit).unwrap(),
r#"{"gallons":1.3208603}"#
);
let unit = Unit::from_json(serde_json::json!({"liters": 5.0}).to_string().as_str()).unwrap();
assert_eq!(unit, Unit::Liters(5.0));
assert_eq!(unit.json().unwrap(), r#"{"gallons":1.3208603}"#);
let unit = Unit::from_json(
serde_json::json!({"gallons": -2.000_000_000_000_001})
.to_string()
.as_str(),
)
.unwrap();
assert_eq!(unit, Unit::Gallons(-2.000_000_000_000_001));
assert_eq!(unit.json().unwrap(), r#"{"liters":-7.570824}"#);
}
impl Display for MilkMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Withdraw => writeln!(f, "Milk withdrawn"),
Self::Refill => writeln!(f, "Refilled milk"),
Self::NoMilkAvailable => writeln!(f, "No milk available"),
Self::WithdrawUnit(u) => write!(f, "{}", u.json().unwrap()),
}
}
}
impl IntoResponse for MilkMessage {
fn into_response(self) -> axum::response::Response {
match self {
Self::Withdraw | Self::WithdrawUnit(_) => (StatusCode::OK, self.to_string()),
Self::Refill => (StatusCode::OK, String::new()),
Self::NoMilkAvailable => (StatusCode::TOO_MANY_REQUESTS, self.to_string()),
}
.into_response()
}
}

View File

@ -0,0 +1,6 @@
mod milk;
mod milk_factory;
pub use milk::{milk, refill};
pub use milk_factory::{MilkFactory, Unit};

38
tests/minus_one/main.rs Normal file
View File

@ -0,0 +1,38 @@
#[cfg(all(test, feature = "task1-9"))]
mod minus_one {
use axum::http::StatusCode;
use axum_test::TestServer;
use itsscb_shuttlings_cch24::router;
fn test_server() -> TestServer {
TestServer::new(router()).unwrap()
}
#[tokio::test]
async fn test_hello_world() {
let server = test_server();
let response = server.get("/hello_world").await;
response.assert_status_ok();
response.assert_text("Hello, world!");
}
#[tokio::test]
async fn test_hello_bird() {
let server = test_server();
let response = server.get("/").await;
response.assert_status_ok();
response.assert_text("Hello, bird!");
}
#[tokio::test]
async fn test_minus_one() {
let server = test_server();
let response = server.get("/-1/seek").await;
response.assert_header("location", "https://www.youtube.com/watch?v=9Gc4QTqslN4");
response.assert_status(StatusCode::FOUND);
}
}

121
tests/task_five/main.rs Normal file
View File

@ -0,0 +1,121 @@
#[cfg(all(test, feature = "task1-9"))]
mod task_five {
use std::thread;
use std::time::Duration;
use axum::http::StatusCode;
use axum_test::TestServer;
use itsscb_shuttlings_cch24::router;
fn test_server() -> TestServer {
TestServer::new(router()).unwrap()
}
#[tokio::test]
async fn test_milk() {
let sever = test_server();
let response = sever.post("/9/milk").await;
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
let response = sever.post("/9/milk").await;
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
let response = sever.post("/9/milk").await;
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
let response = sever.post("/9/milk").await;
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
let response = sever.post("/9/milk").await;
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
let response = sever.post("/9/milk").await;
response.assert_status(StatusCode::TOO_MANY_REQUESTS);
let sever = test_server();
for i in 0..=10 {
let response = sever.post("/9/milk").await;
match i {
0..=4 | 6 | 8 | 9 => {
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
}
5 | 7 | 10 => {
response.assert_status(StatusCode::TOO_MANY_REQUESTS);
response.assert_text("No milk available\n");
match i {
5 => thread::sleep(Duration::from_secs(1)),
7 => thread::sleep(Duration::from_secs(2)),
_ => (),
}
}
_ => {
response.assert_status(StatusCode::SERVICE_UNAVAILABLE);
response.assert_text("No milk available\n");
}
}
}
let sever = test_server();
let response = sever
.post("/9/milk")
.text(r#"{"liters":5}"#)
.content_type("application/json")
.await;
response.assert_status_ok();
response.assert_text(r#"{"gallons":1.3208603}"#);
let sever = test_server();
let response = sever
.post("/9/milk")
.text(r#"{"gallons":5}"#)
.content_type("application/json")
.await;
response.assert_status_ok();
response.assert_text(r#"{"liters":18.927061}"#);
let sever = test_server();
let response = sever
.post("/9/milk")
.text(r#"{"liters":1, "gallons":5}"#)
.content_type("application/json")
.await;
response.assert_status_bad_request();
let response = sever
.post("/9/milk")
.text(r#"{"litres":2}"#)
.content_type("application/json")
.await;
response.assert_status_ok();
response.assert_text(r#"{"pints":3.519508}"#);
let sever = test_server();
for i in 0..=11 {
let response = sever.post("/9/milk").await;
match i {
0..=4 | 6..=10 => {
response.assert_status_ok();
response.assert_text("Milk withdrawn\n");
}
5 | 11 => {
response.assert_status(StatusCode::TOO_MANY_REQUESTS);
response.assert_text("No milk available\n");
let response = sever.post("/9/refill").await;
response.assert_status_ok();
}
_ => {
response.assert_status(StatusCode::SERVICE_UNAVAILABLE);
response.assert_text("No milk available\n");
}
}
}
}
}

87
tests/task_two/main.rs Normal file
View File

@ -0,0 +1,87 @@
#[cfg(all(test, feature = "task1-9"))]
mod task_two {
use axum_test::TestServer;
use itsscb_shuttlings_cch24::router;
fn test_server() -> TestServer {
TestServer::new(router()).unwrap()
}
#[tokio::test]
async fn test_ipv4_dest() {
let server = test_server();
let response = server.get("/2/dest?from=10.0.0.0&key=1.2.3.255").await;
response.assert_status_ok();
response.assert_text("11.2.3.255");
let response = server.get("/2/dest?from=invalid-from&key=1.2.3.255").await;
response.assert_status_bad_request();
let response = server.get("/2/dest?from=10.0.0.0&key=invalid-key").await;
response.assert_status_bad_request();
let response = server
.get("/2/dest?from=128.128.33.0&key=255.0.255.33")
.await;
response.assert_status_ok();
response.assert_text("127.128.32.33");
}
#[tokio::test]
async fn test_ipv4_key() {
let server = test_server();
let response = server.get("/2/key?from=10.0.0.0&to=11.2.3.255").await;
response.assert_status_ok();
response.assert_text("1.2.3.255");
let response = server.get("/2/key?from=invalid-from&to=1.2.3.255").await;
response.assert_status_bad_request();
let response = server.get("/2/key?from=10.0.0.0&to=invalid-to").await;
response.assert_status_bad_request();
let response = server
.get("/2/key?from=128.128.33.0&to=127.128.32.33")
.await;
response.assert_status_ok();
response.assert_text("255.0.255.33");
}
#[tokio::test]
async fn test_ipv6_dest() {
let server = test_server();
let response = server.get("/2/v6/dest?from=fe80::1&key=5:6:7::3333").await;
response.assert_status_ok();
response.assert_text("fe85:6:7::3332");
let response = server
.get("/2/v6/dest?from=invalid-from&key=5:6:7::3333")
.await;
response.assert_status_bad_request();
let response = server.get("/2/v6/dest?from=fe80::1&key=invalid-key").await;
response.assert_status_bad_request();
}
#[tokio::test]
async fn test_ipv6_key() {
let server = test_server();
let response = server
.get("/2/v6/key?from=aaaa::aaaa&to=5555:ffff:c:0:0:c:1234:5555")
.await;
response.assert_status_ok();
response.assert_text("ffff:ffff:c::c:1234:ffff");
let response = server
.get("/2/v6/dest?from=invalid-from&to=5:6:7::3333")
.await;
response.assert_status_bad_request();
let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await;
response.assert_status_bad_request();
}
}