1use crate::{
2 save_admin_session, AdminSessionState, ApprovalEntryRecord, AuthGetInstalledContractRequest,
3 AuthGetInstalledContractResponse, AuthInstallServiceRequest, AuthUpgradeServiceContractRequest,
4 AuthValidateRequestRequest, AuthValidateRequestResponse, AuthenticatedUser, BoundSession,
5 ListApprovalsRequest, RenewBindingTokenResponse, RevokeApprovalRequest, ServiceListEntry,
6 TrellisAuthError,
7};
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::BTreeMap;
11use trellis_client::{TrellisClient, UserConnectOptions};
12
13use crate::protocol::{
14 AuthInstallServiceResponse, AuthUpgradeServiceContractResponse, ListApprovalsResponse,
15 ListServicesResponse, LogoutResponse, MeResponse, RevokeApprovalResponse,
16};
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct PortalRecord {
21 pub portal_id: String,
22 pub app_contract_id: Option<String>,
23 pub entry_url: String,
24 pub disabled: bool,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct PortalDefaultRecord {
30 pub portal_id: Option<String>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct LoginPortalSelectionRecord {
36 pub contract_id: String,
37 pub portal_id: Option<String>,
38}
39
40#[derive(Debug, Clone, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct WorkloadPortalSelectionRecord {
43 pub profile_id: String,
44 pub portal_id: Option<String>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48#[serde(rename_all = "camelCase")]
49struct CreatePortalResponse {
50 portal: PortalRecord,
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize)]
54struct ListPortalsResponse {
55 portals: Vec<PortalRecord>,
56}
57
58#[derive(Debug, Clone, Deserialize, Serialize)]
59struct DisablePortalResponse {
60 success: bool,
61}
62
63#[derive(Debug, Clone, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65struct GetPortalDefaultResponse {
66 default_portal: PortalDefaultRecord,
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
70#[serde(rename_all = "camelCase")]
71struct SetPortalDefaultRequest {
72 portal_id: Option<String>,
73}
74
75#[derive(Debug, Clone, Deserialize, Serialize)]
76#[serde(rename_all = "camelCase")]
77struct SetPortalDefaultResponse {
78 default_portal: PortalDefaultRecord,
79}
80
81#[derive(Debug, Clone, Deserialize, Serialize)]
82struct ListLoginPortalSelectionsResponse {
83 selections: Vec<LoginPortalSelectionRecord>,
84}
85
86#[derive(Debug, Clone, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase")]
88struct SetLoginPortalSelectionRequest {
89 contract_id: String,
90 portal_id: Option<String>,
91}
92
93#[derive(Debug, Clone, Deserialize, Serialize)]
94struct SetLoginPortalSelectionResponse {
95 selection: LoginPortalSelectionRecord,
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize)]
99#[serde(rename_all = "camelCase")]
100struct ClearLoginPortalSelectionRequest {
101 contract_id: String,
102}
103
104#[derive(Debug, Clone, Deserialize, Serialize)]
105struct ClearLoginPortalSelectionResponse {
106 success: bool,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
110struct ListWorkloadPortalSelectionsResponse {
111 selections: Vec<WorkloadPortalSelectionRecord>,
112}
113
114#[derive(Debug, Clone, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116struct SetWorkloadPortalSelectionRequest {
117 profile_id: String,
118 portal_id: Option<String>,
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize)]
122struct SetWorkloadPortalSelectionResponse {
123 selection: WorkloadPortalSelectionRecord,
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize)]
127#[serde(rename_all = "camelCase")]
128struct ClearWorkloadPortalSelectionRequest {
129 profile_id: String,
130}
131
132#[derive(Debug, Clone, Deserialize, Serialize)]
133struct ClearWorkloadPortalSelectionResponse {
134 success: bool,
135}
136
137#[derive(Debug, Clone, Deserialize, Serialize)]
138#[serde(rename_all = "camelCase")]
139struct CreatePortalRequest {
140 portal_id: String,
141 app_contract_id: Option<String>,
142 entry_url: String,
143}
144
145pub async fn connect_admin_client_async(
147 state: &AdminSessionState,
148) -> Result<TrellisClient, TrellisAuthError> {
149 Ok(TrellisClient::connect_user(UserConnectOptions {
150 servers: &state.nats_servers,
151 sentinel_jwt: &state.sentinel_jwt,
152 sentinel_seed: &state.sentinel_seed,
153 session_key_seed_base64url: &state.session_seed,
154 binding_token: &state.binding_token,
155 timeout_ms: 5_000,
156 })
157 .await?)
158}
159
160pub fn persist_renewed_admin_session(
162 state: &mut AdminSessionState,
163 renewed: RenewBindingTokenResponse,
164) -> Result<(), TrellisAuthError> {
165 let renewed = BoundSession {
166 binding_token: renewed.binding_token,
167 inbox_prefix: renewed.inbox_prefix,
168 expires: renewed.expires,
169 sentinel: renewed.sentinel,
170 };
171
172 state.binding_token = renewed.binding_token;
173 state.expires = renewed.expires;
174 state.sentinel_jwt = renewed.sentinel.jwt;
175 state.sentinel_seed = renewed.sentinel.seed;
176 save_admin_session(state)
177}
178
179pub struct AuthClient<'a> {
181 inner: &'a TrellisClient,
182}
183
184impl<'a> AuthClient<'a> {
185 pub fn new(inner: &'a TrellisClient) -> Self {
187 Self { inner }
188 }
189
190 async fn call<Input, Output>(
191 &self,
192 subject: &str,
193 input: &Input,
194 ) -> Result<Output, TrellisAuthError>
195 where
196 Input: Serialize,
197 Output: DeserializeOwned,
198 {
199 let request = serde_json::to_value(input)?;
200 let response = self.inner.request_json_value(subject, &request).await?;
201 Ok(serde_json::from_value(response)?)
202 }
203
204 pub async fn me(&self) -> Result<AuthenticatedUser, TrellisAuthError> {
206 Ok(self
207 .call::<_, MeResponse>("rpc.v1.Auth.Me", &Empty {})
208 .await?
209 .user)
210 }
211
212 pub async fn list_approvals(
214 &self,
215 user: Option<&str>,
216 digest: Option<&str>,
217 ) -> Result<Vec<ApprovalEntryRecord>, TrellisAuthError> {
218 let request = ListApprovalsRequest {
219 user: user.map(ToOwned::to_owned),
220 digest: digest.map(ToOwned::to_owned),
221 };
222 Ok(self
223 .call::<_, ListApprovalsResponse>("rpc.v1.Auth.ListApprovals", &request)
224 .await?
225 .approvals)
226 }
227
228 pub async fn revoke_approval(
230 &self,
231 digest: &str,
232 user: Option<&str>,
233 ) -> Result<bool, TrellisAuthError> {
234 let request = RevokeApprovalRequest {
235 contract_digest: digest.to_string(),
236 user: user.map(ToOwned::to_owned),
237 };
238 Ok(self
239 .call::<_, RevokeApprovalResponse>("rpc.v1.Auth.RevokeApproval", &request)
240 .await?
241 .success)
242 }
243
244 pub async fn list_portals(&self) -> Result<Vec<PortalRecord>, TrellisAuthError> {
246 Ok(self
247 .call::<_, ListPortalsResponse>("rpc.v1.Auth.ListPortals", &trellis_sdk_auth::Empty {})
248 .await?
249 .portals)
250 }
251
252 pub async fn create_portal(
254 &self,
255 portal_id: &str,
256 app_contract_id: Option<&str>,
257 entry_url: &str,
258 ) -> Result<PortalRecord, TrellisAuthError> {
259 Ok(self
260 .call::<_, CreatePortalResponse>(
261 "rpc.v1.Auth.CreatePortal",
262 &CreatePortalRequest {
263 portal_id: portal_id.to_string(),
264 app_contract_id: app_contract_id.map(ToOwned::to_owned),
265 entry_url: entry_url.to_string(),
266 },
267 )
268 .await?
269 .portal)
270 }
271
272 pub async fn disable_portal(&self, portal_id: &str) -> Result<bool, TrellisAuthError> {
274 Ok(self
275 .call::<_, DisablePortalResponse>(
276 "rpc.v1.Auth.DisablePortal",
277 &trellis_sdk_auth::AuthDisablePortalRequest {
278 portal_id: portal_id.to_string(),
279 },
280 )
281 .await?
282 .success)
283 }
284
285 pub async fn get_login_portal_default(
287 &self,
288 ) -> Result<PortalDefaultRecord, TrellisAuthError> {
289 Ok(self
290 .call::<_, GetPortalDefaultResponse>(
291 "rpc.v1.Auth.GetLoginPortalDefault",
292 &trellis_sdk_auth::Empty {},
293 )
294 .await?
295 .default_portal)
296 }
297
298 pub async fn set_login_portal_default(
300 &self,
301 portal_id: Option<&str>,
302 ) -> Result<PortalDefaultRecord, TrellisAuthError> {
303 Ok(self
304 .call::<_, SetPortalDefaultResponse>(
305 "rpc.v1.Auth.SetLoginPortalDefault",
306 &SetPortalDefaultRequest {
307 portal_id: portal_id.map(ToOwned::to_owned),
308 },
309 )
310 .await?
311 .default_portal)
312 }
313
314 pub async fn list_login_portal_selections(
316 &self,
317 ) -> Result<Vec<LoginPortalSelectionRecord>, TrellisAuthError> {
318 Ok(self
319 .call::<_, ListLoginPortalSelectionsResponse>(
320 "rpc.v1.Auth.ListLoginPortalSelections",
321 &trellis_sdk_auth::Empty {},
322 )
323 .await?
324 .selections)
325 }
326
327 pub async fn set_login_portal_selection(
329 &self,
330 contract_id: &str,
331 portal_id: Option<&str>,
332 ) -> Result<LoginPortalSelectionRecord, TrellisAuthError> {
333 Ok(self
334 .call::<_, SetLoginPortalSelectionResponse>(
335 "rpc.v1.Auth.SetLoginPortalSelection",
336 &SetLoginPortalSelectionRequest {
337 contract_id: contract_id.to_string(),
338 portal_id: portal_id.map(ToOwned::to_owned),
339 },
340 )
341 .await?
342 .selection)
343 }
344
345 pub async fn clear_login_portal_selection(
347 &self,
348 contract_id: &str,
349 ) -> Result<bool, TrellisAuthError> {
350 Ok(self
351 .call::<_, ClearLoginPortalSelectionResponse>(
352 "rpc.v1.Auth.ClearLoginPortalSelection",
353 &ClearLoginPortalSelectionRequest {
354 contract_id: contract_id.to_string(),
355 },
356 )
357 .await?
358 .success)
359 }
360
361 pub async fn get_workload_portal_default(
363 &self,
364 ) -> Result<PortalDefaultRecord, TrellisAuthError> {
365 Ok(self
366 .call::<_, GetPortalDefaultResponse>(
367 "rpc.v1.Auth.GetWorkloadPortalDefault",
368 &trellis_sdk_auth::Empty {},
369 )
370 .await?
371 .default_portal)
372 }
373
374 pub async fn set_workload_portal_default(
376 &self,
377 portal_id: Option<&str>,
378 ) -> Result<PortalDefaultRecord, TrellisAuthError> {
379 Ok(self
380 .call::<_, SetPortalDefaultResponse>(
381 "rpc.v1.Auth.SetWorkloadPortalDefault",
382 &SetPortalDefaultRequest {
383 portal_id: portal_id.map(ToOwned::to_owned),
384 },
385 )
386 .await?
387 .default_portal)
388 }
389
390 pub async fn list_workload_portal_selections(
392 &self,
393 ) -> Result<Vec<WorkloadPortalSelectionRecord>, TrellisAuthError> {
394 Ok(self
395 .call::<_, ListWorkloadPortalSelectionsResponse>(
396 "rpc.v1.Auth.ListWorkloadPortalSelections",
397 &trellis_sdk_auth::Empty {},
398 )
399 .await?
400 .selections)
401 }
402
403 pub async fn set_workload_portal_selection(
405 &self,
406 profile_id: &str,
407 portal_id: Option<&str>,
408 ) -> Result<WorkloadPortalSelectionRecord, TrellisAuthError> {
409 Ok(self
410 .call::<_, SetWorkloadPortalSelectionResponse>(
411 "rpc.v1.Auth.SetWorkloadPortalSelection",
412 &SetWorkloadPortalSelectionRequest {
413 profile_id: profile_id.to_string(),
414 portal_id: portal_id.map(ToOwned::to_owned),
415 },
416 )
417 .await?
418 .selection)
419 }
420
421 pub async fn clear_workload_portal_selection(
423 &self,
424 profile_id: &str,
425 ) -> Result<bool, TrellisAuthError> {
426 Ok(self
427 .call::<_, ClearWorkloadPortalSelectionResponse>(
428 "rpc.v1.Auth.ClearWorkloadPortalSelection",
429 &ClearWorkloadPortalSelectionRequest {
430 profile_id: profile_id.to_string(),
431 },
432 )
433 .await?
434 .success)
435 }
436
437 pub async fn list_workload_profiles(
439 &self,
440 contract_id: Option<&str>,
441 disabled: bool,
442 ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadProfilesResponseProfilesItem>, TrellisAuthError> {
443 Ok(self
444 .call::<_, trellis_sdk_auth::AuthListWorkloadProfilesResponse>(
445 "rpc.v1.Auth.ListWorkloadProfiles",
446 &trellis_sdk_auth::AuthListWorkloadProfilesRequest {
447 contract_id: contract_id.map(ToOwned::to_owned),
448 disabled: if disabled { Some(true) } else { None },
449 },
450 )
451 .await?
452 .profiles)
453 }
454
455 pub async fn create_workload_profile(
457 &self,
458 profile_id: &str,
459 contract_id: &str,
460 allow_digests: &[String],
461 review_mode: Option<&str>,
462 contract: Option<BTreeMap<String, Value>>,
463 ) -> Result<trellis_sdk_auth::AuthCreateWorkloadProfileResponseProfile, TrellisAuthError> {
464 #[derive(Debug, Clone, Deserialize, Serialize)]
465 #[serde(rename_all = "camelCase")]
466 struct CreateWorkloadProfileRequest {
467 allowed_digests: Vec<String>,
468 contract_id: String,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 contract: Option<BTreeMap<String, Value>>,
471 #[serde(skip_serializing_if = "Option::is_none")]
472 review_mode: Option<Value>,
473 profile_id: String,
474 }
475
476 Ok(self
477 .call::<_, trellis_sdk_auth::AuthCreateWorkloadProfileResponse>(
478 "rpc.v1.Auth.CreateWorkloadProfile",
479 &CreateWorkloadProfileRequest {
480 profile_id: profile_id.to_string(),
481 contract_id: contract_id.to_string(),
482 allowed_digests: allow_digests.to_vec(),
483 review_mode: review_mode.map(|value| serde_json::json!(value)),
484 contract,
485 },
486 )
487 .await?
488 .profile)
489 }
490
491 pub async fn disable_workload_profile(
493 &self,
494 profile_id: &str,
495 ) -> Result<bool, TrellisAuthError> {
496 Ok(self
497 .call::<_, trellis_sdk_auth::AuthDisableWorkloadProfileResponse>(
498 "rpc.v1.Auth.DisableWorkloadProfile",
499 &trellis_sdk_auth::AuthDisableWorkloadProfileRequest {
500 profile_id: profile_id.to_string(),
501 },
502 )
503 .await?
504 .success)
505 }
506
507 pub async fn provision_workload_instance(
509 &self,
510 profile_id: &str,
511 public_identity_key: &str,
512 activation_key: &str,
513 ) -> Result<trellis_sdk_auth::AuthProvisionWorkloadInstanceResponseInstance, TrellisAuthError> {
514 Ok(self
515 .call::<_, trellis_sdk_auth::AuthProvisionWorkloadInstanceResponse>(
516 "rpc.v1.Auth.ProvisionWorkloadInstance",
517 &trellis_sdk_auth::AuthProvisionWorkloadInstanceRequest {
518 profile_id: profile_id.to_string(),
519 public_identity_key: public_identity_key.to_string(),
520 activation_key: activation_key.to_string(),
521 },
522 )
523 .await?
524 .instance)
525 }
526
527 pub async fn get_workload_activation_status(
529 &self,
530 handoff_id: &str,
531 ) -> Result<trellis_sdk_auth::AuthGetWorkloadActivationStatusResponse, TrellisAuthError> {
532 self.call(
533 "rpc.v1.Auth.GetWorkloadActivationStatus",
534 &trellis_sdk_auth::AuthGetWorkloadActivationStatusRequest {
535 handoff_id: handoff_id.to_string(),
536 },
537 )
538 .await
539 }
540
541 pub async fn list_workload_instances(
543 &self,
544 profile_id: Option<&str>,
545 state: Option<&str>,
546 ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadInstancesResponseInstancesItem>, TrellisAuthError> {
547 Ok(self
548 .call::<_, trellis_sdk_auth::AuthListWorkloadInstancesResponse>(
549 "rpc.v1.Auth.ListWorkloadInstances",
550 &trellis_sdk_auth::AuthListWorkloadInstancesRequest {
551 profile_id: profile_id.map(ToOwned::to_owned),
552 state: state.map(|value| serde_json::json!(value)),
553 },
554 )
555 .await?
556 .instances)
557 }
558
559 pub async fn disable_workload_instance(
561 &self,
562 instance_id: &str,
563 ) -> Result<bool, TrellisAuthError> {
564 Ok(self
565 .call::<_, trellis_sdk_auth::AuthDisableWorkloadInstanceResponse>(
566 "rpc.v1.Auth.DisableWorkloadInstance",
567 &trellis_sdk_auth::AuthDisableWorkloadInstanceRequest {
568 instance_id: instance_id.to_string(),
569 },
570 )
571 .await?
572 .success)
573 }
574
575 pub async fn list_workload_activations(
577 &self,
578 instance_id: Option<&str>,
579 profile_id: Option<&str>,
580 state: Option<&str>,
581 ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadActivationsResponseActivationsItem>, TrellisAuthError> {
582 Ok(self
583 .call::<_, trellis_sdk_auth::AuthListWorkloadActivationsResponse>(
584 "rpc.v1.Auth.ListWorkloadActivations",
585 &trellis_sdk_auth::AuthListWorkloadActivationsRequest {
586 instance_id: instance_id.map(ToOwned::to_owned),
587 profile_id: profile_id.map(ToOwned::to_owned),
588 state: state.map(|value| serde_json::json!(value)),
589 },
590 )
591 .await?
592 .activations)
593 }
594
595 pub async fn revoke_workload_activation(
597 &self,
598 instance_id: &str,
599 ) -> Result<bool, TrellisAuthError> {
600 Ok(self
601 .call::<_, trellis_sdk_auth::AuthRevokeWorkloadActivationResponse>(
602 "rpc.v1.Auth.RevokeWorkloadActivation",
603 &trellis_sdk_auth::AuthRevokeWorkloadActivationRequest {
604 instance_id: instance_id.to_string(),
605 },
606 )
607 .await?
608 .success)
609 }
610
611 pub async fn list_workload_activation_reviews(
613 &self,
614 instance_id: Option<&str>,
615 profile_id: Option<&str>,
616 state: Option<&str>,
617 ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadActivationReviewsResponseReviewsItem>, TrellisAuthError> {
618 Ok(self
619 .call::<_, trellis_sdk_auth::AuthListWorkloadActivationReviewsResponse>(
620 "rpc.v1.Auth.ListWorkloadActivationReviews",
621 &trellis_sdk_auth::AuthListWorkloadActivationReviewsRequest {
622 instance_id: instance_id.map(ToOwned::to_owned),
623 profile_id: profile_id.map(ToOwned::to_owned),
624 state: state.map(|value| serde_json::json!(value)),
625 },
626 )
627 .await?
628 .reviews)
629 }
630
631 pub async fn decide_workload_activation_review(
633 &self,
634 review_id: &str,
635 decision: &str,
636 reason: Option<&str>,
637 ) -> Result<trellis_sdk_auth::AuthDecideWorkloadActivationReviewResponse, TrellisAuthError> {
638 self.call(
639 "rpc.v1.Auth.DecideWorkloadActivationReview",
640 &trellis_sdk_auth::AuthDecideWorkloadActivationReviewRequest {
641 review_id: review_id.to_string(),
642 decision: serde_json::json!(decision),
643 reason: reason.map(ToOwned::to_owned),
644 },
645 )
646 .await
647 }
648
649 pub async fn install_service(
651 &self,
652 input: &AuthInstallServiceRequest,
653 ) -> Result<AuthInstallServiceResponse, TrellisAuthError> {
654 self.call("rpc.v1.Auth.InstallService", input).await
655 }
656
657 pub async fn upgrade_service_contract(
659 &self,
660 input: &AuthUpgradeServiceContractRequest,
661 ) -> Result<AuthUpgradeServiceContractResponse, TrellisAuthError> {
662 self.call("rpc.v1.Auth.UpgradeServiceContract", input).await
663 }
664
665 pub async fn get_installed_contract(
667 &self,
668 input: &AuthGetInstalledContractRequest,
669 ) -> Result<AuthGetInstalledContractResponse, TrellisAuthError> {
670 self.call("rpc.v1.Auth.GetInstalledContract", input).await
671 }
672
673 pub async fn validate_request(
675 &self,
676 input: &AuthValidateRequestRequest,
677 ) -> Result<AuthValidateRequestResponse, TrellisAuthError> {
678 self.call("rpc.v1.Auth.ValidateRequest", input).await
679 }
680
681 pub async fn list_services(&self) -> Result<Vec<ServiceListEntry>, TrellisAuthError> {
683 Ok(self
684 .call::<_, ListServicesResponse>("rpc.v1.Auth.ListServices", &Empty {})
685 .await?
686 .services)
687 }
688
689 pub async fn logout(&self) -> Result<bool, TrellisAuthError> {
691 Ok(self
692 .call::<_, LogoutResponse>("rpc.v1.Auth.Logout", &Empty {})
693 .await?
694 .success)
695 }
696
697 pub async fn renew_binding_token(
699 &self,
700 state: &mut AdminSessionState,
701 ) -> Result<(), TrellisAuthError> {
702 persist_renewed_admin_session(
703 state,
704 self.call("rpc.v1.Auth.RenewBindingToken", &Empty {})
705 .await?,
706 )
707 }
708}
709
710#[derive(Debug, Serialize)]
711struct Empty {}
712
713#[cfg(test)]
714mod tests {
715 use serde_json::{json, Value};
716
717 use super::{
718 CreatePortalRequest, GetPortalDefaultResponse, LoginPortalSelectionRecord, PortalDefaultRecord,
719 PortalRecord, SetLoginPortalSelectionRequest, SetWorkloadPortalSelectionRequest,
720 SetWorkloadPortalSelectionResponse, WorkloadPortalSelectionRecord,
721 };
722
723 #[test]
724 fn portal_create_requests_serialize_with_camel_case_fields() {
725 let value = serde_json::to_value(CreatePortalRequest {
726 portal_id: "main".to_string(),
727 app_contract_id: Some("trellis.portal@v1".to_string()),
728 entry_url: "https://portal.example.com/auth".to_string(),
729 })
730 .expect("serialize portal create request");
731
732 assert_eq!(
733 value,
734 json!({
735 "portalId": "main",
736 "appContractId": "trellis.portal@v1",
737 "entryUrl": "https://portal.example.com/auth"
738 })
739 );
740 }
741
742 #[test]
743 fn portal_records_and_defaults_deserialize_from_camel_case_fields() {
744 let portal: PortalRecord = serde_json::from_value(json!({
745 "portalId": "main",
746 "appContractId": "trellis.portal@v1",
747 "entryUrl": "https://portal.example.com/auth",
748 "disabled": false
749 }))
750 .expect("deserialize portal record");
751 assert_eq!(portal.portal_id, "main");
752 assert_eq!(portal.app_contract_id.as_deref(), Some("trellis.portal@v1"));
753 assert_eq!(portal.entry_url, "https://portal.example.com/auth");
754
755 let response: GetPortalDefaultResponse = serde_json::from_value(json!({
756 "defaultPortal": {
757 "portalId": Value::Null
758 }
759 }))
760 .expect("deserialize portal default response");
761 assert_eq!(response.default_portal.portal_id, None);
762
763 let default_value = serde_json::to_value(PortalDefaultRecord {
764 portal_id: Some("main".to_string()),
765 })
766 .expect("serialize portal default record");
767 assert_eq!(default_value, json!({ "portalId": "main" }));
768 }
769
770 #[test]
771 fn portal_selection_types_use_camel_case_json() {
772 let login_request = serde_json::to_value(SetLoginPortalSelectionRequest {
773 contract_id: "trellis.console@v1".to_string(),
774 portal_id: Some("main".to_string()),
775 })
776 .expect("serialize login portal selection request");
777 assert_eq!(
778 login_request,
779 json!({
780 "contractId": "trellis.console@v1",
781 "portalId": "main"
782 })
783 );
784
785 let login_record: LoginPortalSelectionRecord = serde_json::from_value(json!({
786 "contractId": "trellis.console@v1",
787 "portalId": "main"
788 }))
789 .expect("deserialize login portal selection record");
790 assert_eq!(login_record.contract_id, "trellis.console@v1");
791 assert_eq!(login_record.portal_id.as_deref(), Some("main"));
792
793 let workload_request = serde_json::to_value(SetWorkloadPortalSelectionRequest {
794 profile_id: "reader.default".to_string(),
795 portal_id: None,
796 })
797 .expect("serialize workload portal selection request");
798 assert_eq!(
799 workload_request,
800 json!({
801 "profileId": "reader.default",
802 "portalId": Value::Null
803 })
804 );
805
806 let workload_response: SetWorkloadPortalSelectionResponse = serde_json::from_value(json!({
807 "selection": {
808 "profileId": "reader.default",
809 "portalId": "main"
810 }
811 }))
812 .expect("deserialize workload portal selection response");
813 assert_eq!(workload_response.selection.profile_id, "reader.default");
814 assert_eq!(workload_response.selection.portal_id.as_deref(), Some("main"));
815
816 let workload_record_value = serde_json::to_value(WorkloadPortalSelectionRecord {
817 profile_id: "reader.default".to_string(),
818 portal_id: Some("main".to_string()),
819 })
820 .expect("serialize workload portal selection record");
821 assert_eq!(
822 workload_record_value,
823 json!({
824 "profileId": "reader.default",
825 "portalId": "main"
826 })
827 );
828 }
829}