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