//! Integration tests for the user-pages host (`u.makenot.work`, `u.localhost` //! in tests): custom-page rendering, sanitization on render, item-inherits- //! project CSS, moderation lockout, and the strict, cookieless security posture. use crate::harness::TestHarness; const HOST: &str = "u.localhost"; /// GET a path on the user-pages host. async fn get_u(h: &mut TestHarness, path: &str) -> crate::harness::client::TestResponse { h.client .request_with_headers("GET", path, None, &[("Host", HOST)]) .await } async fn set_user_custom(h: &TestHarness, user_id: makenotwork::db::UserId, html: &str, css: &str) { sqlx::query( "UPDATE users SET custom_html=$2, custom_css=$3, custom_pages_updated_at=now() WHERE id=$1", ) .bind(user_id) .bind(html) .bind(css) .execute(&h.db) .await .expect("set user custom"); } async fn set_project_custom(h: &TestHarness, project_id: &str, html: &str, css: &str) { sqlx::query( "UPDATE projects SET custom_html=$2, custom_css=$3, custom_pages_updated_at=now() \ WHERE id=$1::uuid", ) .bind(project_id) .bind(html) .bind(css) .execute(&h.db) .await .expect("set project custom"); } async fn item_slug(h: &TestHarness, item_id: &str) -> String { let row: (String,) = sqlx::query_as("SELECT slug FROM items WHERE id = $1::uuid") .bind(item_id) .fetch_one(&h.db) .await .unwrap(); row.0 } #[tokio::test] async fn user_page_renders_sanitized_custom_html_and_css() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("alice", "digital", 500).await; set_user_custom( &h, setup.user_id, "

Welcome

