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, data) = 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,
171 password,
172 emails,
173 disabled: self.request.disabled,
174 hide_email: Some(self.request.hide_email),
175 })
176 }
177}
178
179impl IdentifyKey<SymmetricKeySlotId> for SendEditRequestWithKey {
180 fn key_identifier(&self) -> SymmetricKeySlotId {
181 SymmetricKeySlotId::User
182 }
183}
184
185async fn edit_send<R: Repository<Send> + ?Sized>(
186 key_store: &KeyStore<KeySlotIds>,
187 api_client: &bitwarden_api_api::apis::ApiClient,
188 repository: &R,
189 send_id: SendId,
190 request: SendEditRequest,
191) -> Result<SendView, EditSendError> {
192 let id = send_id.to_string();
193
194 let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?;
195
196 let resolved_auth = match &request.auth {
197 AuthEdit::Set { auth } => {
198 auth.validate()?;
199 ResolvedAuth::Overwrite(auth.clone())
200 }
201 AuthEdit::Preserve => ResolvedAuth::Preserve(existing_send.auth_type),
202 };
203
204 let existing_send_view: SendView = key_store.decrypt(&existing_send)?;
206 let send_key = existing_send_view.key.ok_or(MissingFieldError("key"))?;
207
208 let request_with_key = SendEditRequestWithKey {
210 request,
211 send_key,
212 resolved_auth,
213 };
214
215 let send_request = key_store.encrypt(request_with_key)?;
216
217 let resp = api_client.sends_api().put(&id, Some(send_request)).await?;
218
219 let send: Send = resp.try_into()?;
220
221 if send.id != Some(send_id) {
223 return Err(EditSendError::IdMismatch {
224 expected: send_id.into(),
225 returned: send.id.map(Into::into),
226 });
227 }
228
229 repository.set(send_id, send.clone()).await?;
230
231 Ok(key_store.decrypt(&send)?)
232}
233
234#[cfg_attr(feature = "wasm", wasm_bindgen)]
235impl SendClient {
236 pub async fn edit(
238 &self,
239 send_id: SendId,
240 request: SendEditRequest,
241 ) -> Result<SendView, EditSendError> {
242 let key_store = self.client.internal.get_key_store();
243 let config = self.client.internal.get_api_configurations();
244 let repository = self.get_repository()?;
245
246 edit_send(
247 key_store,
248 &config.api_client,
249 repository.as_ref(),
250 send_id,
251 request,
252 )
253 .await
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
260 use bitwarden_core::key_management::SymmetricKeySlotId;
261 use bitwarden_crypto::SymmetricKeyAlgorithm;
262 use bitwarden_test::MemoryRepository;
263 use chrono::{DateTime, Utc};
264 use uuid::uuid;
265
266 use super::*;
267 use crate::{AuthType, SendTextView, SendType, SendViewType};
268
269 #[test]
275 fn auth_edit_round_trips_through_json_without_duplicate_type_keys() {
276 let cases = [
277 (AuthEdit::Preserve, serde_json::json!({"type": "preserve"})),
278 (
279 AuthEdit::Set {
280 auth: SendAuthType::None,
281 },
282 serde_json::json!({"type": "set", "auth": {"type": "none"}}),
283 ),
284 (
285 AuthEdit::Set {
286 auth: SendAuthType::Password {
287 password: "hunter2".to_string(),
288 },
289 },
290 serde_json::json!({
291 "type": "set",
292 "auth": {"type": "password", "password": "hunter2"}
293 }),
294 ),
295 (
296 AuthEdit::Set {
297 auth: SendAuthType::Emails {
298 emails: vec!["[email protected]".to_string(), "[email protected]".to_string()],
299 },
300 },
301 serde_json::json!({
302 "type": "set",
303 "auth": {"type": "emails", "emails": ["[email protected]", "[email protected]"]}
304 }),
305 ),
306 ];
307 for (value, expected_json) in cases {
308 let serialized = serde_json::to_value(&value).expect("serialize");
309 assert_eq!(
310 serialized, expected_json,
311 "wire shape mismatch for {value:?}"
312 );
313 let deserialized: AuthEdit = serde_json::from_value(serialized).expect("round-trip");
314 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
315 }
316 }
317
318 #[tokio::test]
319 async fn test_edit_send() {
320 let store: KeyStore<KeySlotIds> = KeyStore::default();
321 {
322 let mut ctx = store.context_mut();
323 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
324 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
325 .unwrap();
326 }
327
328 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
329
330 let repository = MemoryRepository::<Send>::default();
332 let existing_send_view = SendView {
333 id: None, access_id: None,
335 name: "original".to_string(),
336 notes: Some("original notes".to_string()),
337 key: None, new_password: None,
339 has_password: false,
340 r#type: SendType::Text,
341 file: None,
342 text: Some(SendTextView {
343 text: Some("original text".to_string()),
344 hidden: false,
345 }),
346 data: None,
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 data: None,
515 max_access_count: None,
516 access_count: 0,
517 disabled: false,
518 hide_email: false,
519 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
520 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
521 expiration_date: None,
522 emails: Vec::new(),
523 auth_type: AuthType::None,
524 };
525 let mut existing_send = store.encrypt(existing_send_view).unwrap();
526 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
528 .set(SendId::new(send_id), existing_send)
529 .await
530 .unwrap();
531
532 let api_client = ApiClient::new_mocked(move |mock| {
533 mock.sends_api
534 .expect_put()
535 .returning(move |_id, _model| Err(std::io::Error::other("Simulated error").into()));
536 });
537
538 let result = edit_send(
539 &store,
540 &api_client,
541 &repository,
542 SendId::new(send_id),
543 SendEditRequest {
544 name: "test".to_string(),
545 notes: None,
546 view_type: SendViewType::Text(SendTextView {
547 text: Some("test".to_string()),
548 hidden: false,
549 }),
550 max_access_count: None,
551 disabled: false,
552 hide_email: false,
553 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
554 expiration_date: None,
555 auth: AuthEdit::Set {
556 auth: SendAuthType::None,
557 },
558 },
559 )
560 .await;
561
562 assert!(result.is_err());
563 assert!(matches!(result.unwrap_err(), EditSendError::Api(_)));
564 }
565
566 async fn make_fixture_with_existing_auth(
571 send_id: uuid::Uuid,
572 password_hash: Option<String>,
573 emails: Option<String>,
574 auth_type: AuthType,
575 ) -> (KeyStore<KeySlotIds>, MemoryRepository<Send>) {
576 let store: KeyStore<KeySlotIds> = KeyStore::default();
577 {
578 let mut ctx = store.context_mut();
579 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
580 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
581 .unwrap();
582 }
583
584 let baseline = SendView {
587 id: None,
588 access_id: None,
589 name: "original".to_string(),
590 notes: None,
591 key: None,
592 new_password: None,
593 has_password: false,
594 r#type: SendType::Text,
595 file: None,
596 text: Some(SendTextView {
597 text: Some("secret".to_string()),
598 hidden: false,
599 }),
600 data: None,
601 max_access_count: None,
602 access_count: 0,
603 disabled: false,
604 hide_email: false,
605 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
606 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
607 expiration_date: None,
608 emails: Vec::new(),
609 auth_type: AuthType::None,
610 };
611 let mut existing_send = store.encrypt(baseline).unwrap();
612 existing_send.id = Some(crate::send::SendId::new(send_id));
613 existing_send.password = password_hash;
614 existing_send.emails = emails;
615 existing_send.auth_type = auth_type;
616
617 let repository = MemoryRepository::<Send>::default();
618 repository
619 .set(SendId::new(send_id), existing_send)
620 .await
621 .unwrap();
622
623 (store, repository)
624 }
625
626 async fn capture_edit_put_model(
628 store: &KeyStore<KeySlotIds>,
629 repository: &MemoryRepository<Send>,
630 send_id: uuid::Uuid,
631 request: SendEditRequest,
632 ) -> bitwarden_api_api::models::SendRequestModel {
633 let captured: std::sync::Arc<
634 std::sync::Mutex<Option<bitwarden_api_api::models::SendRequestModel>>,
635 > = std::sync::Arc::new(std::sync::Mutex::new(None));
636 let sink = captured.clone();
637
638 let api_client = ApiClient::new_mocked(move |mock| {
639 let sink = sink.clone();
640 mock.sends_api
641 .expect_put()
642 .returning(move |_id, model| {
643 let model = model.unwrap();
644 *sink.lock().unwrap() = Some(model.clone());
645 Ok(SendResponseModel {
646 id: Some(send_id),
647 name: model.name.clone(),
648 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
649 object: Some("send".to_string()),
650 access_id: None,
651 r#type: model.r#type,
652 auth_type: model.auth_type,
653 notes: model.notes.clone(),
654 file: model.file.clone(),
655 text: model.text.clone(),
656 data: model.data.clone(),
657 key: Some(model.key.clone()),
658 max_access_count: model.max_access_count,
659 access_count: Some(0),
660 password: model.password.clone(),
661 emails: model.emails.clone(),
662 disabled: Some(model.disabled),
663 expiration_date: model.expiration_date.clone(),
664 deletion_date: Some(model.deletion_date.clone()),
665 hide_email: model.hide_email,
666 })
667 })
668 .once();
669 });
670
671 edit_send(
672 store,
673 &api_client,
674 repository,
675 SendId::new(send_id),
676 request,
677 )
678 .await
679 .unwrap();
680
681 captured.lock().unwrap().take().expect("PUT was not called")
682 }
683
684 #[tokio::test]
687 async fn test_edit_preserves_existing_password_when_auth_is_none() {
688 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
689 let existing_hash = "abc123hashstub==".to_string();
690 let (store, repository) =
691 make_fixture_with_existing_auth(send_id, Some(existing_hash), None, AuthType::Password)
692 .await;
693
694 let model = capture_edit_put_model(
695 &store,
696 &repository,
697 send_id,
698 SendEditRequest {
699 name: "updated".to_string(),
700 notes: None,
701 view_type: SendViewType::Text(SendTextView {
702 text: Some("secret".to_string()),
703 hidden: false,
704 }),
705 max_access_count: None,
706 disabled: false,
707 hide_email: false,
708 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
709 expiration_date: None,
710 auth: AuthEdit::Preserve,
711 },
712 )
713 .await;
714
715 assert_eq!(
716 model.password, None,
717 "preserve mode must omit the password hash so the server retains the stored value",
718 );
719 assert_eq!(model.emails, None);
720 assert_eq!(
721 model.auth_type,
722 Some(bitwarden_api_api::models::AuthType::Password),
723 "preserve mode must forward the existing authType so the server doesn't clear auth",
724 );
725 }
726
727 #[tokio::test]
728 async fn test_edit_preserves_existing_emails_when_auth_is_none() {
729 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
730 let existing_emails = "[email protected],[email protected]".to_string();
731 let (store, repository) =
732 make_fixture_with_existing_auth(send_id, None, Some(existing_emails), AuthType::Email)
733 .await;
734
735 let model = capture_edit_put_model(
736 &store,
737 &repository,
738 send_id,
739 SendEditRequest {
740 name: "updated".to_string(),
741 notes: None,
742 view_type: SendViewType::Text(SendTextView {
743 text: Some("secret".to_string()),
744 hidden: false,
745 }),
746 max_access_count: None,
747 disabled: false,
748 hide_email: false,
749 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
750 expiration_date: None,
751 auth: AuthEdit::Preserve,
752 },
753 )
754 .await;
755
756 assert_eq!(model.password, None);
757 assert_eq!(
758 model.emails, None,
759 "preserve mode must omit the email list so the server retains the stored value",
760 );
761 assert_eq!(
762 model.auth_type,
763 Some(bitwarden_api_api::models::AuthType::Email),
764 "preserve mode must forward the existing authType so the server doesn't clear auth",
765 );
766 }
767
768 #[tokio::test]
769 async fn test_edit_preserves_no_auth_when_auth_is_none() {
770 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
771 let (store, repository) =
772 make_fixture_with_existing_auth(send_id, None, None, AuthType::None).await;
773
774 let model = capture_edit_put_model(
775 &store,
776 &repository,
777 send_id,
778 SendEditRequest {
779 name: "updated".to_string(),
780 notes: None,
781 view_type: SendViewType::Text(SendTextView {
782 text: Some("secret".to_string()),
783 hidden: false,
784 }),
785 max_access_count: None,
786 disabled: false,
787 hide_email: false,
788 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
789 expiration_date: None,
790 auth: AuthEdit::Preserve,
791 },
792 )
793 .await;
794
795 assert_eq!(model.password, None);
796 assert_eq!(model.emails, None);
797 assert_eq!(
798 model.auth_type,
799 Some(bitwarden_api_api::models::AuthType::None),
800 );
801 }
802
803 #[tokio::test]
806 async fn test_edit_explicit_auth_none_overrides_existing_password() {
807 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
808 let (store, repository) = make_fixture_with_existing_auth(
809 send_id,
810 Some("existing-hash".to_string()),
811 None,
812 AuthType::Password,
813 )
814 .await;
815
816 let model = capture_edit_put_model(
817 &store,
818 &repository,
819 send_id,
820 SendEditRequest {
821 name: "updated".to_string(),
822 notes: None,
823 view_type: SendViewType::Text(SendTextView {
824 text: Some("secret".to_string()),
825 hidden: false,
826 }),
827 max_access_count: None,
828 disabled: false,
829 hide_email: false,
830 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
831 expiration_date: None,
832 auth: AuthEdit::Set {
833 auth: SendAuthType::None,
834 },
835 },
836 )
837 .await;
838
839 assert_eq!(model.password, None);
840 assert_eq!(model.emails, None);
841 assert_eq!(
842 model.auth_type,
843 Some(bitwarden_api_api::models::AuthType::None),
844 );
845 }
846}