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;
15#[cfg(feature = "wasm")]
16use tsify::Tsify;
17use uuid::Uuid;
18#[cfg(feature = "wasm")]
19use wasm_bindgen::prelude::*;
20
21use crate::{
22 EmptyEmailListError, Send, SendAuthType, SendId, SendView, SendViewType,
23 error::{ItemNotFoundError, SendParseError},
24 send_client::SendClient,
25};
26
27#[allow(missing_docs)]
28#[bitwarden_error(flat)]
29#[derive(Debug, Error)]
30pub enum EditSendError {
31 #[error(transparent)]
32 ItemNotFound(#[from] ItemNotFoundError),
33 #[error(transparent)]
34 Crypto(#[from] CryptoError),
35 #[error(transparent)]
36 Api(#[from] ApiError),
37 #[error(transparent)]
38 EmptyEmailList(#[from] EmptyEmailListError),
39 #[error(transparent)]
40 MissingField(#[from] MissingFieldError),
41 #[error(transparent)]
42 Repository(#[from] RepositoryError),
43 #[error(transparent)]
44 Uuid(#[from] uuid::Error),
45 #[error(transparent)]
46 SendParse(#[from] SendParseError),
47 #[error("Server returned Send with ID {returned:?} but expected {expected}")]
48 IdMismatch {
49 expected: Uuid,
50 returned: Option<Uuid>,
51 },
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "camelCase")]
57#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
58#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
59pub enum AuthEdit {
60 Preserve,
62 Set {
64 auth: SendAuthType,
66 },
67}
68
69#[derive(Serialize, Deserialize, Debug)]
71#[serde(rename_all = "camelCase")]
72#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
73pub struct SendEditRequest {
74 pub name: String,
76 pub notes: Option<String>,
78
79 pub view_type: SendViewType,
81
82 pub max_access_count: Option<u32>,
84 pub disabled: bool,
86 pub hide_email: bool,
88
89 pub deletion_date: DateTime<Utc>,
91 pub expiration_date: Option<DateTime<Utc>>,
93
94 pub auth: AuthEdit,
97}
98
99#[derive(Debug)]
101struct SendEditRequestWithKey {
102 request: SendEditRequest,
103 send_key: String,
104 resolved_auth: ResolvedAuth,
105}
106
107#[derive(Debug)]
109enum ResolvedAuth {
110 Overwrite(SendAuthType),
112 Preserve(crate::AuthType),
115}
116
117impl
118 CompositeEncryptable<
119 KeySlotIds,
120 SymmetricKeySlotId,
121 bitwarden_api_api::models::SendRequestModel,
122 > for SendEditRequestWithKey
123{
124 fn encrypt_composite(
125 &self,
126 ctx: &mut KeyStoreContext<KeySlotIds>,
127 key: SymmetricKeySlotId,
128 ) -> Result<bitwarden_api_api::models::SendRequestModel, CryptoError> {
129 let k = B64Url::try_from(self.send_key.as_str())
131 .map_err(|_| CryptoError::InvalidKey)?
132 .as_bytes()
133 .to_vec();
134
135 let send_key = Send::derive_shareable_key(ctx, &k)?;
136
137 let (send_type, file, text) = self
138 .request
139 .view_type
140 .clone()
141 .encrypt_composite(ctx, send_key)?;
142
143 let (auth_type, password, emails) = match &self.resolved_auth {
144 ResolvedAuth::Overwrite(auth) => {
145 let (password, emails) = auth.auth_data(&k);
146 (auth.auth_type(), password, emails)
147 }
148 ResolvedAuth::Preserve(auth_type) => (*auth_type, None, None),
149 };
150
151 Ok(bitwarden_api_api::models::SendRequestModel {
152 r#type: Some(send_type),
153 auth_type: Some(auth_type.into()),
154 file_length: None,
155 name: Some(self.request.name.encrypt(ctx, send_key)?.to_string()),
156 notes: self
157 .request
158 .notes
159 .as_ref()
160 .map(|n| n.encrypt(ctx, send_key))
161 .transpose()?
162 .map(|e| e.to_string()),
163 key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(),
165 max_access_count: self.request.max_access_count.map(|c| c as i32),
166 expiration_date: self.request.expiration_date.map(|d| d.to_rfc3339()),
167 deletion_date: self.request.deletion_date.to_rfc3339(),
168 file,
169 text,
170 data: None,
172 password,
173 emails,
174 disabled: self.request.disabled,
175 hide_email: Some(self.request.hide_email),
176 })
177 }
178}
179
180impl IdentifyKey<SymmetricKeySlotId> for SendEditRequestWithKey {
181 fn key_identifier(&self) -> SymmetricKeySlotId {
182 SymmetricKeySlotId::User
183 }
184}
185
186async fn edit_send<R: Repository<Send> + ?Sized>(
187 key_store: &KeyStore<KeySlotIds>,
188 api_client: &bitwarden_api_api::apis::ApiClient,
189 repository: &R,
190 send_id: SendId,
191 request: SendEditRequest,
192) -> Result<SendView, EditSendError> {
193 let id = send_id.to_string();
194
195 let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?;
196
197 let resolved_auth = match &request.auth {
198 AuthEdit::Set { auth } => {
199 auth.validate()?;
200 ResolvedAuth::Overwrite(auth.clone())
201 }
202 AuthEdit::Preserve => ResolvedAuth::Preserve(existing_send.auth_type),
203 };
204
205 let existing_send_view: SendView = key_store.decrypt(&existing_send)?;
207 let send_key = existing_send_view.key.ok_or(MissingFieldError("key"))?;
208
209 let request_with_key = SendEditRequestWithKey {
211 request,
212 send_key,
213 resolved_auth,
214 };
215
216 let send_request = key_store.encrypt(request_with_key)?;
217
218 let resp = api_client.sends_api().put(&id, Some(send_request)).await?;
219
220 let send: Send = resp.try_into()?;
221
222 if send.id != Some(send_id) {
224 return Err(EditSendError::IdMismatch {
225 expected: send_id.into(),
226 returned: send.id.map(Into::into),
227 });
228 }
229
230 repository.set(send_id, send.clone()).await?;
231
232 Ok(key_store.decrypt(&send)?)
233}
234
235#[cfg_attr(feature = "wasm", wasm_bindgen)]
236impl SendClient {
237 pub async fn edit(
239 &self,
240 send_id: SendId,
241 request: SendEditRequest,
242 ) -> Result<SendView, EditSendError> {
243 let key_store = self.client.internal.get_key_store();
244 let config = self.client.internal.get_api_configurations();
245 let repository = self.get_repository()?;
246
247 edit_send(
248 key_store,
249 &config.api_client,
250 repository.as_ref(),
251 send_id,
252 request,
253 )
254 .await
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
261 use bitwarden_core::key_management::SymmetricKeySlotId;
262 use bitwarden_crypto::SymmetricKeyAlgorithm;
263 use bitwarden_test::MemoryRepository;
264 use chrono::{DateTime, Utc};
265 use uuid::uuid;
266
267 use super::*;
268 use crate::{AuthType, SendTextView, SendType, SendViewType};
269
270 #[test]
276 fn auth_edit_round_trips_through_json_without_duplicate_type_keys() {
277 let cases = [
278 (AuthEdit::Preserve, serde_json::json!({"type": "preserve"})),
279 (
280 AuthEdit::Set {
281 auth: SendAuthType::None,
282 },
283 serde_json::json!({"type": "set", "auth": {"type": "none"}}),
284 ),
285 (
286 AuthEdit::Set {
287 auth: SendAuthType::Password {
288 password: "hunter2".to_string(),
289 },
290 },
291 serde_json::json!({
292 "type": "set",
293 "auth": {"type": "password", "password": "hunter2"}
294 }),
295 ),
296 (
297 AuthEdit::Set {
298 auth: SendAuthType::Emails {
299 emails: vec!["[email protected]".to_string(), "[email protected]".to_string()],
300 },
301 },
302 serde_json::json!({
303 "type": "set",
304 "auth": {"type": "emails", "emails": ["[email protected]", "[email protected]"]}
305 }),
306 ),
307 ];
308 for (value, expected_json) in cases {
309 let serialized = serde_json::to_value(&value).expect("serialize");
310 assert_eq!(
311 serialized, expected_json,
312 "wire shape mismatch for {value:?}"
313 );
314 let deserialized: AuthEdit = serde_json::from_value(serialized).expect("round-trip");
315 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
316 }
317 }
318
319 #[tokio::test]
320 async fn test_edit_send() {
321 let store: KeyStore<KeySlotIds> = KeyStore::default();
322 {
323 let mut ctx = store.context_mut();
324 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
325 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
326 .unwrap();
327 }
328
329 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
330
331 let repository = MemoryRepository::<Send>::default();
333 let existing_send_view = SendView {
334 id: None, access_id: None,
336 name: "original".to_string(),
337 notes: Some("original notes".to_string()),
338 key: None, new_password: None,
340 has_password: false,
341 r#type: SendType::Text,
342 file: None,
343 text: Some(SendTextView {
344 text: Some("original text".to_string()),
345 hidden: false,
346 }),
347 max_access_count: None,
348 access_count: 0,
349 disabled: false,
350 hide_email: false,
351 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
352 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
353 expiration_date: None,
354 emails: Vec::new(),
355 auth_type: AuthType::None,
356 };
357 let mut existing_send = store.encrypt(existing_send_view).unwrap();
358 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
360 .set(SendId::new(send_id), existing_send)
361 .await
362 .unwrap();
363
364 let api_client = ApiClient::new_mocked(move |mock| {
365 mock.sends_api
366 .expect_put()
367 .returning(move |_id, model| {
368 let model = model.unwrap();
369 Ok(SendResponseModel {
370 id: Some(send_id),
371 name: model.name,
372 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
373 object: Some("send".to_string()),
374 access_id: None,
375 r#type: model.r#type,
376 auth_type: model.auth_type,
377 notes: model.notes,
378 file: model.file,
379 text: model.text,
380 data: model.data,
381 key: Some(model.key),
382 max_access_count: model.max_access_count,
383 access_count: Some(0),
384 password: model.password,
385 emails: model.emails,
386 disabled: Some(model.disabled),
387 expiration_date: model.expiration_date,
388 deletion_date: Some(model.deletion_date),
389 hide_email: model.hide_email,
390 })
391 })
392 .once();
393 });
394
395 let result = edit_send(
396 &store,
397 &api_client,
398 &repository,
399 SendId::new(send_id),
400 SendEditRequest {
401 name: "updated".to_string(),
402 notes: Some("updated notes".to_string()),
403 view_type: SendViewType::Text(SendTextView {
404 text: Some("updated text".to_string()),
405 hidden: false,
406 }),
407 max_access_count: None,
408 disabled: false,
409 hide_email: false,
410 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
411 expiration_date: None,
412 auth: AuthEdit::Set {
413 auth: SendAuthType::None,
414 },
415 },
416 )
417 .await
418 .unwrap();
419
420 assert_eq!(result.id, Some(crate::send::SendId::new(send_id)));
422 assert_eq!(result.name, "updated");
423 assert_eq!(result.notes, Some("updated notes".to_string()));
424 assert!(result.key.is_some(), "Expected a key");
425 assert_eq!(
426 result.revision_date,
427 "2025-01-02T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
428 );
429
430 let stored = repository.get(SendId::new(send_id)).await.unwrap().unwrap();
432 assert_eq!(
433 store
434 .decrypt::<SymmetricKeySlotId, Send, SendView>(&stored)
435 .unwrap()
436 .name,
437 "updated"
438 );
439 }
440
441 #[tokio::test]
442 async fn test_edit_send_not_found() {
443 let store: KeyStore<KeySlotIds> = KeyStore::default();
444 {
445 let mut ctx = store.context_mut();
446 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
447 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
448 .unwrap();
449 }
450
451 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
452 let repository = MemoryRepository::<Send>::default();
453 let api_client = ApiClient::new_mocked(move |_mock| {});
454
455 let result = edit_send(
456 &store,
457 &api_client,
458 &repository,
459 SendId::new(send_id),
460 SendEditRequest {
461 name: "test".to_string(),
462 notes: None,
463 view_type: SendViewType::Text(SendTextView {
464 text: Some("test".to_string()),
465 hidden: false,
466 }),
467 max_access_count: None,
468 disabled: false,
469 hide_email: false,
470 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
471 expiration_date: None,
472 auth: AuthEdit::Set {
473 auth: SendAuthType::None,
474 },
475 },
476 )
477 .await;
478
479 assert!(result.is_err());
480 assert!(matches!(
481 result.unwrap_err(),
482 EditSendError::ItemNotFound(_)
483 ));
484 }
485
486 #[tokio::test]
487 async fn test_edit_send_http_error() {
488 let store: KeyStore<KeySlotIds> = KeyStore::default();
489 {
490 let mut ctx = store.context_mut();
491 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
492 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
493 .unwrap();
494 }
495
496 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
497
498 let repository = MemoryRepository::<Send>::default();
500 let existing_send_view = SendView {
501 id: None, access_id: None,
503 name: "original".to_string(),
504 notes: Some("original notes".to_string()),
505 key: None, new_password: None,
507 has_password: false,
508 r#type: SendType::Text,
509 file: None,
510 text: Some(SendTextView {
511 text: Some("original text".to_string()),
512 hidden: false,
513 }),
514 max_access_count: None,
515 access_count: 0,
516 disabled: false,
517 hide_email: false,
518 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
519 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
520 expiration_date: None,
521 emails: Vec::new(),
522 auth_type: AuthType::None,
523 };
524 let mut existing_send = store.encrypt(existing_send_view).unwrap();
525 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
527 .set(SendId::new(send_id), existing_send)
528 .await
529 .unwrap();
530
531 let api_client = ApiClient::new_mocked(move |mock| {
532 mock.sends_api
533 .expect_put()
534 .returning(move |_id, _model| Err(std::io::Error::other("Simulated error").into()));
535 });
536
537 let result = edit_send(
538 &store,
539 &api_client,
540 &repository,
541 SendId::new(send_id),
542 SendEditRequest {
543 name: "test".to_string(),
544 notes: None,
545 view_type: SendViewType::Text(SendTextView {
546 text: Some("test".to_string()),
547 hidden: false,
548 }),
549 max_access_count: None,
550 disabled: false,
551 hide_email: false,
552 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
553 expiration_date: None,
554 auth: AuthEdit::Set {
555 auth: SendAuthType::None,
556 },
557 },
558 )
559 .await;
560
561 assert!(result.is_err());
562 assert!(matches!(result.unwrap_err(), EditSendError::Api(_)));
563 }
564
565 async fn make_fixture_with_existing_auth(
570 send_id: uuid::Uuid,
571 password_hash: Option<String>,
572 emails: Option<String>,
573 auth_type: AuthType,
574 ) -> (KeyStore<KeySlotIds>, MemoryRepository<Send>) {
575 let store: KeyStore<KeySlotIds> = KeyStore::default();
576 {
577 let mut ctx = store.context_mut();
578 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
579 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
580 .unwrap();
581 }
582
583 let baseline = SendView {
586 id: None,
587 access_id: None,
588 name: "original".to_string(),
589 notes: None,
590 key: None,
591 new_password: None,
592 has_password: false,
593 r#type: SendType::Text,
594 file: None,
595 text: Some(SendTextView {
596 text: Some("secret".to_string()),
597 hidden: false,
598 }),
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}