autumn_plugin_d3/
plugin.rs1use std::borrow::Cow;
7
8use autumn_web::app::AppBuilder;
9use autumn_web::plugin::Plugin;
10
11use crate::assets::D3_ASSETS;
12
13pub const PLUGIN_NAME: &str = "autumn-plugin-d3";
15
16#[derive(Debug, Default, Clone, Copy)]
33#[must_use]
34pub struct D3Plugin;
35
36impl D3Plugin {
37 pub const fn new() -> Self {
39 Self
40 }
41}
42
43impl Plugin for D3Plugin {
44 fn name(&self) -> Cow<'static, str> {
45 Cow::Borrowed(PLUGIN_NAME)
46 }
47
48 fn build(self, app: AppBuilder) -> AppBuilder {
49 app.plugin_assets(&D3_ASSETS)
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::assets::{D3_CSS, D3_JS, INIT_JS, PARSE_JS};
57 use autumn_web::assets::{PLUGIN_ASSETS_ROUTE_MARKER, asset_url};
58 use autumn_web::plugin_conformance::{ConformanceConfig, run_conformance};
59 use autumn_web::route_listing::{RouteClassification, RouteSource};
60 use autumn_web::test::{TestApp, TestClient};
61
62 const JS: &str = "text/javascript; charset=utf-8";
63 const CSS: &str = "text/css; charset=utf-8";
64 const IMMUTABLE: &str = "public, max-age=31536000, immutable";
65 const REVALIDATE: &str = "public, max-age=0, must-revalidate";
66 const FILES: [&str; 4] = [D3_JS, PARSE_JS, INIT_JS, D3_CSS];
67
68 fn client() -> TestClient {
69 TestApp::new().plugin(D3Plugin::new()).build()
70 }
71
72 #[tokio::test]
73 async fn every_file_serves_at_its_hashed_url() {
74 let client = client();
75 for path in FILES {
76 let response = client.get(&D3_ASSETS.url(path)).send().await;
77 let ty = if path == D3_CSS { CSS } else { JS };
78 response
79 .assert_ok()
80 .assert_header("content-type", ty)
81 .assert_header("cache-control", IMMUTABLE);
82 assert_eq!(
83 response.body.as_slice(),
84 D3_ASSETS.get(path).expect("bundled").bytes(),
85 "{path}"
86 );
87 }
88 }
89
90 #[tokio::test]
91 async fn d3_js_is_the_umd_build() {
92 let response = client().get(&D3_ASSETS.url(D3_JS)).send().await;
93 assert!(response.text().contains("d3js.org v7.9.0"));
94 }
95
96 #[tokio::test]
97 async fn plain_urls_serve_with_revalidation_and_etags() {
98 let client = client();
99 for path in FILES {
100 let plain = format!("/static/_plugins/d3/{path}");
101 let response = client.get(&plain).send().await;
102 response
103 .assert_ok()
104 .assert_header("cache-control", REVALIDATE);
105 let etag = response.header("etag").expect("etag").to_owned();
106 client
107 .get(&plain)
108 .header("if-none-match", &etag)
109 .send()
110 .await
111 .assert_status(304);
112 }
113 }
114
115 #[tokio::test]
116 async fn provenance_and_stale_paths_are_not_found() {
117 let client = client();
118 for path in [
119 "/static/_plugins/d3/manifest.json",
120 "/static/_plugins/d3/D3-LICENSE",
121 "/static/_plugins/d3/init.00000000.js",
122 "/static/_plugins/d3/nope.js",
123 ] {
124 client.get(path).send().await.assert_status(404);
125 }
126 }
127
128 #[tokio::test]
129 async fn asset_url_resolves_the_installed_bundle() {
130 let _client = client();
131 for path in FILES {
132 assert_eq!(
133 asset_url(&format!("_plugins/d3/{path}")),
134 D3_ASSETS.url(path)
135 );
136 }
137 }
138
139 #[test]
140 fn bundle_routes_are_public_plugin_routes() {
141 let app = autumn_web::app().plugin(D3Plugin::new());
142 let infos = app.plugin_route_infos().expect("route infos");
143 let routes: Vec<_> = infos
144 .iter()
145 .filter(|info| info.path.starts_with("/static/_plugins/d3/"))
146 .collect();
147 assert_eq!(routes.len(), 8, "four files, two URLs each: {infos:?}");
148 for info in routes {
149 assert_eq!(info.method, "GET");
150 assert_eq!(info.classification, RouteClassification::Public);
151 assert_eq!(info.middleware, [PLUGIN_ASSETS_ROUTE_MARKER]);
152 assert_eq!(info.source, RouteSource::Plugin(PLUGIN_NAME.to_owned()));
153 }
154 }
155
156 #[test]
157 fn plugin_passes_conformance() {
158 let app = autumn_web::app().plugin(D3Plugin::new());
159 let infos = app.plugin_route_infos().expect("route infos");
160 let report = run_conformance(&ConformanceConfig::new(PLUGIN_NAME), &infos);
161 assert!(report.passed(), "{}", report.to_text_report());
162 }
163
164 #[tokio::test]
165 async fn installing_the_plugin_twice_is_harmless() {
166 let client = TestApp::new()
167 .plugin(D3Plugin::new())
168 .plugin(D3Plugin::new())
169 .build();
170 client.get(&D3_ASSETS.url(INIT_JS)).send().await.assert_ok();
171 }
172
173 #[test]
174 fn name_is_the_crate_name() {
175 assert_eq!(D3Plugin::new().name(), PLUGIN_NAME);
176 }
177}