Skip to content

Commit 96c755f

Browse files
committed
feat: add repo write permissions and tests - Add can_write field to SnowbirdRepo, test permissions for creator and joined backends
1 parent 4866989 commit 96c755f

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,197 @@ mod tests {
625625
Ok(())
626626
}
627627

628+
#[actix_web::test]
629+
async fn test_concurrent_downloads() -> Result<()> {
630+
use std::rc::Rc;
631+
632+
// Initialize the app
633+
let path = TmpDir::new("test_concurrent_downloads").await?;
634+
635+
BACKEND.get_or_init(|| init_backend(path.to_path_buf().as_path()));
636+
{
637+
let backend = get_backend().await?;
638+
backend.start().await.expect("Backend failed to start");
639+
}
640+
641+
let app = Rc::new(
642+
test::init_service(
643+
App::new()
644+
.service(status)
645+
.service(web::scope("/api").service(groups::scope())),
646+
)
647+
.await
648+
);
649+
650+
// Step 1: Create a group
651+
let create_group_req = test::TestRequest::post()
652+
.uri("/api/groups")
653+
.set_json(json!({ "name": "Test Group" }))
654+
.to_request();
655+
let create_group_resp: serde_json::Value =
656+
test::call_and_read_body_json(&app, create_group_req).await;
657+
let group_id = create_group_resp["key"]
658+
.as_str()
659+
.expect("No group key returned");
660+
661+
// Step 2: Create a repo
662+
let create_repo_req = test::TestRequest::post()
663+
.uri(&format!("/api/groups/{}/repos", group_id))
664+
.set_json(json!({ "name": "Test Repo" }))
665+
.to_request();
666+
let create_repo_resp: serde_json::Value =
667+
test::call_and_read_body_json(&app, create_repo_req).await;
668+
let repo_id = create_repo_resp["key"]
669+
.as_str()
670+
.expect("No repo key returned");
671+
672+
// Step 3: Upload multiple files
673+
let file_contents = vec![
674+
("file1.txt", b"Content for file 1"),
675+
("file2.txt", b"Content for file 2"),
676+
("file3.txt", b"Content for file 3"),
677+
];
678+
679+
for (file_name, content) in &file_contents {
680+
let upload_req = test::TestRequest::post()
681+
.uri(&format!(
682+
"/api/groups/{}/repos/{}/media/{}",
683+
group_id, repo_id, file_name
684+
))
685+
.set_payload(content.to_vec())
686+
.to_request();
687+
let upload_resp = test::call_service(&app, upload_req).await;
688+
assert!(upload_resp.status().is_success(), "File upload failed for {}", file_name);
689+
}
690+
691+
// Step 4: Create download requests
692+
let mut download_futures = Vec::new();
693+
694+
for (file_name, content) in &file_contents {
695+
let get_file_req = test::TestRequest::get()
696+
.uri(&format!(
697+
"/api/groups/{}/repos/{}/media/{}",
698+
group_id, repo_id, file_name
699+
))
700+
.to_request();
701+
702+
let content = content.to_vec();
703+
let file_name = file_name.to_string();
704+
let app = Rc::clone(&app);
705+
706+
// Create a future for each download
707+
let download_future = async move {
708+
let resp = test::call_service(&app, get_file_req).await;
709+
assert!(resp.status().is_success(), "File download failed for {}", file_name);
710+
711+
let got_file_data = test::read_body(resp).await;
712+
assert_eq!(
713+
got_file_data.to_vec().as_slice(),
714+
content.as_slice(),
715+
"Downloaded content mismatch for {}",
716+
file_name
717+
);
718+
};
719+
720+
download_futures.push(download_future);
721+
}
722+
723+
// Execute all downloads concurrently
724+
futures::future::join_all(download_futures).await;
725+
726+
// Clean up: Stop the backend
727+
{
728+
let backend = get_backend().await?;
729+
backend.stop().await.expect("Backend failed to stop");
730+
}
731+
732+
Ok(())
733+
}
734+
735+
#[actix_web::test]
736+
async fn test_repo_permissions() -> Result<()> {
737+
// Initialize the app
738+
let path = TmpDir::new("test_repo_permissions").await?;
739+
740+
// Initialize backend2 first (this will be the creator of the group/repo)
741+
let store2 = iroh_blobs::store::fs::Store::load(path.to_path_buf().join("iroh2")).await?;
742+
let (veilid_api2, update_rx2) = save_dweb_backend::common::init_veilid(
743+
path.to_path_buf().join("test2").as_path(),
744+
"test2".to_string(),
745+
)
746+
.await?;
747+
let backend2 = save_dweb_backend::backend::Backend::from_dependencies(
748+
&path.to_path_buf(),
749+
veilid_api2.clone(),
750+
update_rx2,
751+
store2,
752+
)
753+
.await
754+
.unwrap();
755+
756+
// Initialize the main backend (this will join the group)
757+
BACKEND.get_or_init(|| init_backend(path.to_path_buf().as_path()));
758+
{
759+
let backend = get_backend().await?;
760+
backend.start().await.expect("Backend failed to start");
761+
}
762+
763+
// Create group and repo in backend2 (creator)
764+
let mut group = backend2.create_group().await?;
765+
let join_url = group.get_url();
766+
group.set_name(TEST_GROUP_NAME).await?;
767+
768+
let repo = group.create_repo().await?;
769+
repo.set_name(TEST_GROUP_NAME).await?;
770+
771+
// Verify creator has write access
772+
let creator_repo: SnowbirdRepo = repo.clone().into();
773+
assert!(creator_repo.can_write, "Creator should have write access");
774+
775+
// Join the group with the main backend
776+
{
777+
let backend = get_backend().await?;
778+
backend.join_from_url(join_url.as_str()).await?;
779+
}
780+
781+
let app = test::init_service(
782+
App::new()
783+
.service(status)
784+
.service(web::scope("/api").service(groups::scope())),
785+
)
786+
.await;
787+
788+
// Get the repo info through the API for the joined backend
789+
let get_repo_req = test::TestRequest::get()
790+
.uri(&format!(
791+
"/api/groups/{}/repos/{}",
792+
group.id().to_string(),
793+
repo.id().to_string()
794+
))
795+
.to_request();
796+
let joined_repo: SnowbirdRepo = test::call_and_read_body_json(&app, get_repo_req).await;
797+
798+
// Verify joined backend has read-only access
799+
assert!(!joined_repo.can_write, "Joined backend should have read-only access");
800+
801+
// List repos to verify permissions are consistent
802+
let list_repos_req = test::TestRequest::get()
803+
.uri(&format!("/api/groups/{}/repos", group.id().to_string()))
804+
.to_request();
805+
let repos_response: ReposResponse = test::call_and_read_body_json(&app, list_repos_req).await;
806+
807+
assert_eq!(repos_response.repos.len(), 1, "Should see one repo");
808+
assert!(!repos_response.repos[0].can_write, "Listed repo should show read-only access");
809+
810+
// Clean up
811+
backend2.stop().await?;
812+
{
813+
let backend = get_backend().await?;
814+
backend.stop().await.expect("Backend failed to stop");
815+
}
816+
tokio::time::sleep(Duration::from_secs(2)).await;
817+
veilid_api2.shutdown().await;
818+
628819
Ok(())
629820
}
630821
}

src/models.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ impl IntoSnowbirdGroupsWithNames for Vec<Box<Group>> {
104104
pub struct SnowbirdRepo {
105105
pub key: String,
106106
pub name: String,
107+
pub can_write: bool,
107108
}
108109

109110
#[async_trait::async_trait]
@@ -120,6 +121,7 @@ impl AsyncFrom<Repo> for SnowbirdRepo {
120121
.get_name()
121122
.await
122123
.unwrap_or_else(|_| "Unknown".to_string()),
124+
can_write: repo.can_write(),
123125
}
124126
}
125127
}
@@ -129,6 +131,7 @@ impl From<&Repo> for SnowbirdRepo {
129131
SnowbirdRepo {
130132
key: repo.id().to_string(),
131133
name: "".to_string(),
134+
can_write: repo.can_write(),
132135
}
133136
}
134137
}
@@ -138,6 +141,7 @@ impl From<Repo> for SnowbirdRepo {
138141
SnowbirdRepo {
139142
key: repo.id().to_string(),
140143
name: "".to_string(),
144+
can_write: repo.can_write(),
141145
}
142146
}
143147
}

0 commit comments

Comments
 (0)