ignition_core/webdev/
mod.rs1pub mod testing;
24
25pub const ROUTE_BUNDLE_VERSION: &str = "1.3.0";
28
29pub const MIN_CLI: &str = "1.0";
31
32pub const ROUTE_FILES: &[(&str, &str)] = &[
41 (
43 "project.json",
44 include_str!("../../webdev/routes/project.json"),
45 ),
46 (
48 "com.inductiveautomation.webdev/resources/cli/tags/resource.json",
49 include_str!(
50 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tags/resource.json"
51 ),
52 ),
53 (
54 "com.inductiveautomation.webdev/resources/cli/tags/config.json",
55 include_str!(
56 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tags/config.json"
57 ),
58 ),
59 (
60 "com.inductiveautomation.webdev/resources/cli/tags/doPost.py",
61 include_str!(
62 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tags/doPost.py"
63 ),
64 ),
65 (
67 "com.inductiveautomation.webdev/resources/cli/tagConfig/resource.json",
68 include_str!(
69 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tagConfig/resource.json"
70 ),
71 ),
72 (
73 "com.inductiveautomation.webdev/resources/cli/tagConfig/config.json",
74 include_str!(
75 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tagConfig/config.json"
76 ),
77 ),
78 (
79 "com.inductiveautomation.webdev/resources/cli/tagConfig/doPost.py",
80 include_str!(
81 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tagConfig/doPost.py"
82 ),
83 ),
84 (
86 "com.inductiveautomation.webdev/resources/cli/alarms/resource.json",
87 include_str!(
88 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/alarms/resource.json"
89 ),
90 ),
91 (
92 "com.inductiveautomation.webdev/resources/cli/alarms/config.json",
93 include_str!(
94 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/alarms/config.json"
95 ),
96 ),
97 (
98 "com.inductiveautomation.webdev/resources/cli/alarms/doPost.py",
99 include_str!(
100 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/alarms/doPost.py"
101 ),
102 ),
103 (
105 "com.inductiveautomation.webdev/resources/cli/tagHistory/resource.json",
106 include_str!(
107 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tagHistory/resource.json"
108 ),
109 ),
110 (
111 "com.inductiveautomation.webdev/resources/cli/tagHistory/config.json",
112 include_str!(
113 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tagHistory/config.json"
114 ),
115 ),
116 (
117 "com.inductiveautomation.webdev/resources/cli/tagHistory/doPost.py",
118 include_str!(
119 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/tagHistory/doPost.py"
120 ),
121 ),
122];
123
124pub const SCRIPT_EXEC_TEMPLATE: &str = include_str!(
133 "../../webdev/routes/com.inductiveautomation.webdev/resources/cli/scriptExec/doPost.py"
134);
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 const VERSION_FILE: &str = include_str!("../../webdev/routes/VERSION");
143
144 #[test]
148 fn route_sources_carry_the_embedded_handshake_constants() {
149 let route_version = format!("ROUTE_VERSION = '{}'", ROUTE_BUNDLE_VERSION);
150 let min_cli = format!("MIN_CLI = '{}'", MIN_CLI);
151 let mut do_post_count = 0;
152 for (name, contents) in ROUTE_FILES {
153 if name.ends_with("doPost.py") {
154 do_post_count += 1;
155 assert!(
156 contents.contains(&route_version),
157 "{name}: missing {route_version}"
158 );
159 assert!(contents.contains(&min_cli), "{name}: missing {min_cli}");
160 }
161 }
162 assert_eq!(do_post_count, 4, "expected the four always-on dispatchers");
163 assert_eq!(
164 VERSION_FILE.trim(),
165 ROUTE_BUNDLE_VERSION,
166 "webdev/routes/VERSION drifted from ROUTE_BUNDLE_VERSION"
167 );
168 }
169
170 #[test]
173 fn always_on_bundle_carries_no_secret_placeholder() {
174 for (name, contents) in ROUTE_FILES {
175 assert!(
176 !contents.contains("__IGN_CLI_SECRET__"),
177 "{name}: the secret placeholder must not ship in the always-on bundle"
178 );
179 }
180 }
181
182 #[test]
185 fn script_exec_template_is_substitutable_and_fail_closed() {
186 assert_eq!(
187 SCRIPT_EXEC_TEMPLATE.matches("__IGN_CLI_SECRET__").count(),
188 1,
189 "the deploy substitution needs exactly one marker occurrence"
190 );
191 assert!(
192 SCRIPT_EXEC_TEMPLATE.contains("SECRET = None"),
193 "the template must keep its fail-closed SECRET default"
194 );
195 }
196
197 #[test]
201 fn member_names_use_forward_slashes_only() {
202 for (name, _) in ROUTE_FILES {
203 assert!(!name.contains('\\'), "backslash in member name: {name}");
204 }
205 }
206
207 #[test]
211 fn manifest_lists_exactly_thirteen_members() {
212 assert_eq!(ROUTE_FILES.len(), 13);
213 assert_eq!(
214 ROUTE_FILES
215 .iter()
216 .filter(|(name, _)| *name == "project.json")
217 .count(),
218 1
219 );
220 assert_eq!(
221 ROUTE_FILES
222 .iter()
223 .filter(|(name, _)| name.ends_with("doPost.py"))
224 .count(),
225 4
226 );
227 for (name, _) in ROUTE_FILES {
228 assert!(
229 *name == "project.json"
230 || name.starts_with("com.inductiveautomation.webdev/resources/cli/"),
231 "member outside the Designer-native layout: {name}"
232 );
233 }
234 }
235
236 #[test]
243 fn tagconfig_route_source_refuses_provider_roots() {
244 let (_, source) = ROUTE_FILES
245 .iter()
246 .find(|(name, _)| {
247 *name == "com.inductiveautomation.webdev/resources/cli/tagConfig/doPost.py"
248 })
249 .expect("tagConfig doPost.py in the manifest");
250 assert!(
251 source.contains("provider_root_unsupported"),
252 "the tagConfig route must keep its provider-root refusal"
253 );
254 assert!(
255 source.contains("def is_provider_root("),
256 "the bracket-form detector must stay nested inside doPost (byte-0 rule)"
257 );
258 assert!(
259 source.contains("'No RpcContext' in traceback.format_exc()"),
260 "the bare-form RpcContext translation must stay"
261 );
262 }
263}