vorma 0.86.0-pre.3

Vorma framework.
Documentation
use super::*;

#[tokio::test]
async fn runtime_host_document_defaults_feed_view_payload() {
	let dist_dir = temp_dist_dir("view-document-json");
	write_view_manifest(&dist_dir);
	let host = view_host_with_document(&dist_dir, custom_document());

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::GET)
				.uri("/items/42?vorma-json=_")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();
	let payload: serde_json::Value = serde_json::from_slice(response.body()).unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert_eq!(payload["title"]["dangerous_inner_html"], "Default Title");
	assert_eq!(
		payload["meta_head_els"][0]["attributes_known_safe"]["content"],
		"Default description from document"
	);
	assert_eq!(
		payload["rest_head_els"][0]["attributes_known_safe"]["href"],
		"/static/favicon.ico"
	);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_view_skew_bypasses_document_builder() {
	let dist_dir = temp_dist_dir("view-skew-document-bypass");
	write_view_manifest(&dist_dir);
	let calls = Arc::new(AtomicUsize::new(0));
	let calls_for_builder = calls.clone();
	let host = view_host_with_document(
		&dist_dir,
		DocumentBuilder::new(move |_| {
			let calls = calls_for_builder.clone();
			async move {
				calls.fetch_add(1, Ordering::SeqCst);
				Ok(crate::document::Document::new())
			}
		}),
	);

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::GET)
				.uri("/items/42?vorma-json=stale")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert_eq!(
		response
			.headers()
			.get(crate::constants::X_VORMA_BUILD_SKEW)
			.unwrap(),
		"1"
	);
	assert_eq!(calls.load(Ordering::SeqCst), 0);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_missing_view_does_not_build_document() {
	let dist_dir = temp_dist_dir("view-missing-document-bypass");
	write_view_manifest(&dist_dir);
	let calls = Arc::new(AtomicUsize::new(0));
	let calls_for_builder = calls.clone();
	let host = view_host_with_document(
		&dist_dir,
		DocumentBuilder::new(move |_| {
			let calls = calls_for_builder.clone();
			async move {
				calls.fetch_add(1, Ordering::SeqCst);
				Err("document builder should not run for missing views".to_owned())
			}
		}),
	);

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::GET)
				.uri("/missing")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();

	assert_eq!(response.status(), StatusCode::NOT_FOUND);
	assert_eq!(calls.load(Ordering::SeqCst), 0);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_builds_document_and_view_handler_concurrently() {
	let dist_dir = temp_dist_dir("view-document-handler-parallel");
	write_view_manifest(&dist_dir);
	let document_started = Arc::new(Notify::new());
	let handler_started = Arc::new(Notify::new());
	let document_started_for_builder = document_started.clone();
	let handler_started_for_builder = handler_started.clone();
	let mut views = Views::new();
	let document_started_for_handler = document_started.clone();
	let handler_started_for_handler = handler_started.clone();
	views.push(View::new(
		"/items/:id",
		"items.tsx",
		InputParser::<()>::default_input(),
		move |ctx: crate::core::ViewCtx<(), &'static str, ()>| {
			let document_started = document_started_for_handler.clone();
			let handler_started = handler_started_for_handler.clone();
			async move {
				handler_started.notify_one();
				document_started.notified().await;
				Ok(json!({"id": ctx.param("id")}))
			}
		},
	));
	let host = runtime_host_with_document(
		&dist_dir,
		views,
		Resources::new(),
		Middlewares::new(),
		DocumentBuilder::new(move |_| {
			let document_started = document_started_for_builder.clone();
			let handler_started = handler_started_for_builder.clone();
			async move {
				document_started.notify_one();
				handler_started.notified().await;
				Ok(crate::document::Document::new())
			}
		}),
	);

	let response = tokio::time::timeout(
		Duration::from_secs(1),
		host.handle_request(empty_request(Method::GET, "/items/42?vorma-json=_")),
	)
	.await
	.expect("document and view handler should not deadlock")
	.unwrap();
	let payload: serde_json::Value = serde_json::from_slice(response.body()).unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert_eq!(payload["views_data"][0]["id"], "42");
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_serves_view_html_shell() {
	let dist_dir = temp_dist_dir("view-html");
	write_view_manifest(&dist_dir);
	let host = view_host(&dist_dir);

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::GET)
				.uri("/items/42?filter=active")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();
	let body = String::from_utf8(response.body().to_vec()).unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert!(body.contains(r#"id="vorma-data-json""#));
	assert!(body.contains(r#"<div id="vorma-root"></div>"#));
	assert!(body.contains(r#"<script src="/static/entry.js" type="module"></script>"#));
	assert_substrings_in_order(
		&body,
		&[
			r#"data-vorma-css-bundle="/static/entry.css""#,
			r#"data-vorma-css-bundle="/static/shared.css""#,
			r#"data-vorma-css-bundle="/static/items.css""#,
		],
	);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_document_shell_renders_attrs_and_body_prefix() {
	let dist_dir = temp_dist_dir("view-document-html");
	write_view_manifest(&dist_dir);
	let host = view_host_with_document(&dist_dir, custom_document());

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::GET)
				.uri("/items/42")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();
	let body = String::from_utf8(response.body().to_vec()).unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert!(body.contains(r#"<html lang="en" data-shell="custom">"#));
	assert!(body.contains(r#"<body data-body="custom">"#));
	assert_substrings_in_order(
		&body,
		&[
			r#"<div data-document-prefix="1">Document prefix</div>"#,
			r#"id="vorma-data-json""#,
			r#"<div id="vorma-root"></div>"#,
		],
	);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_document_builder_receives_real_request() {
	let dist_dir = temp_dist_dir("view-document-request");
	write_view_manifest(&dist_dir);
	let host = view_host_with_document(
		&dist_dir,
		DocumentBuilder::new(|ctx| async move {
			let mut document = crate::document::Document::new();
			document
				.head()
				.meta_property_content("og:url", ctx.request().path());
			Ok(document)
		}),
	);

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::GET)
				.uri("/items/42")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();
	let body = String::from_utf8(response.body().to_vec()).unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert!(body.contains(r#"content="/items/42" property="og:url""#));
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_view_document_error_preserves_client_build_id() {
	let dist_dir = temp_dist_dir("view-document-error");
	write_view_manifest(&dist_dir);
	let host = view_host_with_document(
		&dist_dir,
		DocumentBuilder::new(|_| async move { Err("document failed".to_owned()) }),
	);

	let response = host
		.handle_request(empty_request(Method::GET, "/items/42"))
		.await
		.unwrap();

	assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
	assert_eq!(
		response.headers().get(X_VORMA_CLIENT_BUILD_ID).unwrap(),
		manifest_with_view_assets()
			.to_client_build_id()
			.unwrap()
			.as_str()
	);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_view_document_error_cancels_running_view_handler() {
	let dist_dir = temp_dist_dir("view-document-error-cancels-handler");
	write_view_manifest(&dist_dir);
	let handler_started = Arc::new(Notify::new());
	let (cancel_tx, cancel_rx) = oneshot::channel();
	let cancel_tx = Arc::new(Mutex::new(Some(cancel_tx)));
	let mut views = Views::new();
	let handler_started_for_handler = handler_started.clone();
	let cancel_tx_for_handler = cancel_tx.clone();
	views.push(View::new(
		"/items/:id",
		"items.tsx",
		InputParser::<()>::default_input(),
		move |_ctx: crate::core::ViewCtx<(), &'static str, ()>| {
			let handler_started = handler_started_for_handler.clone();
			let cancel_tx = cancel_tx_for_handler.clone();
			async move {
				let _drop_signal = DropSignal(cancel_tx);
				handler_started.notify_one();
				std::future::pending::<vorma_tasks::Result<serde_json::Value, &'static str>>().await
			}
		},
	));
	let handler_started_for_document = handler_started.clone();
	let host = runtime_host_with_document(
		&dist_dir,
		views,
		Resources::new(),
		Middlewares::new(),
		DocumentBuilder::new(move |_| {
			let handler_started = handler_started_for_document.clone();
			async move {
				handler_started.notified().await;
				Err("document failed".to_owned())
			}
		}),
	);

	let response = tokio::time::timeout(
		Duration::from_secs(1),
		host.handle_request(empty_request(Method::GET, "/items/42")),
	)
	.await
	.expect("document error should not hang on view handler")
	.unwrap();

	assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
	tokio::time::timeout(Duration::from_secs(1), cancel_rx)
		.await
		.expect("document failure should cancel running view handler")
		.expect("view handler should report cancellation");
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_view_manifest_mismatch_preserves_client_build_id() {
	let dist_dir = temp_dist_dir("view-manifest-mismatch");
	write_manifest(&dist_dir);
	let host = view_host(&dist_dir);

	let response = host
		.handle_request(empty_request(Method::GET, "/items/42?vorma-json=_"))
		.await
		.unwrap();

	assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
	assert_eq!(
		response.headers().get(X_VORMA_CLIENT_BUILD_ID).unwrap(),
		manifest().to_client_build_id().unwrap().as_str()
	);
	fs::remove_dir_all(dist_dir).unwrap();
}

#[tokio::test]
async fn runtime_host_head_view_preserves_content_length_without_body() {
	let dist_dir = temp_dist_dir("view-head");
	write_view_manifest(&dist_dir);
	let host = view_host(&dist_dir);

	let response = host
		.handle_request(
			Request::builder()
				.method(Method::HEAD)
				.uri("/items/42")
				.body(Bytes::new())
				.unwrap(),
		)
		.await
		.unwrap();

	assert_eq!(response.status(), StatusCode::OK);
	assert!(response.headers().contains_key(CONTENT_LENGTH));
	assert!(response.body().is_empty());
	fs::remove_dir_all(dist_dir).unwrap();
}