From 0d1a1c152d31aec834e09f0aeb86a0c8e63aacfd Mon Sep 17 00:00:00 2001
From: itsscb <dev@itsscb.de>
Date: Wed, 25 Dec 2024 23:06:28 +0100
Subject: [PATCH] challenge: 23

---
 Cargo.toml                              |  10 +-
 Shuttle.toml                            |   2 +
 assets/23.html                          | 277 ++++++++++++++++++++++++
 src/lib.rs                              |  50 +++--
 src/main.rs                             |  10 +
 src/routes/mod.rs                       |   3 +
 src/routes/task_twentythree/lockfile.rs | 100 +++++++++
 src/routes/task_twentythree/mod.rs      |  11 +
 src/routes/task_twentythree/ornament.rs |  64 ++++++
 src/routes/task_twentythree/present.rs  |  79 +++++++
 src/routes/task_twentythree/star.rs     |   7 +
 tests/task_nineteen/main.rs             |   2 +-
 12 files changed, 593 insertions(+), 22 deletions(-)
 create mode 100644 Shuttle.toml
 create mode 100644 assets/23.html
 create mode 100644 src/routes/task_twentythree/lockfile.rs
 create mode 100644 src/routes/task_twentythree/mod.rs
 create mode 100644 src/routes/task_twentythree/ornament.rs
 create mode 100644 src/routes/task_twentythree/present.rs
 create mode 100644 src/routes/task_twentythree/star.rs

diff --git a/Cargo.toml b/Cargo.toml
index bc66c97..9ef1b53 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,14 +29,18 @@ sqlx = { version = "0.8.2", features = [
     "migrate",
 ] }
 uuid = { version = "1.11.0", features = ["v4"] }
+tower-http = { version = "0.6.2", features = ["fs"], optional = true }
+html-escape = "0.2.13"
+regex = "1.11.1"
+hex = "0.4.3"
 
 [dev-dependencies]
 axum-test = "16.4.0"
 
 [features]
-default = ["shuttle-shared-db"]
+default = ["tower-http", "toml"]
 task1-9 = ["cargo-manifest", "serde_yml", "toml"]
 task12 = ["rand"]
 task16 = ["jsonwebtoken"]
