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_public_base_url,
11    load_static_routes_artifact, load_tunnel_artifact, persist_static_routes_artifact,
12    persist_tunnel_artifact, static_routes_artifact_path, tunnel_artifact_path,
13};
14pub use prompts::{
15    prompt_static_routes_policy, prompt_static_routes_policy_with_answers, prompt_tunnel_mode,
16};
17pub use types::{PlatformSetupAnswers, StaticRoutesAnswers, StaticRoutesPolicy, TunnelAnswers};
18
19#[cfg(test)]
20mod tests {
21    use super::prompts::merge_prompt_seed;
22    use super::types::{
23        PACK_DECLARED_POLICY, STATIC_ROUTES_VERSION, SURFACE_DISABLED, SURFACE_ENABLED,
24        StaticRoutesAnswers, StaticRoutesPolicy,
25    };
26    use super::{load_effective_static_routes_defaults, persist_static_routes_artifact};
27
28    #[test]
29    fn disabled_is_default() {
30        let policy = StaticRoutesPolicy::normalize(None, "dev").unwrap();
31        assert_eq!(policy, StaticRoutesPolicy::disabled());
32    }
33
34    #[test]
35    fn enabled_requires_base_url() {
36        let err = StaticRoutesPolicy::normalize(
37            Some(&StaticRoutesAnswers {
38                public_web_enabled: Some(true),
39                ..Default::default()
40            }),
41            "dev",
42        )
43        .unwrap_err();
44        assert!(err.to_string().contains("public_base_url is required"));
45    }
46
47    #[test]
48    fn normalizes_public_base_url() {
49        let policy = StaticRoutesPolicy::normalize(
50            Some(&StaticRoutesAnswers {
51                public_web_enabled: Some(true),
52                public_base_url: Some("https://example.com/base/".into()),
53                ..Default::default()
54            }),
55            "prod",
56        )
57        .unwrap();
58        assert_eq!(
59            policy.public_base_url.as_deref(),
60            Some("https://example.com/base")
61        );
62        assert_eq!(policy.public_surface_policy, SURFACE_ENABLED);
63        assert_eq!(policy.default_route_prefix_policy, PACK_DECLARED_POLICY);
64        assert_eq!(policy.tenant_path_policy, PACK_DECLARED_POLICY);
65    }
66
67    #[test]
68    fn rejects_query_and_fragment() {
69        let err = StaticRoutesPolicy::normalize(
70            Some(&StaticRoutesAnswers {
71                public_web_enabled: Some(true),
72                public_base_url: Some("https://example.com?x=1".into()),
73                ..Default::default()
74            }),
75            "prod",
76        )
77        .unwrap_err();
78        assert!(err.to_string().contains("query string"));
79
80        let err = StaticRoutesPolicy::normalize(
81            Some(&StaticRoutesAnswers {
82                public_web_enabled: Some(true),
83                public_base_url: Some("https://example.com#frag".into()),
84                ..Default::default()
85            }),
86            "prod",
87        )
88        .unwrap_err();
89        assert!(err.to_string().contains("fragment"));
90    }
91
92    #[test]
93    fn allows_http_loopback_in_dev_only() {
94        let policy = StaticRoutesPolicy::normalize(
95            Some(&StaticRoutesAnswers {
96                public_web_enabled: Some(true),
97                public_base_url: Some("http://127.0.0.1:3000/".into()),
98                ..Default::default()
99            }),
100            "dev",
101        )
102        .unwrap();
103        assert_eq!(
104            policy.public_base_url.as_deref(),
105            Some("http://127.0.0.1:3000")
106        );
107
108        let err = StaticRoutesPolicy::normalize(
109            Some(&StaticRoutesAnswers {
110                public_web_enabled: Some(true),
111                public_base_url: Some("http://127.0.0.1:3000".into()),
112                ..Default::default()
113            }),
114            "prod",
115        )
116        .unwrap_err();
117        assert!(err.to_string().contains("dev"));
118    }
119
120    #[test]
121    fn rejects_enabled_with_disabled_surface_policy() {
122        let err = StaticRoutesPolicy::normalize(
123            Some(&StaticRoutesAnswers {
124                public_web_enabled: Some(true),
125                public_base_url: Some("https://example.com".into()),
126                public_surface_policy: Some("disabled".into()),
127                ..Default::default()
128            }),
129            "prod",
130        )
131        .unwrap_err();
132        assert!(err.to_string().contains("incompatible"));
133    }
134
135    #[test]
136    fn persists_and_loads_artifact() {
137        let temp = tempfile::tempdir().unwrap();
138        let policy = StaticRoutesPolicy::normalize(
139            Some(&StaticRoutesAnswers {
140                public_web_enabled: Some(true),
141                public_base_url: Some("https://example.com".into()),
142                ..Default::default()
143            }),
144            "prod",
145        )
146        .unwrap();
147        let path = persist_static_routes_artifact(temp.path(), &policy).unwrap();
148        assert!(path.exists());
149        let loaded = super::load_static_routes_artifact(temp.path())
150            .unwrap()
151            .unwrap();
152        assert_eq!(loaded, policy);
153    }
154
155    #[test]
156    fn effective_defaults_fall_back_to_runtime_endpoint() {
157        let temp = tempfile::tempdir().unwrap();
158        let runtime_dir = temp
159            .path()
160            .join("state")
161            .join("runtime")
162            .join("demo.default");
163        std::fs::create_dir_all(&runtime_dir).unwrap();
164        std::fs::write(
165            runtime_dir.join("endpoints.json"),
166            r#"{"tenant":"demo","team":"default","public_base_url":"https://runtime.example.com"}"#,
167        )
168        .unwrap();
169
170        let loaded =
171            load_effective_static_routes_defaults(temp.path(), "demo", Some("default")).unwrap();
172        assert_eq!(
173            loaded.and_then(|policy| policy.public_base_url),
174            Some("https://runtime.example.com".to_string())
175        );
176    }
177
178    #[test]
179    fn merge_prompt_seed_overlays_partial_answers_on_existing_policy() {
180        let existing = StaticRoutesPolicy {
181            version: STATIC_ROUTES_VERSION,
182            public_web_enabled: false,
183            public_base_url: Some("https://existing.example.com".into()),
184            public_surface_policy: SURFACE_DISABLED.into(),
185            default_route_prefix_policy: PACK_DECLARED_POLICY.into(),
186            tenant_path_policy: PACK_DECLARED_POLICY.into(),
187        };
188        let answers = StaticRoutesAnswers {
189            public_web_enabled: Some(true),
190            public_base_url: None,
191            public_surface_policy: Some(SURFACE_ENABLED.into()),
192            default_route_prefix_policy: None,
193            tenant_path_policy: None,
194        };
195
196        let merged = merge_prompt_seed(Some(&answers), Some(&existing));
197        assert!(merged.public_web_enabled);
198        assert_eq!(
199            merged.public_base_url.as_deref(),
200            Some("https://existing.example.com")
201        );
202        assert_eq!(merged.public_surface_policy, SURFACE_ENABLED);
203    }
204}