Skip to main content

uptrakit_web_api_types/
lib.rs

1/// Re-export `SecretString` so consumers (web-api, CLI, openapi-client) can
2/// use it without a direct `uptrakit-shared-types` dependency.
3pub use uptrakit_shared_types::SecretString;
4
5/// Re-export `PluginRole` so consumers can construct and match on role values
6/// without a direct `uptrakit-shared-types` dependency.
7pub use uptrakit_shared_types::PluginRole;
8
9pub mod access_presets;
10pub mod agents;
11pub mod api_tokens;
12pub mod audit_logs;
13pub mod auth;
14pub mod autodiscovery;
15pub mod batch_actions;
16pub mod command_validation;
17pub mod device_auth;
18pub mod discovery_allowlist;
19pub mod enrollment_tokens;
20pub mod error;
21pub mod events;
22pub mod host_tags;
23pub mod hosts;
24pub mod instance_config_state;
25pub mod instance_plugins;
26pub(crate) mod json_object;
27pub mod masked_url;
28pub mod mfa;
29pub mod notifications;
30pub mod oauth;
31pub mod oidc_auth;
32pub mod oidc_providers;
33pub mod pagination;
34pub mod permissions;
35pub mod plugin_config_test;
36pub mod plugin_configs;
37pub mod plugin_type_settings;
38pub mod prelude;
39pub mod profile;
40pub mod registration;
41pub mod roles;
42pub mod scheduler;
43pub mod server_cert;
44pub mod services;
45pub mod settings_access;
46pub mod settings_agent_certs;
47pub mod settings_ca;
48pub mod settings_combined;
49pub mod settings_network;
50pub mod settings_reset;
51pub mod system_services;
52pub use masked_url::MaskedUrl;
53pub mod settings_nats;
54pub mod settings_oauth;
55pub mod settings_provider_github;
56pub mod settings_zeroconf;
57pub mod software_items;
58pub mod surfaces;
59pub mod system_alerts;
60pub mod system_enrollment_tokens;
61pub mod update_batches;
62pub mod update_history;
63pub mod users;
64pub mod validation;
65
66/// Default value for `enabled` fields in create-request types.
67///
68/// Used as `#[serde(default = "crate::default_enabled")]` in
69/// [`plugin_configs::CreatePluginConfigRequest`].
70pub fn default_enabled() -> bool {
71    true
72}
73
74/// Default value for the `featured` field in create-request types.
75///
76/// Used as `#[serde(default = "crate::default_featured")]` in
77/// [`software_items::CreateSoftwareItemRequest`].
78pub fn default_featured() -> bool {
79    true
80}
81
82#[cfg(test)]
83mod tests {
84    #![expect(
85        clippy::assertions_on_result_states,
86        reason = "test assertions — is_ok/is_err provides readable failure messages"
87    )]
88    use crate::auth::{AuthResponse, UserResponse};
89    use crate::error::ErrorResponse;
90    use crate::oidc_providers::CreateOidcProviderRequest;
91    use crate::permissions::Permission;
92    use crate::plugin_configs::CreatePluginConfigRequest;
93    use crate::registration::RegistrationMode;
94    use crate::software_items::CreateSoftwareItemRequest;
95    use crate::update_history::UpdateStatus;
96    use strum::IntoEnumIterator;
97    use uptrakit_shared_types::SecretString;
98    use uuid::Uuid;
99
100    // ── 1. Permission enum round-trip ─────────────────────────────────────
101
102    #[test]
103    fn permission_serde_round_trip() {
104        for perm in Permission::iter() {
105            let json = serde_json::to_string(&perm).unwrap();
106            let deserialized: Permission = serde_json::from_str(&json).unwrap();
107            assert_eq!(deserialized, perm);
108        }
109    }
110
111    #[test]
112    fn permission_as_str_values() {
113        assert_eq!(Permission::ViewSettings.as_str(), "view_settings");
114        assert_eq!(
115            Permission::ManageAuthSettings.as_str(),
116            "manage_auth_settings"
117        );
118        assert_eq!(Permission::ViewServices.as_str(), "view_services");
119        assert_eq!(Permission::UpdateServices.as_str(), "update_services");
120        assert_eq!(
121            Permission::ManageGlobalSettings.as_str(),
122            "manage_global_settings"
123        );
124    }
125
126    #[test]
127    fn permission_from_str_valid() {
128        assert_eq!(
129            "view_settings".parse::<Permission>().ok(),
130            Some(Permission::ViewSettings)
131        );
132        assert_eq!(
133            "manage_auth_settings".parse::<Permission>().ok(),
134            Some(Permission::ManageAuthSettings)
135        );
136        assert_eq!(
137            "view_services".parse::<Permission>().ok(),
138            Some(Permission::ViewServices)
139        );
140        assert_eq!(
141            "update_services".parse::<Permission>().ok(),
142            Some(Permission::UpdateServices)
143        );
144        assert_eq!(
145            "manage_global_settings".parse::<Permission>().ok(),
146            Some(Permission::ManageGlobalSettings)
147        );
148    }
149
150    #[test]
151    fn permission_from_str_unknown_becomes_other() {
152        let p: Permission = "totally_unknown_perm".parse().unwrap();
153        assert!(matches!(p, Permission::Other(ref s) if s == "totally_unknown_perm"));
154    }
155
156    #[test]
157    fn permission_other_not_in_iter() {
158        for p in Permission::iter() {
159            assert!(
160                !matches!(p, Permission::Other(_)),
161                "Other should not appear in iter"
162            );
163        }
164    }
165
166    #[test]
167    fn permission_other_as_str_returns_inner() {
168        let p = Permission::Other("foo_bar".to_string());
169        assert_eq!(p.as_str(), "foo_bar");
170    }
171
172    #[test]
173    fn permission_display_matches_as_str() {
174        for perm in Permission::iter() {
175            assert_eq!(format!("{perm}"), perm.as_str());
176        }
177    }
178
179    #[test]
180    fn permission_iter_covers_all_variants() {
181        // 36 after ViewInstanceConfigState + ManageInstanceConfigState added
182        assert_eq!(Permission::iter().count(), 36);
183    }
184
185    #[test]
186    fn permission_iter_covers_all_variants_with_access_mcp() {
187        // 36 after ViewInstanceConfigState + ManageInstanceConfigState added
188        assert_eq!(Permission::iter().count(), 36);
189    }
190
191    #[test]
192    fn access_mcp_as_str() {
193        assert_eq!(Permission::AccessMcp.as_str(), "access_mcp");
194    }
195
196    #[test]
197    fn access_mcp_round_trips_from_str() {
198        let p: Permission = "access_mcp".parse().unwrap();
199        assert_eq!(p, Permission::AccessMcp);
200    }
201
202    #[test]
203    fn permission_as_str_round_trips_through_from_str() {
204        for perm in Permission::iter() {
205            let s = perm.as_str();
206            let parsed: Permission = s
207                .parse()
208                .expect("from_str should succeed for as_str output");
209            assert_eq!(parsed, perm);
210        }
211    }
212
213    // ── 2. RegistrationMode enum round-trip ──────────────────────────────
214
215    #[test]
216    fn registration_mode_serde_round_trip() {
217        for mode in RegistrationMode::iter() {
218            let json = serde_json::to_string(&mode).unwrap();
219            let deserialized: RegistrationMode = serde_json::from_str(&json).unwrap();
220            assert_eq!(deserialized, mode);
221        }
222    }
223
224    #[test]
225    fn registration_mode_as_str_values() {
226        assert_eq!(RegistrationMode::Open.as_str(), "open");
227        assert_eq!(RegistrationMode::Invite.as_str(), "invite");
228        assert_eq!(RegistrationMode::Closed.as_str(), "closed");
229    }
230
231    #[test]
232    fn registration_mode_from_str_valid() {
233        assert_eq!(
234            "open".parse::<RegistrationMode>().ok(),
235            Some(RegistrationMode::Open)
236        );
237        assert_eq!(
238            "invite".parse::<RegistrationMode>().ok(),
239            Some(RegistrationMode::Invite)
240        );
241        assert_eq!(
242            "closed".parse::<RegistrationMode>().ok(),
243            Some(RegistrationMode::Closed)
244        );
245    }
246
247    #[test]
248    fn registration_mode_from_str_invalid_returns_none() {
249        assert!("disabled".parse::<RegistrationMode>().is_err());
250        assert!("".parse::<RegistrationMode>().is_err());
251        assert!("OPEN".parse::<RegistrationMode>().is_err());
252    }
253
254    #[test]
255    fn registration_mode_as_str_round_trips_through_from_str() {
256        for mode in RegistrationMode::iter() {
257            let s = mode.as_str();
258            let parsed: RegistrationMode = s
259                .parse()
260                .expect("from_str should succeed for as_str output");
261            assert_eq!(parsed, mode);
262        }
263    }
264
265    #[test]
266    fn registration_mode_display_matches_as_str() {
267        for mode in RegistrationMode::iter() {
268            assert_eq!(format!("{mode}"), mode.as_str());
269        }
270    }
271
272    // ── 4. Serde defaults ─────────────────────────────────────────────────
273
274    #[test]
275    fn create_oidc_provider_request_default_scopes() {
276        let json = serde_json::json!({
277            "name": "Test Provider",
278            "slug": "test",
279            "issuer_url": "https://issuer.example.com",
280            "client_id": "client-id",
281            "client_secret": "client-secret"
282        });
283        let req: CreateOidcProviderRequest = serde_json::from_value(json).unwrap();
284        assert_eq!(req.scopes, "openid email profile groups");
285    }
286
287    #[test]
288    fn create_oidc_provider_request_default_auto_create() {
289        let json = serde_json::json!({
290            "name": "Test Provider",
291            "slug": "test",
292            "issuer_url": "https://issuer.example.com",
293            "client_id": "client-id",
294            "client_secret": "client-secret"
295        });
296        let req: CreateOidcProviderRequest = serde_json::from_value(json).unwrap();
297        assert!(req.auto_create_users);
298    }
299
300    #[test]
301    fn create_oidc_provider_request_explicit_overrides_defaults() {
302        let json = serde_json::json!({
303            "name": "Test Provider",
304            "slug": "test",
305            "issuer_url": "https://issuer.example.com",
306            "client_id": "client-id",
307            "client_secret": "client-secret",
308            "scopes": "openid",
309            "auto_create_users": false
310        });
311        let req: CreateOidcProviderRequest = serde_json::from_value(json).unwrap();
312        assert_eq!(req.scopes, "openid");
313        assert!(!req.auto_create_users);
314    }
315
316    #[test]
317    fn create_software_item_request_default_featured() {
318        let json = serde_json::json!({ "name": "Node.js" });
319        let req: CreateSoftwareItemRequest = serde_json::from_value(json).unwrap();
320        assert!(req.featured);
321    }
322
323    #[test]
324    fn create_software_item_request_explicit_featured_false() {
325        let json = serde_json::json!({
326            "name": "Node.js",
327            "featured": false
328        });
329        let req: CreateSoftwareItemRequest = serde_json::from_value(json).unwrap();
330        assert!(!req.featured);
331    }
332
333    #[test]
334    fn create_plugin_config_request_default_enabled() {
335        let json = serde_json::json!({
336            "name": "GitHub Releases",
337            "plugin_type": "releases_github",
338            "config": {}
339        });
340        let req: CreatePluginConfigRequest = serde_json::from_value(json).unwrap();
341        assert!(req.enabled);
342    }
343
344    #[test]
345    fn create_plugin_config_request_explicit_enabled_false() {
346        let json = serde_json::json!({
347            "name": "GitHub Releases",
348            "plugin_type": "releases_github",
349            "config": {},
350            "enabled": false
351        });
352        let req: CreatePluginConfigRequest = serde_json::from_value(json).unwrap();
353        assert!(!req.enabled);
354    }
355
356    // ── 5. Option fields serialize as null ─────────────────────────────────
357    // (device_auth_poll_response tests removed — DeviceAuthPollResponse deleted in RFC 8628 migration)
358
359    // ── 6. Representative struct round-trips ──────────────────────────────
360
361    #[test]
362    fn user_response_round_trip() {
363        let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("valid uuid");
364        let user = UserResponse {
365            id: user_id,
366            email: "owner@example.com".to_string(),
367            first_name: "Owner".to_string(),
368            last_name: "User".to_string(),
369            permissions: vec![
370                Permission::ViewSettings,
371                Permission::ManageAuthSettings,
372                Permission::ViewServices,
373                Permission::UpdateServices,
374                Permission::ManageGlobalSettings,
375            ],
376            has_pending_email_change: false,
377        };
378        let json = serde_json::to_string(&user).unwrap();
379        let deserialized: UserResponse = serde_json::from_str(&json).unwrap();
380
381        assert_eq!(deserialized.id, user_id);
382        assert_eq!(deserialized.email, "owner@example.com");
383        assert_eq!(deserialized.first_name, "Owner");
384        assert_eq!(deserialized.last_name, "User");
385        assert_eq!(deserialized.permissions.len(), 5);
386        assert_eq!(deserialized.permissions[0], Permission::ViewSettings);
387        assert_eq!(deserialized.permissions[1], Permission::ManageAuthSettings);
388        assert_eq!(deserialized.permissions[2], Permission::ViewServices);
389        assert_eq!(deserialized.permissions[3], Permission::UpdateServices);
390        assert_eq!(
391            deserialized.permissions[4],
392            Permission::ManageGlobalSettings
393        );
394    }
395
396    #[test]
397    fn user_response_empty_permissions() {
398        let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").expect("valid uuid");
399        let user = UserResponse {
400            id: user_id,
401            email: "viewer@example.com".to_string(),
402            first_name: "View".to_string(),
403            last_name: "Only".to_string(),
404            permissions: vec![],
405            has_pending_email_change: false,
406        };
407        let json = serde_json::to_string(&user).unwrap();
408        let deserialized: UserResponse = serde_json::from_str(&json).unwrap();
409
410        assert!(deserialized.permissions.is_empty());
411    }
412
413    #[test]
414    fn auth_response_round_trip() {
415        let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("valid uuid");
416        let auth = AuthResponse {
417            access_token: SecretString::new("eyJhbGciOiJIUzI1NiJ9.test"),
418            refresh_token: SecretString::new("refresh-token-value"),
419            expires_in: 3600,
420            token_type: "Bearer".to_string(),
421            user: UserResponse {
422                id: user_id,
423                email: "admin@example.com".to_string(),
424                first_name: "Admin".to_string(),
425                last_name: "User".to_string(),
426                permissions: vec![Permission::ViewSettings, Permission::UpdateServices],
427                has_pending_email_change: false,
428            },
429        };
430        let json = serde_json::to_string(&auth).unwrap();
431        let deserialized: AuthResponse = serde_json::from_str(&json).unwrap();
432
433        assert_eq!(
434            deserialized.access_token.expose_secret(),
435            "eyJhbGciOiJIUzI1NiJ9.test"
436        );
437        assert_eq!(
438            deserialized.refresh_token.expose_secret(),
439            "refresh-token-value"
440        );
441        assert_eq!(deserialized.expires_in, 3600);
442        assert_eq!(deserialized.token_type, "Bearer");
443        assert_eq!(deserialized.user.id, user_id);
444        assert_eq!(deserialized.user.email, "admin@example.com");
445        assert_eq!(deserialized.user.permissions.len(), 2);
446        assert_eq!(deserialized.user.permissions[0], Permission::ViewSettings);
447        assert_eq!(deserialized.user.permissions[1], Permission::UpdateServices);
448    }
449
450    // ── 6. UpdateStatus enum round-trip ─────────────────────────────────
451
452    #[test]
453    fn update_status_serde_round_trip() {
454        for status in UpdateStatus::iter() {
455            let json = serde_json::to_string(&status).unwrap();
456            let deserialized: UpdateStatus = serde_json::from_str(&json).unwrap();
457            assert_eq!(deserialized, status);
458        }
459    }
460
461    #[test]
462    fn update_status_as_str_values() {
463        assert_eq!(UpdateStatus::Queued.as_str(), "queued");
464        assert_eq!(UpdateStatus::Pending.as_str(), "pending");
465        assert_eq!(UpdateStatus::InProgress.as_str(), "in_progress");
466        assert_eq!(UpdateStatus::AwaitingRestart.as_str(), "awaiting_restart");
467        assert_eq!(UpdateStatus::Completed.as_str(), "completed");
468        assert_eq!(UpdateStatus::Failed.as_str(), "failed");
469    }
470
471    #[test]
472    fn update_status_from_str_valid() {
473        assert_eq!(
474            "queued".parse::<UpdateStatus>().ok(),
475            Some(UpdateStatus::Queued)
476        );
477        assert_eq!(
478            "pending".parse::<UpdateStatus>().ok(),
479            Some(UpdateStatus::Pending)
480        );
481        assert_eq!(
482            "in_progress".parse::<UpdateStatus>().ok(),
483            Some(UpdateStatus::InProgress)
484        );
485        assert_eq!(
486            "completed".parse::<UpdateStatus>().ok(),
487            Some(UpdateStatus::Completed)
488        );
489        assert_eq!(
490            "failed".parse::<UpdateStatus>().ok(),
491            Some(UpdateStatus::Failed)
492        );
493    }
494
495    #[test]
496    fn update_status_from_str_invalid_returns_none() {
497        assert!("unknown".parse::<UpdateStatus>().is_err());
498        assert!("".parse::<UpdateStatus>().is_err());
499        assert!("PENDING".parse::<UpdateStatus>().is_err());
500    }
501
502    #[test]
503    fn update_status_as_str_round_trips_through_from_str() {
504        for status in UpdateStatus::iter() {
505            let s = status.as_str();
506            let parsed: UpdateStatus = s
507                .parse()
508                .expect("from_str should succeed for as_str output");
509            assert_eq!(parsed, status);
510        }
511    }
512
513    #[test]
514    fn update_status_display_matches_as_str() {
515        for status in UpdateStatus::iter() {
516            assert_eq!(format!("{status}"), status.as_str());
517        }
518    }
519
520    // ── 8. ErrorResponse round-trip ─────────────────────────────────────
521
522    #[test]
523    fn error_response_serialization_without_code() {
524        let resp = ErrorResponse {
525            error: "Something went wrong".to_string(),
526            code: None,
527        };
528        let json = serde_json::to_value(&resp).unwrap();
529        let obj = json.as_object().unwrap();
530
531        assert_eq!(obj.get("error").unwrap(), "Something went wrong");
532        assert!(
533            obj.get("code").unwrap().is_null(),
534            "code should serialize as null when None"
535        );
536    }
537
538    #[test]
539    fn error_response_serialization_with_code() {
540        let resp = ErrorResponse {
541            error: "Not found".to_string(),
542            code: Some("not_found".to_string()),
543        };
544        let json = serde_json::to_value(&resp).unwrap();
545        let obj = json.as_object().unwrap();
546
547        assert_eq!(obj.get("error").unwrap(), "Not found");
548        assert_eq!(obj.get("code").unwrap(), "not_found");
549    }
550
551    #[test]
552    fn error_response_round_trip_without_code() {
553        let resp = ErrorResponse {
554            error: "Bad request".to_string(),
555            code: None,
556        };
557        let json = serde_json::to_string(&resp).unwrap();
558        let deserialized: ErrorResponse = serde_json::from_str(&json).unwrap();
559
560        assert_eq!(deserialized.error, "Bad request");
561        assert!(deserialized.code.is_none());
562    }
563
564    #[test]
565    fn error_response_round_trip_with_code() {
566        let resp = ErrorResponse {
567            error: "Forbidden".to_string(),
568            code: Some("insufficient_permissions".to_string()),
569        };
570        let json = serde_json::to_string(&resp).unwrap();
571        let deserialized: ErrorResponse = serde_json::from_str(&json).unwrap();
572
573        assert_eq!(deserialized.error, "Forbidden");
574        assert_eq!(
575            deserialized.code.as_deref(),
576            Some("insufficient_permissions")
577        );
578    }
579
580    #[test]
581    fn error_response_deserialization_without_code_field() {
582        let json = r#"{"error":"test error"}"#;
583        let resp: ErrorResponse = serde_json::from_str(json).unwrap();
584
585        assert_eq!(resp.error, "test error");
586        assert!(resp.code.is_none());
587    }
588
589    // ── 9. Pagination ────────────────────────────────────────────────────
590
591    #[test]
592    fn pagination_resolve_defaults() {
593        use crate::pagination::{DEFAULT_PER_PAGE, PaginationParams};
594        let params = PaginationParams {
595            page: None,
596            per_page: None,
597        };
598        let resolved = params.resolve();
599        assert_eq!(resolved.page, 1);
600        assert_eq!(resolved.per_page, DEFAULT_PER_PAGE);
601    }
602
603    #[test]
604    fn pagination_resolve_explicit_values() {
605        use crate::pagination::PaginationParams;
606        let params = PaginationParams {
607            page: Some(3),
608            per_page: Some(50),
609        };
610        let resolved = params.resolve();
611        assert_eq!(resolved.page, 3);
612        assert_eq!(resolved.per_page, 50);
613    }
614
615    #[test]
616    fn pagination_resolve_clamps_page_zero_to_one() {
617        use crate::pagination::PaginationParams;
618        let params = PaginationParams {
619            page: Some(0),
620            per_page: None,
621        };
622        let resolved = params.resolve();
623        assert_eq!(resolved.page, 1);
624    }
625
626    #[test]
627    fn pagination_resolve_clamps_per_page_zero_to_one() {
628        use crate::pagination::PaginationParams;
629        let params = PaginationParams {
630            page: None,
631            per_page: Some(0),
632        };
633        let resolved = params.resolve();
634        assert_eq!(resolved.per_page, 1);
635    }
636
637    #[test]
638    fn pagination_resolve_clamps_per_page_above_max() {
639        use crate::pagination::{MAX_PER_PAGE, PaginationParams};
640        let params = PaginationParams {
641            page: None,
642            per_page: Some(5000),
643        };
644        let resolved = params.resolve();
645        assert_eq!(resolved.per_page, MAX_PER_PAGE);
646    }
647
648    #[test]
649    fn pagination_offset_calculation() {
650        use crate::pagination::ResolvedPagination;
651        let p = ResolvedPagination {
652            page: 1,
653            per_page: 20,
654        };
655        assert_eq!(p.offset(), 0);
656
657        let p = ResolvedPagination {
658            page: 2,
659            per_page: 20,
660        };
661        assert_eq!(p.offset(), 20);
662
663        let p = ResolvedPagination {
664            page: 3,
665            per_page: 50,
666        };
667        assert_eq!(p.offset(), 100);
668    }
669
670    #[test]
671    fn pagination_total_pages_calculation() {
672        use crate::pagination::ResolvedPagination;
673        let p = ResolvedPagination {
674            page: 1,
675            per_page: 20,
676        };
677        assert_eq!(p.total_pages(0), 0);
678        assert_eq!(p.total_pages(1), 1);
679        assert_eq!(p.total_pages(20), 1);
680        assert_eq!(p.total_pages(21), 2);
681        assert_eq!(p.total_pages(100), 5);
682    }
683
684    #[test]
685    fn paginated_response_serialization_round_trip() {
686        use crate::pagination::{PaginatedResponse, ResolvedPagination};
687        let pagination = ResolvedPagination {
688            page: 2,
689            per_page: 10,
690        };
691        let resp = PaginatedResponse::new(vec!["a".to_string(), "b".to_string()], 25, pagination);
692
693        let json = serde_json::to_string(&resp).unwrap();
694        let deserialized: PaginatedResponse<String> = serde_json::from_str(&json).unwrap();
695
696        assert_eq!(deserialized.items, vec!["a", "b"]);
697        assert_eq!(deserialized.total, 25);
698        assert_eq!(deserialized.page, 2);
699        assert_eq!(deserialized.per_page, 10);
700        assert_eq!(deserialized.total_pages, 3);
701    }
702
703    #[test]
704    fn paginated_response_empty_items() {
705        use crate::pagination::{PaginatedResponse, ResolvedPagination};
706        let pagination = ResolvedPagination {
707            page: 1,
708            per_page: 20,
709        };
710        let resp = PaginatedResponse::<String>::new(vec![], 0, pagination);
711
712        assert!(resp.items.is_empty());
713        assert_eq!(resp.total, 0);
714        assert_eq!(resp.total_pages, 0);
715    }
716
717    // ── 10. Prelude compile check ───────────────────────────────────────
718
719    #[test]
720    fn prelude_re_exports_resolve() {
721        use crate::prelude::*;
722
723        // Auth
724        let _: AuthResponse;
725        let _: UserResponse;
726
727        // Services
728        let _ = ServiceStatus::Pending;
729
730        // Hosts
731        let _: HostResponse;
732
733        // Software items
734        let _: SoftwareItemResponse;
735        let _ = TriggerUpdateStatus::Pending;
736
737        // Plugin configs
738        let _: PluginConfigResponse;
739
740        // Update history
741        let _ = UpdateStatus::Pending;
742
743        // API tokens
744        let _: ApiTokenListResponse;
745        let _: CreateApiTokenResponse;
746
747        // OIDC
748        let _: OidcProviderResponse;
749        let _: AuthMethodsResponse;
750
751        // Notifications
752        let _ = NotificationEventType::UpdateAvailable;
753        let _ = NotificationDeliveryStatus::Pending;
754        let _: NotificationChannelResponse;
755        let _: NotificationRuleResponse;
756        let _: NotificationLogResponse;
757        let _: TestNotificationResponse;
758
759        // Common
760        let _: ErrorResponse;
761        let _: PaginatedResponse<String>;
762        let _: PaginationParams;
763        let _ = Permission::ViewSettings;
764        let _ = RegistrationMode::Open;
765    }
766
767    // ── 11. Request type validation ─────────────────────────────────────
768
769    #[test]
770    fn register_request_valid() {
771        use crate::auth::RegisterRequest;
772        use crate::validation::Validate;
773        let req = RegisterRequest {
774            email: "user@example.com".to_string(),
775            first_name: "John".to_string(),
776            last_name: "Doe".to_string(),
777            password: SecretString::new("password12345678"),
778            registration_token: None,
779        };
780        assert!(req.validate().is_ok());
781    }
782
783    #[test]
784    fn register_request_email_missing_at() {
785        use crate::auth::RegisterRequest;
786        use crate::validation::Validate;
787        let req = RegisterRequest {
788            email: "invalid-email".to_string(),
789            first_name: "John".to_string(),
790            last_name: "Doe".to_string(),
791            password: SecretString::new("password12345678"),
792            registration_token: None,
793        };
794        let err = req.validate().unwrap_err();
795        assert_eq!(err.field, "email");
796    }
797
798    #[test]
799    fn register_request_password_too_short() {
800        use crate::auth::RegisterRequest;
801        use crate::validation::Validate;
802        let req = RegisterRequest {
803            email: "user@example.com".to_string(),
804            first_name: "John".to_string(),
805            last_name: "Doe".to_string(),
806            password: SecretString::new("short"),
807            registration_token: None,
808        };
809        let err = req.validate().unwrap_err();
810        assert_eq!(err.field, "password");
811    }
812
813    #[test]
814    fn register_request_empty_first_name() {
815        use crate::auth::RegisterRequest;
816        use crate::validation::Validate;
817        let req = RegisterRequest {
818            email: "user@example.com".to_string(),
819            first_name: "".to_string(),
820            last_name: "Doe".to_string(),
821            password: SecretString::new("password12345678"),
822            registration_token: None,
823        };
824        let err = req.validate().unwrap_err();
825        assert_eq!(err.field, "first_name");
826    }
827
828    #[test]
829    fn login_request_valid() {
830        use crate::auth::LoginRequest;
831        use crate::validation::Validate;
832        let req = LoginRequest {
833            email: "user@example.com".to_string(),
834            password: SecretString::new("any-password"),
835        };
836        assert!(req.validate().is_ok());
837    }
838
839    #[test]
840    fn login_request_empty_password() {
841        use crate::auth::LoginRequest;
842        use crate::validation::Validate;
843        let req = LoginRequest {
844            email: "user@example.com".to_string(),
845            password: SecretString::new(""),
846        };
847        let err = req.validate().unwrap_err();
848        assert_eq!(err.field, "password");
849    }
850
851    #[test]
852    fn create_oidc_provider_valid() {
853        use crate::validation::Validate;
854        let req = CreateOidcProviderRequest {
855            name: "Test Provider".to_string(),
856            slug: "test-provider".to_string(),
857            logo_url: None,
858            issuer_url: "https://issuer.example.com".to_string(),
859            client_id: "client-id".to_string(),
860            client_secret: SecretString::new("secret"),
861            scopes: "openid email".to_string(),
862            auto_create_users: true,
863            allow_private_network_issuers: Some(true),
864            role_claim_path: None,
865            role_mapping: Default::default(),
866        };
867        assert!(req.validate().is_ok());
868    }
869
870    #[test]
871    fn create_oidc_provider_invalid_slug() {
872        use crate::validation::Validate;
873        let req = CreateOidcProviderRequest {
874            name: "Test".to_string(),
875            slug: "INVALID_SLUG".to_string(),
876            logo_url: None,
877            issuer_url: "https://issuer.example.com".to_string(),
878            client_id: "client-id".to_string(),
879            client_secret: SecretString::new("secret"),
880            scopes: "openid".to_string(),
881            auto_create_users: true,
882            allow_private_network_issuers: Some(true),
883            role_claim_path: None,
884            role_mapping: Default::default(),
885        };
886        let err = req.validate().unwrap_err();
887        assert_eq!(err.field, "slug");
888    }
889
890    #[test]
891    fn create_oidc_provider_invalid_issuer_url() {
892        use crate::validation::Validate;
893        let req = CreateOidcProviderRequest {
894            name: "Test".to_string(),
895            slug: "test".to_string(),
896            logo_url: None,
897            issuer_url: "ftp://issuer.example.com".to_string(),
898            client_id: "client-id".to_string(),
899            client_secret: SecretString::new("secret"),
900            scopes: "openid".to_string(),
901            auto_create_users: true,
902            allow_private_network_issuers: Some(true),
903            role_claim_path: None,
904            role_mapping: Default::default(),
905        };
906        let err = req.validate().unwrap_err();
907        assert_eq!(err.field, "issuer_url");
908    }
909
910    #[test]
911    fn create_software_item_valid() {
912        use crate::validation::Validate;
913        let req = CreateSoftwareItemRequest {
914            name: "Node.js".to_string(),
915            featured: true,
916            icon_url: None,
917        };
918        assert!(req.validate().is_ok());
919    }
920
921    #[test]
922    fn create_software_item_empty_name_fails() {
923        use crate::validation::Validate;
924        let req = CreateSoftwareItemRequest {
925            name: "".to_string(),
926            featured: true,
927            icon_url: None,
928        };
929        let err = req.validate().unwrap_err();
930        assert_eq!(err.field, "name");
931    }
932
933    #[test]
934    fn create_plugin_config_valid() {
935        use crate::validation::Validate;
936        use uptrakit_shared_types::plugin_ids;
937        let req = CreatePluginConfigRequest {
938            name: "GitHub Releases".to_string(),
939            plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
940            config: serde_json::json!({}),
941            enabled: true,
942        };
943        assert!(req.validate().is_ok());
944    }
945
946    #[test]
947    fn create_plugin_config_empty_name() {
948        use crate::validation::Validate;
949        use uptrakit_shared_types::plugin_ids;
950        let req = CreatePluginConfigRequest {
951            name: "".to_string(),
952            plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
953            config: serde_json::json!({}),
954            enabled: true,
955        };
956        let err = req.validate().unwrap_err();
957        assert_eq!(err.field, "name");
958    }
959
960    #[test]
961    fn update_scheduled_task_valid_interval() {
962        use crate::scheduler::UpdateScheduledTaskRequest;
963        use crate::validation::Validate;
964        let req = UpdateScheduledTaskRequest {
965            interval_seconds: Some(300),
966            jitter_seconds: Some(30),
967            enabled: None,
968            task_config: None,
969        };
970        assert!(req.validate().is_ok());
971    }
972
973    #[test]
974    fn update_scheduled_task_invalid_interval() {
975        use crate::scheduler::UpdateScheduledTaskRequest;
976        use crate::validation::Validate;
977        let req = UpdateScheduledTaskRequest {
978            interval_seconds: Some(0),
979            jitter_seconds: None,
980            enabled: None,
981            task_config: None,
982        };
983        let err = req.validate().unwrap_err();
984        assert_eq!(err.field, "interval_seconds");
985    }
986
987    #[test]
988    fn update_network_settings_valid() {
989        use crate::settings_network::UpdateNetworkSettingsRequest;
990        use crate::validation::Validate;
991        let req = UpdateNetworkSettingsRequest {
992            trusted_proxies: Some(vec!["10.0.0.0/8".to_string()]),
993            real_ip_header: Some("X-Real-IP".to_string()),
994            sans: None,
995            https_addr: None,
996            forwarded_client_cert_info_header: None,
997            forwarded_client_cert_pem_header: None,
998            pki_addr: Some("https://pki.example.com".to_string()),
999            regenerate_cert: None,
1000        };
1001        assert!(req.validate().is_ok());
1002    }
1003
1004    #[test]
1005    fn update_network_settings_empty_trusted_proxy() {
1006        use crate::settings_network::UpdateNetworkSettingsRequest;
1007        use crate::validation::Validate;
1008        let req = UpdateNetworkSettingsRequest {
1009            trusted_proxies: Some(vec!["".to_string()]),
1010            real_ip_header: None,
1011            sans: None,
1012            https_addr: None,
1013            forwarded_client_cert_info_header: None,
1014            forwarded_client_cert_pem_header: None,
1015            pki_addr: None,
1016            regenerate_cert: None,
1017        };
1018        let err = req.validate().unwrap_err();
1019        assert_eq!(err.field, "trusted_proxies");
1020    }
1021
1022    // (device_auth SSE type tests removed — DeviceAuthAuthorizedSse / DeviceAuthExpiredSse
1023    //  deleted in RFC 8628 migration)
1024}