-task19 = []
-task23 = []
+task19 = ["shuttle-shared-db"]
+task23 = ["tower-http", "toml"]
diff --git a/Shuttle.toml b/Shuttle.toml
new file mode 100644
index 0000000..6510119
--- /dev/null
+++ b/Shuttle.toml
@@ -0,0 +1,2 @@
+[build]
+assets = ["assets/*"]
diff --git a/assets/23.html b/assets/23.html
new file mode 100644
index 0000000..b0bdedb
--- /dev/null
+++ b/assets/23.html
@@ -0,0 +1,277 @@
+<html>
+    <head>
+        <script src="https://unpkg.com/htmx.org@2.0.4"></script>
+        <style>
+body {
+    --darkgrey: #0d0d0d;
+    --red: #a00;
+    --green: #060;
+    --white: #eee;
+    background-color: var(--darkgrey);
+    color: var(--white);
+}
+main {
+    max-width: 600px;
+    margin: auto;
+    margin-top: 100px;
+}
+
+.tree {
+    position: relative;
+    height: 500px;
+}
+.tree * {
+    position: absolute;
+}
+#star {
+    top: -20px;
+    left: 150px;
+    width: 100px;
+    height: 100px;
+    background-color: darkgoldenrod;
+    clip-path: polygon(
+        50% 0%,
+        61% 35%,
+        98% 35%,
+        68% 57%,
+        79% 91%,
+        50% 70%,
+        21% 91%,
+        32% 57%,
+        2% 35%,
+        39% 35%
+    );
+}
+#star.lit {
+    background: linear-gradient(10deg, rgba(227,233,0,1) 0%, rgba(241,245,148,1) 100%);
+}
+.tree-part {
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-color: transparent transparent green transparent;
+    border-width: 0 100px 100px 100px;
+}
+.tree-part1 {
+    left: 100px;
+    top: 20px;
+}
+.tree-part2 {
+    left: 100px;
+    top: 60px;
+    transform: scale(120%);
+    filter: brightness(90%);
+}
+.tree-part3 {
+    left: 100px;
+    top: 100px;
+    transform: scale(135%);
+}
+.tree-part4 {
+    left: 100px;
+    top: 140px;
+    transform: scale(150%);
+    filter: brightness(90%);
+}
+.tree-part5 {
+    left: 100px;
+    top: 180px;
+    transform: scale(160%);
+}
+.tree-base {
+    width: 40px;
+    height: 40px;
+    left: 180px;
+    top: 310px;
+    background-color: rgb(97, 47, 13);
+    margin: 0 auto;
+}
+.ornament {
+    width: 20px;
+    height: 20px;
+    background-color: darkred;
+    border-radius: 50%;
+    transition: background-color 1s ease;
+}
+.ornament.on {
+    background-color: red;
+}
+#ornament1 {
+    top: 90px;
+    left: 150px;
+}
+#ornament2 {
+    top: 140px;
+    left: 210px;
+}
+#ornament3 {
+    top: 172px;
+    left: 160px;
+}
+#ornament4 {
+    top: 253px;
+    left: 110px;
+}
+#ornament5 {
+    top: 200px;
+    left: 260px;
+}
+#ornament6 {
+    top: 230px;
+    left: 200px;
+}
+#ornament7 {
+    top: 270px;
+    left: 250px;
+}
+.present {
+    width: 100px;
+    height: 100px;
+    box-shadow: black 0px 0px 11px 1px;
+}
+.present.red {
+    background-color: red;
+}
+.present.blue {
+    background-color: blue;
+}
+.present.purple {
+    background-color: purple;
+}
+.present:nth-child(1) {
+    top: 280px;
+    left: 350px;
+}
+.present:nth-child(2) {
+    top: 280px;
+    left: 460px;
+}
+.present:nth-child(3) {
+    top: 320px;
+    left: 420px;
+}
+.present .ribbon {
+    background: rgb(227,233,0);
+    background: linear-gradient(0deg, rgba(227,233,0,1) 0%, rgba(241,245,148,1) 100%);
+}
+.present .ribbon:nth-child(1) {
+    width: 100px;
+    height: 20px;
+    top: 40px;
+}
+.present .ribbon:nth-child(2) {
+    width: 20px;
+    height: 100px;
+    left: 40px;
+}
+.present .ribbon:nth-child(3) {
+    width: 20px;
+    height: 30px;
+    left: 30px;
+    top: -20px;
+    transform: rotate(-45deg);
+}
+.present .ribbon:nth-child(4) {
+    width: 20px;
+    height: 30px;
+    left: 50px;
+    top: -20px;
+    transform: rotate(35deg);
+}
+#switch {
+    top: 400px;
+    left: 145px;
+    border: none;
+    background-color: #ccc;
+    color: black;
+    padding: 1em;
+    border-radius: .5em;
+    cursor: pointer;
+    font-weight: bold;
+}
+.text {
+    font-size: 200%;
+    font-weight: bold;
+    text-align: center;
+}
+.rocket {
+    width: 80px;
+    height: 80px;
+}
+.spacer {
+    height: 800px;
+}
+#lockfilecanvas {
+    height: 276px;
+    width: 276px;
+    margin: auto;
+    border: 1px solid grey;
+    margin-bottom: 100px;
+    position: relative;
+}
+#lockfilecanvas * {
+    position: absolute;
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+}
+        </style>
+    </head>
+    <body>
+        <main>
+            <div class="tree">
+                <div class="present red" hx-get="/23/present/blue" hx-swap="outerHTML">
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                </div>
+                <div class="present red" hx-get="/23/present/blue" hx-swap="outerHTML">
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                </div>
+                <div class="present red" hx-get="/23/present/blue" hx-swap="outerHTML">
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                    <div class="ribbon"></div>
+                </div>
+                <div class="tree-base"></div>
+                <div class="tree-part tree-part5"></div>
+                <div class="tree-part tree-part4"></div>
+                <div class="tree-part tree-part3"></div>
+                <div class="tree-part tree-part2"></div>
+                <div class="tree-part tree-part1"></div>
+                <div class="ornament" id="ornament1" hx-trigger="load changed delay:2000ms once" hx-get="/23/ornament/on/1" hx-swap="outerHTML"></div>
+                <div class="ornament" id="ornament2" hx-trigger="load changed delay:1600ms once" hx-get="/23/ornament/on/2" hx-swap="outerHTML"></div>
+                <div class="ornament" id="ornament3" hx-trigger="load changed delay:200ms once" hx-get="/23/ornament/on/3" hx-swap="outerHTML"></div>
+                <div class="ornament" id="ornament4" hx-trigger="load changed delay:400ms once" hx-get="/23/ornament/on/4" hx-swap="outerHTML"></div>
+                <div class="ornament" id="ornament5" hx-trigger="load changed delay:800ms once" hx-get="/23/ornament/on/5" hx-swap="outerHTML"></div>
+                <div class="ornament" id="ornament6" hx-trigger="load changed delay:1200ms once" hx-get="/23/ornament/on/6" hx-swap="outerHTML"></div>
+                <div class="ornament" id="ornament7" hx-trigger="load changed delay:1400ms once" hx-get="/23/ornament/on/7" hx-swap="outerHTML"></div>
+                <div class="present-wrap present-wrap1"></div>
+                <div class="present-wrap present-wrap2"></div>
+                <div class="present-wrap present-wrap3"></div>
+                <div class="present-wrap present-wrap4"></div>
+                <div id="star"></div>
+                <button id="switch" hx-get="/23/star" hx-swap="outerHTML" hx-target="#star">
+                    Light the star
+                </button>
+            </div>
+            <div class="text">Merry Christmas!</div>
+            <div class="text">/ Shuttle</div>
+            <div class="text"><img class="rocket" src="https://console.shuttle.dev/images/rocket.gif"></div>
+            <div class="spacer"></div>
+            <div class="text">Bonus task:</div>
+            <form hx-post="/23/lockfile" enctype="multipart/form-data" hx-target="#lockfilecanvas">
+                <input type="file" name="lockfile" required>
+                <br>
+                <br>
+                <button type="submit">Submit lockfile</button>
+            </form>
+            <div id="lockfilecanvas"></div>
+        </main>
+    </body>
+</html>
diff --git a/src/lib.rs b/src/lib.rs
index 3166b4e..a191cd6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,6 @@
 mod routes;
 
