/ src / api / v1 / delegates.rs
delegates.rs
  1  use axum::extract::State;
  2  use axum::response::IntoResponse;
  3  use axum::routing::get;
  4  use axum::{Json, Router};
  5  
  6  use radicle::identity::Did;
  7  use radicle::storage::ReadStorage;
  8  
  9  use crate::api::error::Error;
 10  use crate::api::query::{PaginationQuery, RepoQuery};
 11  use crate::api::Context;
 12  use crate::axum_extra::{Path, Query};
 13  
 14  pub fn router(ctx: Context) -> Router {
 15      Router::new()
 16          .route("/delegates/{did}/repos", get(delegates_repos_handler))
 17          .with_state(ctx)
 18  }
 19  
 20  /// List all repos which delegate is a part of.
 21  /// `GET /delegates/:did/repos`
 22  async fn delegates_repos_handler(
 23      State(ctx): State<Context>,
 24      Path(did): Path<Did>,
 25      Query(qs): Query<PaginationQuery>,
 26  ) -> impl IntoResponse {
 27      let PaginationQuery {
 28          show,
 29          page,
 30          per_page,
 31      } = qs;
 32      let page = page.unwrap_or(0);
 33      let per_page = per_page.unwrap_or(10);
 34      let storage = &ctx.profile.storage;
 35      let web_config = ctx.web_config().read().await;
 36      let pinned = &web_config.pinned;
 37      let mut repos = match show {
 38          RepoQuery::All => storage
 39              .repositories()?
 40              .into_iter()
 41              .filter(|repo| repo.doc.visibility().is_public())
 42              .filter(|repo| repo.doc.delegates().iter().any(|d| *d == did))
 43              .collect::<Vec<_>>(),
 44          RepoQuery::Pinned => storage
 45              .repositories_by_id(pinned.repositories.iter())
 46              .filter_map(|result| match result {
 47                  Ok(repo) => Some(repo),
 48                  Err(e) => {
 49                      tracing::warn!("Failed to load pinned repository: {}", e);
 50                      None
 51                  }
 52              })
 53              .filter(|repo| repo.doc.visibility().is_public())
 54              .filter(|repo| repo.doc.delegates().iter().any(|d| *d == did))
 55              .collect::<Vec<_>>(),
 56      };
 57      repos.sort_by_key(|p| p.rid);
 58  
 59      let infos = repos
 60          .into_iter()
 61          .filter_map(|id| {
 62              let Ok((repo, doc)) = ctx.repo(id.rid) else {
 63                  return None;
 64              };
 65              let Ok(repo_info) = ctx.repo_info(&repo, doc) else {
 66                  return None;
 67              };
 68  
 69              Some(repo_info)
 70          })
 71          .skip(page * per_page)
 72          .take(per_page)
 73          .collect::<Vec<_>>();
 74  
 75      Ok::<_, Error>(Json(infos))
 76  }
 77  
 78  #[cfg(test)]
 79  mod routes {
 80      use std::net::SocketAddr;
 81  
 82      use axum::extract::connect_info::MockConnectInfo;
 83      use axum::http::StatusCode;
 84      use serde_json::json;
 85  
 86      use crate::test::{self, get, CONTRIBUTOR_ALIAS, DID, HEAD, RID};
 87  
 88      #[tokio::test]
 89      async fn test_delegates_repos() {
 90          let tmp = tempfile::tempdir().unwrap();
 91          let seed = test::seed(tmp.path());
 92          let app = super::router(seed.clone())
 93              .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))));
 94          let response = get(
 95              &app,
 96              "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/repos?show=all",
 97          )
 98          .await;
 99  
