Skip to main content

greentic_setup/platform_setup/
mod.rs

1//! Bundle-level platform setup types and static routes policy handling.
2
3mod persistence;
4mod prompts;
5mod types;
6mod url;
7
8// Re-export public types
9pub use persistence::{
10    load_effective_static_routes_defaults, load_runtime_local_base_url,
11    load_runtime_public_base_url, load_static_routes_artifact, load_telemetry_artifact,
12    load_tunnel_artifact, load_tunnel_handoff_artifact, persist_static_routes_artifact,
13    persist_telemetry_artifact, persist_tunnel_artifact, persist_tunnel_handoff_artifact,
14    static_routes_artifact_path, telemetry_artifact_path, tunnel_artifact_path,
15    tunnel_handoff_artifact_path,
16};
17pub use prompts::{
18    prompt_static_routes_policy, prompt_static_routes_policy_with_answers, prompt_tunnel_mode,
19};
20pub use types::{
21    PlatformSetupAnswers, StaticRoutesAnswers, StaticRoutesPolicy, TelemetryAnswers, TunnelAnswers,
22    TunnelHandoff,
23};
24
25#[cfg(test)]
26mod tests {
27    use super::prompts::merge_prompt_seed;
28    use super::types::{
29        PACK_DECLARED_POLICY, STATIC_ROUTES_VERSION, SURFACE_DISABLED, SURFACE_ENABLED,
30        StaticRoutesAnswers, StaticRoutesPolicy,
31    };
32    use super::{
33        load_effective_static_routes_defaults, load_runtime_local_base_url,
34        load_tunnel_handoff_artifact, persist_static_routes_artifact,
35        persist_tunnel_handoff_artifact, tunnel_handoff_artifact_path,
36    };
37
38    #[test]
39    fn disabled_is_default() {
40        let policy = StaticRoutesPolicy::normalize(None, "dev").unwrap();
41        assert_eq!(policy, StaticRoutesPolicy::disabled());
42    }
43
44    #[test]
45    fn enabled_requires_base_url() {
46        let err = StaticRoutesPolicy::normalize(
47            Some(&StaticRoutesAnswers {
48                public_web_enabled: Some(true),
49                ..Default::default()
50            }),
51            "dev",
52        )
53        .unwrap_err();
54        assert!(err.to_string().contains("public_base_url is required"));
55    }
56
57    #[test]
58    fn normalizes_public_base_url() {
59        let policy = StaticRoutesPolicy::normalize(
60            Some(&StaticRoutesAnswers {
61                public_web_enabled: Some(true),
62                public_base_url: Some("https://example.com/base/".into()),
63                ..Default::default()
64            }),
65            "prod",
66        )
67        .unwrap();
68        assert_eq!(
69            policy.public_base_url.as_deref(),
70            Some("https://example.com/base")
71        );
72        assert_eq!(policy.public_surface_policy, SURFACE_ENABLED);
73        assert_eq!(policy.default_route_prefix_policy, PACK_DECLARED_POLICY);
74        assert_eq!(policy.tenant_path_policy, PACK_DECLARED_POLICY);
75    }
76
77    #[test]
78    fn rejects_query_and_fragment() {
79        let err = StaticRoutesPolicy::normalize(
80            Some(&StaticRoutesAnswers {
81                public_web_enabled: Some(true),
82                public_base_url: Some("https://example.com?x=1".into()),
83                ..Default::default()
84            }),
85            "prod",
86        )
87        .unwrap_err();
88        assert!(err.to_string().contains("query string"));
89
90        let err = StaticRoutesPolicy::normalize(
91            Some(&StaticRoutesAnswers {
92                public_web_enabled: Some(true),
93                public_base_url: Some("https://example.com#frag".into()),
94                ..Default::default()
95            }),
96            "prod",
97        )
98        .unwrap_err();
99        assert!(err.to_string().contains("fragment"));
100    }
101
102    #[test]
103    fn allows_http_loopback_in_dev_only() {
104        let policy = StaticRoutesPolicy::normalize(
105            Some(&StaticRoutesAnswers {
106                public_web_enabled: Some(true),
107                public_base_url: Some("http://127.0.0.1:3000/".into()),
108                ..Default::default()
109            }),
110            "dev",
111        )
112        .unwrap();
113        assert_eq!(
114            policy.public_base_url.as_deref(),
115            Some("http://127.0.0.1:3000")
116        );
117
118        let err = StaticRoutesPolicy::normalize(
119            Some(&StaticRoutesAnswers {
120                public_web_enabled: Some(true),
121                public_base_url: Some("http://127.0.0.1:3000".into()),
122                ..Default::default()
123            }),
124            "prod",
125        )
126        .unwrap_err();
127        assert!(err.to_string().contains("dev"));
128    }
129
130    #[test]
131    fn rejects_enabled_with_disabled_surface_policy() {
132        let err = StaticRoutesPolicy::normalize(
133            Some(&StaticRoutesAnswers {
134                public_web_enabled: Some(true),
135                public_base_url: Some("https://example.com".into()),
136                public_surface_policy: Some("disabled".into()),
137                ..Default::default()
138            }),
139            "prod",
140        )
141        .unwrap_err();
142        assert!(err.to_string().contains("incompatible"));
143    }
144
145    #[test]
146    fn persists_and_loads_artifact() {
147        let temp = tempfile::tempdir().unwrap();
148        let policy = StaticRoutesPolicy::normalize(
149            Some(&StaticRoutesAnswers {
150                public_web_enabled: Some(true),
151                public_base_url: Some("https://example.com".into()),
152                ..Default::default()
153            }),
154            "prod",
155        )
156        .unwrap();
157        let path = persist_static_routes_artifact(temp.path(), &policy).unwrap();
158        assert!(path.exists());
159        let loaded = super::load_static_routes_artifact(temp.path())
160            .unwrap()
161            .unwrap();
162        assert_eq!(loaded, policy);
163    }
164
165    #[test]
166    fn effective_defaults_fall_back_to_runtime_endpoint() {
167        let temp = tempfile::tempdir().unwrap();
168        let runtime_dir = temp
169            .path()
170            .join("state")
171            .join("runtime")
172            .join("demo.default");
173        std::fs::create_dir_all(&runtime_dir).unwrap();
174        std::fs::write(
175            runtime_dir.join("endpoints.json"),
176            r#"{"tenant":"demo","team":"default","public_base_url":"https://runtime.example.com"}"#,
177        )
178        .unwrap();
179
180        let loaded =
181            load_effective_static_routes_defaults(temp.path(), "demo", Some("default")).unwrap();
182        assert_eq!(
183            loaded.and_then(|policy| policy.public_base_url),
184            Some("https://runtime.example.com".to_string())
185        );
186    }
187
188    #[test]
189    fn runtime_local_base_url_uses_gateway_endpoint() {
190        let temp = tempfile::tempdir().unwrap();
191        let runtime_dir = temp
192            .path()
193            .join("state")
194            .join("runtime")
195            .join("demo.default");
196        std::fs::create_dir_all(&runtime_dir).unwrap();
197        std::fs::write(
198            runtime_dir.join("endpoints.json"),
199            r#"{"tenant":"demo","team":"default","gateway_listen_addr":"127.0.0.1","gateway_port":8081}"#,
200        )
201        .unwrap();
202
203        assert_eq!(
204            load_runtime_local_base_url(temp.path(), "demo", Some("default")).unwrap(),
205            Some("http://127.0.0.1:8081".to_string())
206        );
207    }
208
209    #[test]
210    fn tunnel_handoff_round_trips_through_the_bundle_artifact() {
211        let temp = tempfile::tempdir().unwrap();
212        assert_eq!(load_tunnel_handoff_artifact(temp.path()).unwrap(), None);
213
214        let handoff = super::TunnelHandoff {
215            service: "cloudflared".to_string(),
216            local_port: 8080,
217            public_base_url: "https://survey-revenues.trycloudflare.com".to_string(),
218        };
219        persist_tunnel_handoff_artifact(temp.path(), &handoff).unwrap();
220
221        assert_eq!(
222            load_tunnel_handoff_artifact(temp.path()).unwrap(),
223            Some(handoff)
224        );
225        // Same directory as static-routes.json — greentic-start reads both
226        // as bundle-scoped config, not the machine-wide `.greentic/` dir.
227        assert!(
228            tunnel_handoff_artifact_path(temp.path())
229                .parent()
230                .unwrap()
231                .ends_with("state/config/platform")
232        );
233    }
234
235    #[test]
236    fn merge_prompt_seed_overlays_partial_answers_on_existing_policy() {
237        let existing = StaticRoutesPolicy {
238            version: STATIC_ROUTES_VERSION,
239            public_web_enabled: false,
240            public_base_url: Some("https://existing.example.com".into()),
241            public_surface_policy: SURFACE_DISABLED.into(),
242            default_route_prefix_policy: PACK_DECLARED_POLICY.into(),
243            tenant_path_policy: PACK_DECLARED_POLICY.into(),
244        };
245        let answers = StaticRoutesAnswers {
246            public_web_enabled: Some(true),
247            public_base_url: None,
248            public_surface_policy: Some(SURFACE_ENABLED.into()),
249            default_route_prefix_policy: None,
250            tenant_path_policy: None,
251        };
252
253        let merged = merge_prompt_seed(Some(&answers), Some(&existing));
254        assert!(merged.public_web_enabled);
255        assert_eq!(
256            merged.public_base_url.as_deref(),
257            Some("https://existing.example.com")
258        );
259        assert_eq!(merged.public_surface_policy, SURFACE_ENABLED);
260    }
261}