", "body { background: red } .hero { color: blue } \ .x { background: url(https://evil.com/y.png) } @import url(https://evil.com/z.css);", ) .await; let resp = get_u(&mut h, "/alice").await; assert_eq!(resp.status, 200, "body: {}", resp.text); // Platform chrome is present (outside the canvas). assert!(resp.text.contains("makenot.work")); // Canvas is scoped to the owner id. assert!(resp.text.contains(&format!("uc-{}", setup.user_id))); // Allowed content survives. assert!(resp.text.contains("Welcome")); // Script + off-platform references are gone. assert!(!resp.text.contains("alert(1)")); assert!(!resp.text.contains("My storefront", ".mnw-buy { display: none } header { color: red }", ) .await; let resp = get_u(&mut h, "/bob/bob-proj").await; assert_eq!(resp.status, 200, "body: {}", resp.text); // Creator HTML + project-scoped CSS. assert!(resp.text.contains("My storefront")); assert!(resp.text.contains(&format!(".user-canvas#uc-{}", setup.project_id))); // System slots present and the price rendered. assert!(resp.text.contains("mnw-buy")); assert!(resp.text.contains("$12.99")); // The file list links the published item back to the apex. assert!(resp.text.contains("Test Item")); assert!(resp.text.contains("/i/")); // The creator's attempt to hide the buy slot was stripped. assert!(!resp.text.replace(' ', "").contains("display:none")); } #[tokio::test] async fn item_inherits_project_css_rescoped_to_item_canvas() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("carol", "digital", 0).await; h.publish_project_and_item(&setup.project_id, &setup.item_id).await; // Use a length value (survives verbatim; lightningcss normalizes color names). set_project_custom(&h, &setup.project_id, "

store

", ".hero { padding: 17px }").await; let slug = item_slug(&h, &setup.item_id).await; let resp = get_u(&mut h, &format!("/carol/carol-proj/{slug}")).await; assert_eq!(resp.status, 200, "body: {}", resp.text); // Item canvas keyed on the parent project id; project CSS re-scoped to it. assert!(resp.text.contains(&format!("ic-{}", setup.project_id))); assert!(resp.text.contains(&format!(".item-canvas#ic-{} .hero", setup.project_id))); assert!(resp.text.contains("17px")); // Item slot + default layout (no creator HTML on item pages). assert!(resp.text.contains("mnw-item")); assert!(resp.text.contains("Test Item")); assert!(resp.text.contains("Free")); } #[tokio::test] async fn unstyled_project_item_is_plain_default() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("dave", "digital", 500).await; h.publish_project_and_item(&setup.project_id, &setup.item_id).await; // No custom CSS set on the project. let slug = item_slug(&h, &setup.item_id).await; let resp = get_u(&mut h, &format!("/dave/dave-proj/{slug}")).await; assert_eq!(resp.status, 200, "body: {}", resp.text); // Default layout renders, but with no creator CSS (only the reduced-motion // guard is conditional on there being CSS, so it's absent here). assert!(resp.text.contains("mnw-item")); assert!(!resp.text.contains(".item-canvas#ic-")); } #[tokio::test] async fn locked_user_renders_chrome_only() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("erin", "digital", 500).await; set_user_custom(&h, setup.user_id, "

SecretLayout

", ".hero{color:red}").await; sqlx::query("UPDATE users SET custom_pages_locked = true WHERE id = $1") .bind(setup.user_id) .execute(&h.db) .await .unwrap(); let resp = get_u(&mut h, "/erin").await; assert_eq!(resp.status, 200, "body: {}", resp.text); // Chrome present, but the creator content is withheld while locked. assert!(resp.text.contains("makenot.work")); assert!(!resp.text.contains("SecretLayout")); } #[tokio::test] async fn strict_csp_and_no_session_cookie() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("frank", "digital", 500).await; set_user_custom(&h, setup.user_id, "

hi

", "p{color:red}").await; let resp = get_u(&mut h, "/frank").await; assert_eq!(resp.status, 200); let csp = resp .headers .get("content-security-policy") .and_then(|v| v.to_str().ok()) .unwrap_or(""); assert!(csp.contains("default-src 'none'"), "csp: {csp}"); assert!(csp.contains("frame-ancestors 'none'"), "csp: {csp}"); // No script source is allowed at all. assert!(!csp.contains("script-src"), "csp: {csp}"); // The user-pages host must never set a session cookie. assert!( resp.headers.get(axum::http::header::SET_COOKIE).is_none(), "u. host leaked a Set-Cookie" ); } #[tokio::test] async fn unknown_handle_is_404() { let mut h = TestHarness::new().await; let resp = get_u(&mut h, "/nobody-here").await; assert_eq!(resp.status, 404); } #[tokio::test] async fn bare_host_redirects_to_apex() { let mut h = TestHarness::new().await; let resp = get_u(&mut h, "/").await; // Temporary redirect to the apex. assert!(resp.status.is_redirection(), "status: {}", resp.status); } // ── Editor (apex, authenticated) ───────────────────────────────────────────── fn form_body(pairs: &[(&str, &str)]) -> String { url::form_urlencoded::Serializer::new(String::new()) .extend_pairs(pairs) .finish() } #[tokio::test] async fn editor_get_renders() { let mut h = TestHarness::new().await; h.create_creator("ed1").await; let resp = h.client.get("/dashboard/custom-page").await; assert_eq!(resp.status, 200, "body: {}", resp.text); assert!(resp.text.contains("name=\"custom_html\"")); assert!(resp.text.contains("name=\"custom_css\"")); assert!(resp.text.contains("id=\"cp-preview\"")); assert!(resp.text.contains("/preview/")); } #[tokio::test] async fn save_persists_and_renders_on_u_host() { let mut h = TestHarness::new().await; let uid = h.create_creator("ed2").await; let body = form_body(&[("custom_html", "

MyHero