100          assert_eq!(
101              response.status(),
102              StatusCode::OK,
103              "failed response: {:?}",
104              response.json().await
105          );
106          assert_eq!(
107              response.json().await,
108              json!([
109                {
110                  "payloads": {
111                    "xyz.radicle.project": {
112                      "data": {
113                        "defaultBranch": "master",
114                        "description": "Rad repository for tests",
115                        "name": "hello-world",
116                      },
117                      "meta": {
118                        "head": HEAD,
119                        "patches": {
120                          "open": 1,
121                          "draft": 0,
122                          "archived": 0,
123                          "merged": 0,
124                        },
125                        "issues": {
126                          "open": 1,
127                          "closed": 0,
128                        },
129                      }
130                    }
131                  },
132                  "delegates": [
133                    {
134                      "id": DID,
135                      "alias": CONTRIBUTOR_ALIAS
136                    }
137                  ],
138                  "threshold": 1,
139                  "visibility": {
140                    "type": "public"
141                  },
142                  "rid": RID,
143                  "seeding": 1,
144                },
145                {
146                  "payloads": {
147                    "xyz.radicle.project": {
148                      "data": {
149                        "defaultBranch": "master",
150                        "description": "Rad repository for sorting",
151                        "name": "again-hello-world",
152                      },
153                      "meta": {
154                        "head": "344dcd184df5bf37aab6c107fa9371a1c5b3321a",
155                        "patches": {
156                          "open": 0,
157                          "draft": 0,
158                          "archived": 0,
159                          "merged": 0,
160                        },
161                        "issues": {
162                          "open": 0,
163                          "closed": 0,
164                        },
165                      }
166                    }
167                  },
168                  "delegates": [
169                    {
170                      "id": DID,
171                      "alias": CONTRIBUTOR_ALIAS
172                    },
173                  ],
174                  "threshold": 1,
175                  "visibility": {
176                    "type": "public"
177                  },
178                  "rid": "rad:z4GypKmh1gkEfmkXtarcYnkvtFUfE",
179                  "seeding": 1,
180                }
181              ])
182          );
183  
184          let app = super::router(seed).layer(MockConnectInfo(SocketAddr::from((
185              [192, 168, 13, 37],
186              8080,
187          ))));
188          let response = get(
189              &app,
190              "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/repos?show=all",
191          )
192          .await;
193  
194          assert_eq!(
195              response.status(),
196              StatusCode::OK,
197              "failed response: {:?}",
198              response.json().await
199          );
200          assert_eq!(
201              response.json().await,
202              json!([
203                {
204                  "payloads": {
205                    "xyz.radicle.project": {
206                      "data": {
207                        "defaultBranch": "master",
208                        "description": "Rad repository for tests",
209                        "name": "hello-world",
210                      },
211                      "meta": {
212                        "head": HEAD,
213                        "patches": {
214                          "open": 1,
215                          "draft": 0,
216                          "archived": 0,
217                          "merged": 0,
218                        },
219                        "issues": {
220                          "open": 1,
221                          "closed": 0,
222                        },
223                      }
224                    }
225                  },
226                  "delegates": [
227                    {
228                      "id": DID,
229                      "alias": CONTRIBUTOR_ALIAS
230                    }
231                  ],
232                  "threshold": 1,
233                  "visibility": {
234                    "type": "public"
235                  },
236                  "rid": RID,
237                  "seeding": 1,
238                },
239                {
240                  "payloads": {
241                    "xyz.radicle.project": {
242                      "data": {
243                        "defaultBranch": "master",
244                        "description": "Rad repository for sorting",
245                        "name": "again-hello-world",
246                      },
247                      "meta": {
248                        "head": "344dcd184df5bf37aab6c107fa9371a1c5b3321a",
249                        "patches": {
250                          "open": 0,
251                          "draft": 0,
252                          "archived": 0,
253                          "merged": 0,
254                        },
255                        "issues": {
256                          "open": 0,
257                          "closed": 0,
258                        },
259                      }
260                    }
261                  },
262                  "delegates": [
263                    {
264                      "id": DID,
265                      "alias": CONTRIBUTOR_ALIAS
266                    },
267                  ],
268                  "threshold": 1,
269                  "visibility": {
270                    "type": "public"
271                  },
272                  "rid": "rad:z4GypKmh1gkEfmkXtarcYnkvtFUfE",
273                  "seeding": 1,
274                }
275              ])
276          );
277      }
278  }