1use bitwarden_core::{
2 ApiError, MissingFieldError,
3 key_management::{KeySlotIds, SymmetricKeySlotId},
4};
5use bitwarden_crypto::{
6 CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes,
7 PrimitiveEncryptable,
8};
9use bitwarden_encoding::B64Url;
10use bitwarden_error::bitwarden_error;
11use bitwarden_state::repository::{Repository, RepositoryError};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15use uuid::Uuid;
16#[cfg(feature = "wasm")]
17use wasm_bindgen::prelude::*;
18
19use crate::{
20 EmptyEmailListError, Send, SendAuthType, SendId, SendView, SendViewType,
21 error::{ItemNotFoundError, SendParseError},
22 send_client::SendClient,
23};
24
25#[allow(missing_docs)]
26#[bitwarden_error(flat)]
27#[derive(Debug, Error)]
28pub enum EditSendError {
29 #[error(transparent)]
30 ItemNotFound(#[from] ItemNotFoundError),
31 #[error(transparent)]
32 Crypto(#[from] CryptoError),
33 #[error(transparent)]
34 Api(#[from] ApiError),
35 #[error(transparent)]
36 EmptyEmailList(#[from] EmptyEmailListError),
37 #[error(transparent)]
38 MissingField(#[from] MissingFieldError),
39 #[error(transparent)]
40 Repository(#[from] RepositoryError),
41 #[error(transparent)]
42 Uuid(#[from] uuid::Error),
43 #[error(transparent)]
44 SendParse(#[from] SendParseError),
45 #[error("Server returned Send with ID {returned:?} but expected {expected}")]
46 IdMismatch {
47 expected: Uuid,
48 returned: Option<Uuid>,
49 },
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(tag = "type", rename_all = "camelCase")]
55#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
56#[bitwarden_ffi::wasm_record]
57pub enum AuthEdit {
58 Preserve,
60 Set {
62 auth: SendAuthType,
64 },
65}
66
67#[derive(Serialize, Deserialize, Debug)]
69#[serde(rename_all = "camelCase")]
70#[bitwarden_ffi::wasm_record]
71pub struct SendEditRequest {
72 pub name: String,
74 pub notes: Option<String>,
76
77 pub view_type: SendViewType,
79
80 pub max_access_count: Option<u32>,
82 pub disabled: bool,
84 pub hide_email: bool,
86
87 pub deletion_date: DateTime<Utc>,
89 pub expiration_date: Option<DateTime<Utc>>,
91
92 pub auth: AuthEdit,
95}
96
97#[derive(Debug)]
99struct SendEditRequestWithKey {
100 request: SendEditRequest,
101 send_key: String,
102 resolved_auth: ResolvedAuth,
103}
104
105#[derive(Debug)]
107enum ResolvedAuth {
108 Overwrite(SendAuthType),
110 Preserve(crate::AuthType),
113}
114
115impl
116 CompositeEncryptable<
117 KeySlotIds,
118 SymmetricKeySlotId,
119 bitwarden_api_api::models::SendRequestModel,
120 > for SendEditRequestWithKey
121{
122 fn encrypt_composite(
123 &self,
124 ctx: &mut KeyStoreContext<KeySlotIds>,
125 key: SymmetricKeySlotId,
126 ) -> Result<bitwarden_api_api::models::SendRequestModel, CryptoError> {
127 let k = B64Url::try_from(self.send_key.as_str())
129 .map_err(|_| CryptoError::InvalidKey)?
130 .as_bytes()
131 .to_vec();
132
133 let send_key = Send::derive_shareable_key(ctx, &k)?;
134
135 let (send_type, file, text, data) = self
136 .request
137 .view_type
138 .clone()
139 .encrypt_composite(ctx, send_key)?;
140
141 let (auth_type, password, emails) = match &self.resolved_auth {
142 ResolvedAuth::Overwrite(auth) => {
143 let (password, emails) = auth.auth_data(&k);
144 (auth.auth_type(), password, emails)
145 }
146 ResolvedAuth::Preserve(auth_type) => (*auth_type, None, None),
147 };
148
149 Ok(bitwarden_api_api::models::SendRequestModel {
150 r#type: Some(send_type),
151 auth_type: Some(auth_type.into()),
152 file_length: None,
153 name: Some(self.request.name.encrypt(ctx, send_key)?.to_string()),
154 notes: self
155 .request
156 .notes
157 .as_ref()
158 .map(|n| n.encrypt(ctx, send_key))
159 .transpose()?
160 .map(|e| e.to_string()),
161 key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(),
163 max_access_count: self.request.max_access_count.map(|c| c as i32),
164 expiration_date: self.request.expiration_date.map(|d| d.to_rfc3339()),
165 deletion_date: self.request.deletion_date.to_rfc3339(),
166 file,
167 text,
168 data,
169 password,
170 emails,
171 disabled: self.request.disabled,
172 hide_email: Some(self.request.hide_email),
173 })
174 }
175}
176
177impl IdentifyKey<SymmetricKeySlotId> for SendEditRequestWithKey {
178 fn key_identifier(&self) -> SymmetricKeySlotId {
179 SymmetricKeySlotId::User
180 }
181}
182
183async fn edit_send<R: Repository<Send> + ?Sized>(
184 key_store: &KeyStore<KeySlotIds>,
185 api_client: &bitwarden_api_api::apis::ApiClient,
186 repository: &R,
187 send_id: SendId,
188 request: SendEditRequest,
189) -> Result<SendView, EditSendError> {
190 let id = send_id.to_string();
191
192 let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?;
193
194 let resolved_auth = match &request.auth {
195 AuthEdit::Set { auth } => {
196 auth.validate()?;
197 ResolvedAuth::Overwrite(auth.clone())
198 }
199 AuthEdit::Preserve => ResolvedAuth::Preserve(existing_send.auth_type),
200 };
201
202 let existing_send_view: SendView = key_store.decrypt(&existing_send)?;
204 let send_key = existing_send_view.key.ok_or(MissingFieldError("key"))?;
205
206 let request_with_key = SendEditRequestWithKey {
208 request,
209 send_key,
210 resolved_auth,
211 };
212
213 let send_request = key_store.encrypt(request_with_key)?;
214
215 let resp = api_client.sends_api().put(&id, Some(send_request)).await?;
216
217 let send: Send = resp.try_into()?;
218
219 if send.id != Some(send_id) {
221 return Err(EditSendError::IdMismatch {
222 expected: send_id.into(),
223 returned: send.id.map(Into::into),
224 });
225 }
226
227 repository.set(send_id, send.clone()).await?;
228
229 Ok(key_store.decrypt(&send)?)
230}
231
232#[bitwarden_ffi::wasm_export]
233impl SendClient {
234 pub async fn edit(
236 &self,
237 send_id: SendId,
238 request: SendEditRequest,
239 ) -> Result<SendView, EditSendError> {
240 let key_store = self.client.internal.get_key_store();
241 let config = self.client.internal.get_api_configurations();
242 let repository = self.get_repository()?;
243
244 edit_send(
245 key_store,
246 &config.api_client,
247 repository.as_ref(),
248 send_id,
249 request,
250 )
251 .await
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
258 use bitwarden_core::key_management::SymmetricKeySlotId;
259 use bitwarden_crypto::SymmetricKeyAlgorithm;
260 use bitwarden_test::MemoryRepository;
261 use chrono::{DateTime, Utc};
262 use uuid::uuid;
263
264 use super::*;
265 use crate::{AuthType, SendTextView, SendType, SendViewType};
266
267 #[test]
273 fn auth_edit_round_trips_through_json_without_duplicate_type_keys() {
274 let cases = [
275 (AuthEdit::Preserve, serde_json::json!({"type": "preserve"})),
276 (
277 AuthEdit::Set {
278 auth: SendAuthType::None,
279 },
280 serde_json::json!({"type": "set", "auth": {"type": "none"}}),
281 ),
282 (
283 AuthEdit::Set {
284 auth: SendAuthType::Password {
285 password: "hunter2".to_string(),
286 },
287 },
288 serde_json::json!({
289 "type": "set",
290 "auth": {"type": "password", "password": "hunter2"}
291 }),
292 ),
293 (
294 AuthEdit::Set {
295 auth: SendAuthType::Emails {
296 emails: vec!["[email protected]".to_string(), "[email protected]".to_string()],
297 },
298 },
299 serde_json::json!({
300 "type": "set",
301 "auth": {"type": "emails", "emails": ["[email protected]", "[email protected]"]}
302 }),
303 ),
304 ];
305 for (value, expected_json) in cases {
306 let serialized = serde_json::to_value(&value).expect("serialize");
307 assert_eq!(
308 serialized, expected_json,
309 "wire shape mismatch for {value:?}"
310 );
311 let deserialized: AuthEdit = serde_json::from_value(serialized).expect("round-trip");
312 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
313 }
314 }
315
316 #[tokio::test]
317 async fn test_edit_send() {
318 let store: KeyStore<KeySlotIds> = KeyStore::default();
319 {
320 let mut ctx = store.context_mut();
321 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
322 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
323 .unwrap();
324 }
325
326 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
327
328 let repository = MemoryRepository::<Send>::default();
330 let existing_send_view = SendView {
331 id: None, access_id: None,
333 name: "original".to_string(),
334 notes: Some("original notes".to_string()),
335 key: None, new_password: None,
337 has_password: false,
338 r#type: SendType::Text,
339 file: None,
340 text: Some(SendTextView {
341 text: Some("original text".to_string()),
342 hidden: false,
343 }),
344 data: None,
345 max_access_count: None,
346 access_count: 0,
347 disabled: false,
348 hide_email: false,
349 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
350 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
351 expiration_date: None,
352 emails: Vec::new(),
353 auth_type: AuthType::None,
354 };
355 let mut existing_send = store.encrypt(existing_send_view).unwrap();
356 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
358 .set(SendId::new(send_id), existing_send)
359 .await
360 .unwrap();
361
362 let api_client = ApiClient::new_mocked(move |mock| {
363 mock.sends_api
364 .expect_put()
365 .returning(move |_id, model| {
366 let model = model.unwrap();
367 Ok(SendResponseModel {
368 id: Some(send_id),
369 name: model.name,
370 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
371 object: Some("send".to_string()),
372 access_id: None,
373 r#type: model.r#type,
374 auth_type: model.auth_type,
375 notes: model.notes,
376 file: model.file,
377 text: model.text,
378 data: model.data,
379 key: Some(model.key),
380 max_access_count: model.max_access_count,
381 access_count: Some(0),
382 password: model.password,
383 emails: model.emails,
384 disabled: Some(model.disabled),
385 expiration_date: model.expiration_date,
386 deletion_date: Some(model.deletion_date),
387 hide_email: model.hide_email,
388 })
389 })
390 .once();
391 });
392
393 let result = edit_send(
394 &store,
395 &api_client,
396 &repository,
397 SendId::new(send_id),
398 SendEditRequest {
399 name: "updated".to_string(),
400 notes: Some("updated notes".to_string()),
401 view_type: SendViewType::Text(SendTextView {
402 text: Some("updated text".to_string()),
403 hidden: false,
404 }),
405 max_access_count: None,
406 disabled: false,
407 hide_email: false,
408 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
409 expiration_date: None,
410 auth: AuthEdit::Set {
411 auth: SendAuthType::None,
412 },
413 },
414 )
415 .await
416 .unwrap();
417
418 assert_eq!(result.id, Some(crate::send::SendId::new(send_id)));
420 assert_eq!(result.name, "updated");
421 assert_eq!(result.notes, Some("updated notes".to_string()));
422 assert!(result.key.is_some(), "Expected a key");
423 assert_eq!(
424 result.revision_date,
425 "2025-01-02T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
426 );
427
428 let stored = repository.get(SendId::new(send_id)).await.unwrap().unwrap();
430 assert_eq!(
431 store
432 .decrypt::<SymmetricKeySlotId, Send, SendView>(&stored)
433 .unwrap()
434 .name,
435 "updated"
436 );
437 }
438
439 #[tokio::test]
440 async fn test_edit_send_not_found() {
441 let store: KeyStore<KeySlotIds> = KeyStore::default();
442 {
443 let mut ctx = store.context_mut();
444 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
445 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
446 .unwrap();
447 }
448
449 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
450 let repository = MemoryRepository::<Send>::default();
451 let api_client = ApiClient::new_mocked(move |_mock| {});
452
453 let result = edit_send(
454 &store,
455 &api_client,
456 &repository,
457 SendId::new(send_id),
458 SendEditRequest {
459 name: "test".to_string(),
460 notes: None,
461 view_type: SendViewType::Text(SendTextView {
462 text: Some("test".to_string()),
463 hidden: false,
464 }),
465 max_access_count: None,
466 disabled: false,
467 hide_email: false,
468 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
469 expiration_date: None,
470 auth: AuthEdit::Set {
471 auth: SendAuthType::None,
472 },
473 },
474 )
475 .await;
476
477 assert!(result.is_err());
478 assert!(matches!(
479 result.unwrap_err(),
480 EditSendError::ItemNotFound(_)
481 ));
482 }
483
484 #[tokio::test]
485 async fn test_edit_send_http_error() {
486 let store: KeyStore<KeySlotIds> = KeyStore::default();
487 {
488 let mut ctx = store.context_mut();
489 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
490 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
491 .unwrap();
492 }
493
494 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
495
496 let repository = MemoryRepository::<Send>::default();
498 let existing_send_view = SendView {
499 id: None, access_id: None,
501 name: "original".to_string(),
502 notes: Some("original notes".to_string()),
503 key: None, new_password: None,
505 has_password: false,
506 r#type: SendType::Text,
507 file: None,
508 text: Some(SendTextView {
509 text: Some("original text".to_string()),
510 hidden: false,
511 }),
512 data: None,
513 max_access_count: None,
514 access_count: 0,
515 disabled: false,
516 hide_email: false,
517 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
518 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
519 expiration_date: None,
520 emails: Vec::new(),
521 auth_type: AuthType::None,
522 };
523 let mut existing_send = store.encrypt(existing_send_view).unwrap();
524 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
526 .set(SendId::new(send_id), existing_send)
527 .await
528 .unwrap();
529
530 let api_client = ApiClient::new_mocked(move |mock| {
531 mock.sends_api
532 .expect_put()
533 .returning(move |_id, _model| Err(std::io::Error::other("Simulated error").into()));
534 });
535
536 let result = edit_send(
537 &store,
538 &api_client,
539 &repository,
540 SendId::new(send_id),
541 SendEditRequest {
542 name: "test".to_string(),
543 notes: None,
544 view_type: SendViewType::Text(SendTextView {
545 text: Some("test".to_string()),
546 hidden: false,
547 }),
548 max_access_count: None,
549 disabled: false,
550 hide_email: false,
551 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
552 expiration_date: None,
553 auth: AuthEdit::Set {
554 auth: SendAuthType::None,
555 },
556 },
557 )
558 .await;
559
560 assert!(result.is_err());
561 assert!(matches!(result.unwrap_err(), EditSendError::Api(_)));
562 }
563
564 async fn make_fixture_with_existing_auth(
569 send_id: uuid::Uuid,
570 password_hash: Option<String>,
571 emails: Option<String>,
572 auth_type: AuthType,
573 ) -> (KeyStore<KeySlotIds>, MemoryRepository<Send>) {
574 let store: KeyStore<KeySlotIds> = KeyStore::default();
575 {
576 let mut ctx = store.context_mut();
577 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
578 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
579 .unwrap();
580 }
581
582 let baseline = SendView {
585 id: None,
586 access_id: None,
587 name: "original".to_string(),
588 notes: None,
589 key: None,
590 new_password: None,
591 has_password: false,
592 r#type: SendType::Text,
593 file: None,
594 text: Some(SendTextView {
595 text: Some("secret".to_string()),
596 hidden: false,
597 }),
598 data: None,
599 max_access_count: None,
600 access_count: 0,
601 disabled: false,
602 hide_email: false,
603 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
604 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
605 expiration_date: None,
606 emails: Vec::new(),
607 auth_type: AuthType::None,
608 };
609 let mut existing_send = store.encrypt(baseline).unwrap();
610 existing_send.id = Some(crate::send::SendId::new(send_id));
611 existing_send.password = password_hash;
612 existing_send.emails = emails;
613 existing_send.auth_type = auth_type;
614
615 let repository = MemoryRepository::<Send>::default();
616 repository
617 .set(SendId::new(send_id), existing_send)
618 .await
619 .unwrap();
620
621 (store, repository)
622 }
623
624 async fn capture_edit_put_model(
626 store: &KeyStore<KeySlotIds>,
627 repository: &MemoryRepository<Send>,
628 send_id: uuid::Uuid,
629 request: SendEditRequest,
630 ) -> bitwarden_api_api::models::SendRequestModel {
631 let captured: std::sync::Arc<
632 std::sync::Mutex<Option<bitwarden_api_api::models::SendRequestModel>>,
633 > = std::sync::Arc::new(std::sync::Mutex::new(None));
634 let sink = captured.clone();
635
636 let api_client = ApiClient::new_mocked(move |mock| {
637 let sink = sink.clone();
638 mock.sends_api
639 .expect_put()
640 .returning(move |_id, model| {
641 let model = model.unwrap();
642 *sink.lock().unwrap() = Some(model.clone());
643 Ok(SendResponseModel {
644 id: Some(send_id),
645 name: model.name.clone(),
646 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
647 object: Some("send".to_string()),
648 access_id: None,
649 r#type: model.r#type,
650 auth_type: model.auth_type,
651 notes: model.notes.clone(),
652 file: model.file.clone(),
653 text: model.text.clone(),
654 data: model.data.clone(),
655 key: Some(model.key.clone()),
656 max_access_count: model.max_access_count,
657 access_count: Some(0),
658 password: model.password.clone(),
659 emails: model.emails.clone(),
660 disabled: Some(model.disabled),
661 expiration_date: model.expiration_date.clone(),
662 deletion_date: Some(model.deletion_date.clone()),
663 hide_email: model.hide_email,
664 })
665 })
666 .once();
667 });
668
669 edit_send(
670 store,
671 &api_client,
672 repository,
673 SendId::new(send_id),
674 request,
675 )
676 .await
677 .unwrap();
678
679 captured.lock().unwrap().take().expect("PUT was not called")
680 }
681
682 #[tokio::test]
685 async fn test_edit_preserves_existing_password_when_auth_is_none() {
686 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
687 let existing_hash = "abc123hashstub==".to_string();
688 let (store, repository) =
689 make_fixture_with_existing_auth(send_id, Some(existing_hash), None, AuthType::Password)
690 .await;
691
692 let model = capture_edit_put_model(
693 &store,
694 &repository,
695 send_id,
696 SendEditRequest {
697 name: "updated".to_string(),
698 notes: None,
699 view_type: SendViewType::Text(SendTextView {
700 text: Some("secret".to_string()),
701 hidden: false,
702 }),
703 max_access_count: None,
704 disabled: false,
705 hide_email: false,
706 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
707 expiration_date: None,
708 auth: AuthEdit::Preserve,
709 },
710 )
711 .await;
712
713 assert_eq!(
714 model.password, None,
715 "preserve mode must omit the password hash so the server retains the stored value",
716 );
717 assert_eq!(model.emails, None);
718 assert_eq!(
719 model.auth_type,
720 Some(bitwarden_api_api::models::AuthType::Password),
721 "preserve mode must forward the existing authType so the server doesn't clear auth",
722 );
723 }
724
725 #[tokio::test]
726 async fn test_edit_preserves_existing_emails_when_auth_is_none() {
727 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
728 let existing_emails = "[email protected],[email protected]".to_string();
729 let (store, repository) =
730 make_fixture_with_existing_auth(send_id, None, Some(existing_emails), AuthType::Email)
731 .await;
732
733 let model = capture_edit_put_model(
734 &store,
735 &repository,
736 send_id,
737 SendEditRequest {
738 name: "updated".to_string(),
739 notes: None,
740 view_type: SendViewType::Text(SendTextView {
741 text: Some("secret".to_string()),
742 hidden: false,
743 }),
744 max_access_count: None,
745 disabled: false,
746 hide_email: false,
747 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
748 expiration_date: None,
749 auth: AuthEdit::Preserve,
750 },
751 )
752 .await;
753
754 assert_eq!(model.password, None);
755 assert_eq!(
756 model.emails, None,
757 "preserve mode must omit the email list so the server retains the stored value",
758 );
759 assert_eq!(
760 model.auth_type,
761 Some(bitwarden_api_api::models::AuthType::Email),
762 "preserve mode must forward the existing authType so the server doesn't clear auth",
763 );
764 }
765
766 #[tokio::test]
767 async fn test_edit_preserves_no_auth_when_auth_is_none() {
768 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
769 let (store, repository) =
770 make_fixture_with_existing_auth(send_id, None, None, AuthType::None).await;
771
772 let model = capture_edit_put_model(
773 &store,
774 &repository,
775 send_id,
776 SendEditRequest {
777 name: "updated".to_string(),
778 notes: None,
779 view_type: SendViewType::Text(SendTextView {
780 text: Some("secret".to_string()),
781 hidden: false,
782 }),
783 max_access_count: None,
784 disabled: false,
785 hide_email: false,
786 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
787 expiration_date: None,
788 auth: AuthEdit::Preserve,
789 },
790 )
791 .await;
792
793 assert_eq!(model.password, None);
794 assert_eq!(model.emails, None);
795 assert_eq!(
796 model.auth_type,
797 Some(bitwarden_api_api::models::AuthType::None),
798 );
799 }
800
801 #[tokio::test]
804 async fn test_edit_explicit_auth_none_overrides_existing_password() {
805 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
806 let (store, repository) = make_fixture_with_existing_auth(
807 send_id,
808 Some("existing-hash".to_string()),
809 None,
810 AuthType::Password,
811 )
812 .await;
813
814 let model = capture_edit_put_model(
815 &store,
816 &repository,
817 send_id,
818 SendEditRequest {
819 name: "updated".to_string(),
820 notes: None,
821 view_type: SendViewType::Text(SendTextView {
822 text: Some("secret".to_string()),
823 hidden: false,
824 }),
825 max_access_count: None,
826 disabled: false,
827 hide_email: false,
828 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
829 expiration_date: None,
830 auth: AuthEdit::Set {
831 auth: SendAuthType::None,
832 },
833 },
834 )
835 .await;
836
837 assert_eq!(model.password, None);
838 assert_eq!(model.emails, None);
839 assert_eq!(
840 model.auth_type,
841 Some(bitwarden_api_api::models::AuthType::None),
842 );
843 }
844}