-use axum::routing::{delete, put};
+#[cfg(feature = "task19")]
 use routes::task_nineteen::{cite, draft, list, remove, reset, undo};
 #[cfg(feature = "task12")]
 use routes::{board, place, random_board, reset, Board};
@@ -11,19 +11,37 @@ use routes::{
     hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one,
     refill, MilkFactory,
 };
+use tower_http::services::ServeDir;
 
+#[allow(unused_imports)]
+use axum::{
+    routing::{delete, get, post, put},
+    Router,
+};
+#[cfg(feature = "task19")]
 #[allow(unused_imports)]
 pub fn router(pool: Option<sqlx::PgPool>) -> axum::Router {
-    use axum::{
-        routing::{get, post},
-        Router,
-    };
+    return pool.map_or_else(Router::new, |pool| {
+        Router::new()
+            .route("/19/reset", post(reset))
+            .route("/19/draft", post(draft))
+            .route("/19/undo/:id", put(undo))
+            .route("/19/remove/:id", delete(remove))
+            .route("/19/cite/:id", get(cite))
+            .route("/19/list", get(list))
+            .with_state(pool)
+    });
+}
+
+#[cfg(not(feature = "task19"))]
+pub fn router() -> axum::Router {
+    use routes::task_twentythree::{lockfile, ornament, present, star};
 
     #[cfg(feature = "task1-9")]
     let milk_factory = MilkFactory::new();
 
     #[cfg(feature = "task1-9")]
-    return Router::new()
+    return return Router::new()
         .route("/hello_world", get(hello_world))
         .route("/-1/seek", get(minus_one))
         .route("/2/dest", get(ipv4_dest))
@@ -37,7 +55,7 @@ pub fn router(pool: Option<sqlx::PgPool>) -> axum::Router {
         .route("/", get(hello_bird));
 
     #[cfg(feature = "task12")]
-    Router::new()
+    return Router::new()
         .route("/12/board", get(board))
         .route("/12/reset", post(reset))
         .route("/12/place/:team/:column", post(place))
@@ -45,19 +63,15 @@ pub fn router(pool: Option<sqlx::PgPool>) -> axum::Router {
         .with_state(Board::new());
 
     #[cfg(feature = "task16")]
-    Router::new()
+    return Router::new()
         .route("/16/wrap", post(wrap))
         .route("/16/unwrap", get(unwrap))
         .route("/16/decode", post(decode));
 
-    pool.map_or_else(Router::new, |pool| {
-        Router::new()
-            .route("/19/reset", post(reset))
-            .route("/19/draft", post(draft))
-            .route("/19/undo/:id", put(undo))
-            .route("/19/remove/:id", delete(remove))
-            .route("/19/cite/:id", get(cite))
-            .route("/19/list", get(list))
-            .with_state(pool)
-    })
+    Router::new()
+        .route("/23/star", get(star))
+        .route("/23/lockfile", post(lockfile))
+        .route("/23/present/:color", get(present))
+        .route("/23/ornament/:state/:id", get(ornament))
+        .nest_service("/assets/", ServeDir::new("assets"))
 }
diff --git a/src/main.rs b/src/main.rs
index 880008e..c672b36 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,6 +9,7 @@ async fn main() -> shuttle_axum::ShuttleAxum {
     Ok(router.into())
 }
 
+#[cfg(feature = "task19")]
 #[shuttle_runtime::main]
 #[allow(clippy::unused_async)]
 async fn main(#[shuttle_shared_db::Postgres] pool: sqlx::PgPool) -> shuttle_axum::ShuttleAxum {
@@ -21,3 +22,12 @@ async fn main(#[shuttle_shared_db::Postgres] pool: sqlx::PgPool) -> shuttle_axum
 
     Ok(router.into())
 }
+
+#[cfg(not(feature = "task19"))]
+#[shuttle_runtime::main]
+#[allow(clippy::unused_async)]
+async fn main() -> shuttle_axum::ShuttleAxum {
+    let router = router();
+
+    Ok(router.into())
+}
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
index b9607ef..b317467 100644
--- a/src/routes/mod.rs
+++ b/src/routes/mod.rs
@@ -8,8 +8,11 @@ mod task_sixteen;
 #[cfg(feature = "task16")]
 pub use task_sixteen::{decode, unwrap, wrap};
 
+#[cfg(feature = "task19")]
 pub mod task_nineteen;
 
+pub mod task_twentythree;
+
 #[cfg(feature = "task1-9")]
 mod hello_bird;
 
diff --git a/src/routes/task_twentythree/lockfile.rs b/src/routes/task_twentythree/lockfile.rs
new file mode 100644
index 0000000..4424015
--- /dev/null
+++ b/src/routes/task_twentythree/lockfile.rs
@@ -0,0 +1,100 @@
+use axum::{
+    http::StatusCode,
+    response::{Html, IntoResponse},
+};
+use axum_extra::extract::Multipart;
+use regex::Regex;
+use toml::Value;
+use tracing::{error, info, instrument};
+
+#[instrument()]
+pub async fn lockfile(
+    mut multipart: Multipart,
+) -> Result<impl IntoResponse, (StatusCode, &'static str)> {
+    let mut content = String::new();
+    while let Some(field) = multipart.next_field().await.map_err(|_| {
+        error!("Failed to read field from multipart");
+        (
+            StatusCode::BAD_REQUEST,
+            "Failed to read field from multipart",
+        )
+    })? {
+        let data = field.bytes().await.map_err(|_| {
+            error!("Failed to read bytes from multipart");
+            (
+                StatusCode::BAD_REQUEST,
+                "Failed to read bytes from multipart",
+            )
+        })?;
+
+        content.push_str(&String::from_utf8_lossy(&data));
+    }
+
+    if toml::from_str::<Value>(&content).is_err() {
+        error!("Failed to parse TOML from multipart");
+        return Err((
+            StatusCode::BAD_REQUEST,
+            "Failed to parse TOML from multipart",
+        ));
+    }
+
+    let re = Regex::new(r#"checksum = "([^"]+)"\n"#)
+        .map_err(|_| (StatusCode::BAD_REQUEST, "Failed to compile regex"))?;
+
+    let mut output = Vec::new();
+    for cap in re.captures_iter(&content) {
+        if let Some(m) = cap.get(1) {
+            let checksum = m.as_str();
+            match checksum_to_sprinkle(checksum) {
+                Ok(sprinkle) => output.push(sprinkle),
+                Err(_) => return Err((StatusCode::UNPROCESSABLE_ENTITY, "Invalid checksum")),
+            }
+        }
+    }
+
+    if output.is_empty() {
+        error!("No checksums found");
+        return Err((StatusCode::BAD_REQUEST, "No checksums found"));
+    }
+
+    info!(output = ?output);
+    Ok(Html(output.join("\n")).into_response())
+}
+
+fn checksum_to_sprinkle(checksum: &str) -> Result<String, &'static str> {
+    if hex::decode(checksum).is_err() {
+        return Err("Invalid checksum");
+    }
+
+    let color = checksum.chars().take(6).collect::<String>();
+    let top = u32::from_str_radix(&checksum.chars().skip(6).take(2).collect::<String>(), 16)
+        .map_err(|_| "Invalid top value")?;
+
+    let left = u32::from_str_radix(&checksum.chars().skip(8).take(2).collect::<String>(), 16)
+        .map_err(|_| "Invalid top value")?;
+
+    Ok(format!(
+        r#"<div style="background-color:#{color};top:{top}px;left:{left}px;"></div>"#
+    ))
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_checksum_to_sprinkle() {
+        assert_eq!(
+            checksum_to_sprinkle(
+                "337789faa0372648a8ac286b2f92a53121fe118f12e29009ac504872a5413cc6"
+            ),
+            Ok(r#"<div style="background-color:#337789;top:250px;left:160px;"></div>"#.to_string())
+        );
+        assert_eq!(
+            checksum_to_sprinkle(
+                "22ba454b13e4e29b5b892a62c334360a571de5a25c936283416c94328427dd57"
+            ),
+            Ok(r#"<div style="background-color:#22ba45;top:75px;left:19px;"></div>"#.to_string())
+        );
+    }
+}
diff --git a/src/routes/task_twentythree/mod.rs b/src/routes/task_twentythree/mod.rs
new file mode 100644
index 0000000..210020e
--- /dev/null
+++ b/src/routes/task_twentythree/mod.rs
@@ -0,0 +1,11 @@
+mod star;
+pub use star::star;
+
+mod present;
+pub use present::present;
+
+mod ornament;
+pub use ornament::ornament;
+
+mod lockfile;
+pub use lockfile::lockfile;
diff --git a/src/routes/task_twentythree/ornament.rs b/src/routes/task_twentythree/ornament.rs
new file mode 100644
index 0000000..56ebca7
--- /dev/null
+++ b/src/routes/task_twentythree/ornament.rs
@@ -0,0 +1,64 @@
+use std::fmt::Display;
+
+use axum::{
+    extract::Path,
+    http::StatusCode,
+    response::{Html, IntoResponse},
+};
+use html_escape::encode_safe;
+use tracing::{info, instrument};
+
+#[derive(Debug, PartialEq, Clone)]
+enum State {
+    On,
+    Off,
+}
+
+impl State {
+    const fn next(&self) -> Self {
+        match self {
+            Self::On => Self::Off,
+            Self::Off => Self::On,
+        }
+    }
+
+    const fn is_on(&self) -> &str {
+        match self {
+            Self::On => " on",
+            Self::Off => "",
+        }
+    }
+}
+
+impl TryFrom<String> for State {
+    type Error = &'static str;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        match s.to_lowercase().as_str() {
+            "on" => Ok(Self::On),
+            "off" => Ok(Self::Off),
+            _ => Err("invalid state"),
+        }
+    }
+}
+
+impl Display for State {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let state = match self {
+            Self::On => "on",
+            Self::Off => "off",
+        };
+        write!(f, "{state}")
+    }
+}
+
+#[instrument()]
+pub async fn ornament(Path((state, id)): Path<(String, String)>) -> impl IntoResponse {
+    State::try_from(state).map_or_else(|_| (StatusCode::IM_A_TEAPOT, "I'm a teapot").into_response(),
+        |state| {
+            let ornament = format!(r#"<div class="ornament{on}" id="ornament{id}" hx-trigger="load delay:2s once" hx-get="/23/ornament/{next_state}/{id}" hx-swap="outerHTML"></div>"#,on = state.is_on(), id = encode_safe(&id), next_state = state.next() );
+            info!(ornament);
+            Html(ornament).into_response()
+        }
+    )
+}
diff --git a/src/routes/task_twentythree/present.rs b/src/routes/task_twentythree/present.rs
new file mode 100644
index 0000000..1176244
--- /dev/null
+++ b/src/routes/task_twentythree/present.rs
@@ -0,0 +1,79 @@
+use std::fmt::Display;
+
+use axum::{
+    extract::Path,
+    http::StatusCode,
+    response::{Html, IntoResponse},
+};
+use tracing::instrument;
+
+#[derive(Debug, PartialEq, Clone)]
+enum Color {
+    Red,
+    Blue,
+    Purple,
+}
+
+impl Color {
+    const fn next(&self) -> Self {
+        match self {
+            Self::Red => Self::Blue,
+            Self::Blue => Self::Purple,
+            Self::Purple => Self::Red,
+        }
+    }
+}
+
+impl Display for Color {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let color = match self {
+            Self::Red => "red",
+            Self::Blue => "blue",
+            Self::Purple => "purple",
+        };
+        write!(f, "{color}")
+    }
+}
+
+impl TryFrom<String> for Color {
+    type Error = &'static str;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        match s.to_lowercase().as_str() {
+            "blue" => Ok(Self::Blue),
+            "purple" => Ok(Self::Purple),
+            "red" => Ok(Self::Red),
+            _ => Err("invalid color"),
+        }
+    }
+}
+
+// impl From<String> for Color {
+//     fn from(s: String) -> Self {
+//         match s.to_lowercase().as_str() {
+//             "blue" => Self::Blue,
+//             "purple" => Self::Purple,
+//             _ => Self::Red,
+//         }
+//     }
+// }
+
+#[instrument()]
+pub async fn present(Path(color): Path<String>) -> impl IntoResponse {
+    Color::try_from(color).map_or_else(
+        |_| (StatusCode::IM_A_TEAPOT, "I'm a teapot").into_response(),
+        |color| {
+            Html(format!(
+                r#"<div class="present {}" hx-get="/23/present/{}" hx-swap="outerHTML">
+        <div class="ribbon"></div>
+        <div class="ribbon"></div>
+        <div class="ribbon"></div>
+        <div class="ribbon"></div>
+    </div>"#,
+                color,
+                color.next()
+            ))
+            .into_response()
+        },
+    )
+}
diff --git a/src/routes/task_twentythree/star.rs b/src/routes/task_twentythree/star.rs
new file mode 100644
index 0000000..43f58ca
--- /dev/null
+++ b/src/routes/task_twentythree/star.rs
@@ -0,0 +1,7 @@
+use axum::response::{Html, IntoResponse};
+use tracing::instrument;
+
+#[instrument()]
+pub async fn star() -> impl IntoResponse {
+    Html(r#"<div id="star" class="lit"></div>"#)
+}
diff --git a/tests/task_nineteen/main.rs b/tests/task_nineteen/main.rs
index 33afd36..6078b7e 100644
--- a/tests/task_nineteen/main.rs
+++ b/tests/task_nineteen/main.rs
@@ -1,4 +1,4 @@
-// #[cfg(feature = "task19")]
+#[cfg(feature = "task19")]
 mod task_nineteen {
     use axum::http::StatusCode;
     use axum_test::TestServer;