"), ("custom_css", ".intro { color: red }")]); let resp = h.client.post_form("/dashboard/custom-page", &body).await; assert!(resp.status.is_success(), "save status {}: {}", resp.status, resp.text); let live = get_u(&mut h, "/ed2").await; assert_eq!(live.status, 200, "body: {}", live.text); assert!(live.text.contains("MyHero")); assert!(live.text.contains(&format!(".user-canvas#uc-{uid}"))); } #[tokio::test] async fn autosave_surfaces_blocked_references() { let mut h = TestHarness::new().await; h.create_creator("ed3").await; let body = form_body(&[ ("custom_html", "\"x\""), ("custom_css", ".a { background: url(https://evil.com/y.png) }"), ]); let resp = h.client.post_form("/dashboard/custom-page/draft", &body).await; assert!(resp.status.is_success(), "draft status {}: {}", resp.status, resp.text); // The blocked-references panel lists the stripped off-platform refs... assert!(resp.text.contains("evil.com"), "panel: {}", resp.text); assert!(resp.text.contains("Off-platform link")); // ...and the out-of-band iframe reloads the preview. assert!(resp.text.contains("cp-preview")); } #[tokio::test] async fn reset_clears_custom_page() { let mut h = TestHarness::new().await; h.create_creator("ed4").await; let body = form_body(&[("custom_html", "

GoneSoon

"), ("custom_css", "")]); h.client.post_form("/dashboard/custom-page", &body).await; // It renders before reset. assert!(get_u(&mut h, "/ed4").await.text.contains("GoneSoon")); let resp = h.client.post_form("/dashboard/custom-page/reset", "").await; assert!(resp.status.is_redirection() || resp.status.is_success(), "reset: {}", resp.status); let live = get_u(&mut h, "/ed4").await; assert!(!live.text.contains("GoneSoon")); } #[tokio::test] async fn project_editor_rejects_non_owner() { let mut h = TestHarness::new().await; let _owner = h.create_creator_with_item("owna", "digital", 0).await; // Switch to a different logged-in creator. h.create_creator("intruder").await; let resp = h.client.get("/dashboard/project/owna-proj/custom-page").await; assert_eq!(resp.status, 404); } // ── Security-review checklist coverage ─────────────────────────────────────── #[tokio::test] async fn custom_content_never_renders_on_the_apex_domain() { // Isolation: custom HTML/CSS is served only from the u. host. The apex // profile page must never include it (no sanitizer-bypass reaches a // cookie-bearing origin). let mut h = TestHarness::new().await; h.create_creator("apexiso").await; let body = form_body(&[("custom_html", "

SecretOnlyOnU

"), ("custom_css", ".x{color:red}")]); h.client.post_form("/dashboard/custom-page", &body).await; let apex = h.client.get("/u/apexiso").await; assert_eq!(apex.status, 200, "body: {}", apex.text); assert!(!apex.text.contains("SecretOnlyOnU"), "custom HTML leaked onto the apex profile"); } #[tokio::test] async fn locked_owner_project_and_item_render_default() { let mut h = TestHarness::new().await; let setup = h.create_creator_with_item("lockp", "digital", 0).await; h.publish_project_and_item(&setup.project_id, &setup.item_id).await; set_project_custom(&h, &setup.project_id, "

StyledStore

", ".hero { padding: 17px }").await; // Sanity: renders before the lock. assert!(get_u(&mut h, "/lockp/lockp-proj").await.text.contains("StyledStore")); sqlx::query("UPDATE users SET custom_pages_locked = true WHERE id = $1") .bind(setup.user_id) .execute(&h.db) .await .unwrap(); let proj = get_u(&mut h, "/lockp/lockp-proj").await; assert!(!proj.text.contains("StyledStore"), "locked project still shows custom HTML"); let slug = item_slug(&h, &setup.item_id).await; let item = get_u(&mut h, &format!("/lockp/lockp-proj/{slug}")).await; assert!(!item.text.contains("17px"), "locked item still wears project CSS"); } #[tokio::test] async fn locked_user_editor_save_is_blocked() { let mut h = TestHarness::new().await; let uid = h.create_creator("lockuser").await; sqlx::query("UPDATE users SET custom_pages_locked = true WHERE id = $1") .bind(uid) .execute(&h.db) .await .unwrap(); let body = form_body(&[("custom_html", "

ShouldNotSave

"), ("custom_css", "")]); let resp = h.client.post_form("/dashboard/custom-page", &body).await; assert!(resp.text.to_lowercase().contains("locked"), "expected locked notice: {}", resp.text); // Nothing was published. let live = get_u(&mut h, "/lockuser").await; assert!(!live.text.contains("ShouldNotSave")); }