upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/lib/repo_ref.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-09-04 08:04:48 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-04 13:30:59 +0100
commit949c6459aa7683453a7160423b689ceadb08954b (patch)
tree230c26ecb11b99916e5570e548673eb09ecf0a36 /src/lib/repo_ref.rs
parenta825311f2c55661aaab3a163bda9109295c96044 (diff)
refactor: organise into lib and bin structure
the make the code more readable this commit just moves the files, the next commit should fix the imports
Diffstat (limited to 'src/lib/repo_ref.rs')
-rw-r--r--src/lib/repo_ref.rs700
1 files changed, 700 insertions, 0 deletions
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
new file mode 100644
index 0000000..0e57d96
--- /dev/null
+++ b/src/lib/repo_ref.rs
@@ -0,0 +1,700 @@
1use std::{
2 collections::{HashMap, HashSet},
3 fs::File,
4 io::BufReader,
5 str::FromStr,
6};
7
8use anyhow::{bail, Context, Result};
9use console::Style;
10use nostr::{nips::nip01::Coordinate, FromBech32, PublicKey, Tag, TagStandard, ToBech32};
11use nostr_sdk::{Kind, NostrSigner, Timestamp};
12use serde::{Deserialize, Serialize};
13
14#[cfg(not(test))]
15use crate::client::Client;
16use crate::{
17 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms},
18 client::{get_event_from_global_cache, get_events_from_cache, sign_event, Connect},
19 git::{NostrUrlDecoded, Repo, RepoActions},
20};
21
22#[derive(Default)]
23pub struct RepoRef {
24 pub name: String,
25 pub description: String,
26 pub identifier: String,
27 pub root_commit: String,
28 pub git_server: Vec<String>,
29 pub web: Vec<String>,
30 pub relays: Vec<String>,
31 pub maintainers: Vec<PublicKey>,
32 pub events: HashMap<Coordinate, nostr::Event>,
33 // code languages and hashtags
34}
35
36impl TryFrom<nostr::Event> for RepoRef {
37 type Error = anyhow::Error;
38
39 fn try_from(event: nostr::Event) -> Result<Self> {
40 if !event.kind.eq(&Kind::GitRepoAnnouncement) {
41 bail!("incorrect kind");
42 }
43 let mut r = Self::default();
44
45 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("d")) {
46 r.identifier = t.as_vec()[1].clone();
47 }
48
49 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("name")) {
50 r.name = t.as_vec()[1].clone();
51 }
52
53 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("description")) {
54 r.description = t.as_vec()[1].clone();
55 }
56
57 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("clone")) {
58 r.git_server = t.clone().to_vec();
59 r.git_server.remove(0);
60 }
61
62 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("web")) {
63 r.web = t.clone().to_vec();
64 r.web.remove(0);
65 }
66
67 if let Some(t) = event.tags.iter().find(|t| {
68 t.as_vec()[0].eq("r")
69 && t.as_vec()[1].len().eq(&40)
70 && git2::Oid::from_str(t.as_vec()[1].as_str()).is_ok()
71 }) {
72 r.root_commit = t.as_vec()[1].clone();
73 }
74
75 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("relays")) {
76 r.relays = t.clone().to_vec();
77 r.relays.remove(0);
78 }
79
80 if let Some(t) = event.tags.iter().find(|t| t.as_vec()[0].eq("maintainers")) {
81 let mut maintainers = t.clone().to_vec();
82 maintainers.remove(0);
83 if !maintainers.contains(&event.pubkey.to_string()) {
84 r.maintainers.push(event.pubkey);
85 }
86 for pk in maintainers {
87 r.maintainers.push(
88 nostr_sdk::prelude::PublicKey::from_str(&pk)
89 .context(format!("cannot convert entry from maintainers tag {pk} into a valid nostr public key. it should be in hex format"))
90 .context("invalid repository event")?,
91 );
92 }
93 } else {
94 r.maintainers = vec![event.pubkey];
95 }
96 r.events = HashMap::new();
97 r.events.insert(
98 Coordinate {
99 kind: event.kind,
100 identifier: event.identifier().unwrap().to_string(),
101 public_key: event.author(),
102 relays: vec![],
103 },
104 event,
105 );
106 Ok(r)
107 }
108}
109
110impl RepoRef {
111 pub async fn to_event(&self, signer: &NostrSigner) -> Result<nostr::Event> {
112 sign_event(
113 nostr_sdk::EventBuilder::new(
114 nostr::event::Kind::GitRepoAnnouncement,
115 "",
116 [
117 vec![
118 Tag::identifier(if self.identifier.to_string().is_empty() {
119 // fiatjaf thought a random string. its not in the draft nip.
120 // thread_rng()
121 // .sample_iter(&Alphanumeric)
122 // .take(15)
123 // .map(char::from)
124 // .collect()
125
126 // an identifier based on first commit is better so that users dont
127 // accidentally create two seperate identifiers for the same repo
128 // there is a hesitancy to use the commit id
129 // in another conversaion with fiatjaf he suggested the first 6
130 // character of the commit id
131 // here we are using 7 which is the standard for shorthand commit id
132 self.root_commit.to_string()[..7].to_string()
133 } else {
134 self.identifier.to_string()
135 }),
136 Tag::custom(
137 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("r")),
138 vec![self.root_commit.to_string(), "euc".to_string()],
139 ),
140 Tag::from_standardized(TagStandard::Name(self.name.clone())),
141 Tag::from_standardized(TagStandard::Description(self.description.clone())),
142 Tag::custom(
143 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")),
144 self.git_server.clone(),
145 ),
146 Tag::custom(
147 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("web")),
148 self.web.clone(),
149 ),
150 Tag::custom(
151 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("relays")),
152 self.relays.clone(),
153 ),
154 Tag::custom(
155 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("maintainers")),
156 self.maintainers
157 .iter()
158 .map(std::string::ToString::to_string)
159 .collect::<Vec<String>>(),
160 ),
161 Tag::custom(
162 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
163 vec![format!("git repository: {}", self.name.clone())],
164 ),
165 ],
166 // code languages and hashtags
167 ]
168 .concat(),
169 ),
170 signer,
171 )
172 .await
173 .context("failed to create repository reference event")
174 }
175 /// coordinates without relay hints
176 pub fn coordinates(&self) -> HashSet<Coordinate> {
177 let mut res = HashSet::new();
178 for m in &self.maintainers {
179 res.insert(Coordinate {
180 kind: Kind::GitRepoAnnouncement,
181 public_key: *m,
182 identifier: self.identifier.clone(),
183 relays: vec![],
184 });
185 }
186 res
187 }
188
189 /// coordinates without relay hints
190 pub fn coordinate_with_hint(&self) -> Coordinate {
191 Coordinate {
192 kind: Kind::GitRepoAnnouncement,
193 public_key: *self
194 .maintainers
195 .first()
196 .context("no maintainers in repo ref")
197 .unwrap(),
198 identifier: self.identifier.clone(),
199 relays: if let Some(relay) = self.relays.first() {
200 vec![relay.to_string()]
201 } else {
202 vec![]
203 },
204 }
205 }
206
207 /// coordinates without relay hints
208 pub fn coordinates_with_timestamps(&self) -> Vec<(Coordinate, Option<Timestamp>)> {
209 self.coordinates()
210 .iter()
211 .map(|c| (c.clone(), self.events.get(c).map(|e| e.created_at)))
212 .collect::<Vec<(Coordinate, Option<Timestamp>)>>()
213 }
214}
215
216pub async fn get_repo_coordinates(
217 git_repo: &Repo,
218 #[cfg(test)] client: &crate::client::MockConnect,
219 #[cfg(not(test))] client: &Client,
220) -> Result<HashSet<Coordinate>> {
221 try_and_get_repo_coordinates(git_repo, client, true).await
222}
223
224pub async fn try_and_get_repo_coordinates(
225 git_repo: &Repo,
226 #[cfg(test)] client: &crate::client::MockConnect,
227 #[cfg(not(test))] client: &Client,
228 prompt_user: bool,
229) -> Result<HashSet<Coordinate>> {
230 let mut repo_coordinates = get_repo_coordinates_from_git_config(git_repo)?;
231
232 if repo_coordinates.is_empty() {
233 repo_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo)?;
234 }
235
236 if repo_coordinates.is_empty() {
237 repo_coordinates = get_repo_coordinates_from_maintainers_yaml(git_repo, client).await?;
238 }
239
240 if repo_coordinates.is_empty() {
241 if prompt_user {
242 repo_coordinates = get_repo_coordinates_from_user_prompt(git_repo)?;
243 } else {
244 bail!("couldn't find repo coordinates in git config nostr.repo or in maintainers.yaml");
245 }
246 }
247 Ok(repo_coordinates)
248}
249
250fn get_repo_coordinates_from_git_config(git_repo: &Repo) -> Result<HashSet<Coordinate>> {
251 let mut repo_coordinates = HashSet::new();
252 if let Some(repo_override) = git_repo.get_git_config_item("nostr.repo", Some(false))? {
253 for s in repo_override.split(',') {
254 if let Ok(c) = Coordinate::parse(s) {
255 repo_coordinates.insert(c);
256 }
257 }
258 }
259 Ok(repo_coordinates)
260}
261
262fn get_repo_coordinates_from_nostr_remotes(git_repo: &Repo) -> Result<HashSet<Coordinate>> {
263 let mut repo_coordinates = HashSet::new();
264 for remote_name in git_repo.git_repo.remotes()?.iter().flatten() {
265 if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() {
266 if let Ok(nostr_url_decoded) = NostrUrlDecoded::from_str(remote_url) {
267 for c in nostr_url_decoded.coordinates {
268 repo_coordinates.insert(c);
269 }
270 }
271 }
272 }
273 Ok(repo_coordinates)
274}
275
276async fn get_repo_coordinates_from_maintainers_yaml(
277 git_repo: &Repo,
278 #[cfg(test)] client: &crate::client::MockConnect,
279 #[cfg(not(test))] client: &Client,
280) -> Result<HashSet<Coordinate>> {
281 let mut repo_coordinates = HashSet::new();
282 if let Ok(repo_config) = get_repo_config_from_yaml(git_repo) {
283 let maintainers = {
284 let mut maintainers = HashSet::new();
285 for m in &repo_config.maintainers {
286 if let Ok(maintainer) = PublicKey::parse(m) {
287 maintainers.insert(maintainer);
288 }
289 }
290 maintainers
291 };
292 if let Some(identifier) = repo_config.identifier {
293 for public_key in maintainers {
294 repo_coordinates.insert(Coordinate {
295 kind: Kind::GitRepoAnnouncement,
296 public_key,
297 identifier: identifier.clone(),
298 relays: vec![],
299 });
300 }
301 } else {
302 // if repo_config.identifier.is_empty() {
303 // this will only apply for a few repositories created before ngit v1.3
304 // that haven't updated their maintainers.yaml
305 if let Ok(Some(current_user_npub)) = git_repo.get_git_config_item("nostr.npub", None) {
306 if let Ok(current_user) = PublicKey::parse(current_user_npub) {
307 for m in &repo_config.maintainers {
308 if let Ok(maintainer) = PublicKey::parse(m) {
309 if current_user.eq(&maintainer) {
310 println!(
311 "please run `ngit init` to add the repo identifier to maintainers.yaml"
312 );
313 }
314 }
315 }
316 }
317 }
318 // look find all repo refs with root_commit. for identifier
319 let filter = nostr::Filter::default()
320 .kind(nostr::Kind::GitRepoAnnouncement)
321 .reference(git_repo.get_root_commit()?.to_string())
322 .authors(maintainers.clone());
323 let mut events =
324 get_events_from_cache(git_repo.get_path()?, vec![filter.clone()]).await?;
325 if events.is_empty() {
326 events =
327 get_event_from_global_cache(git_repo.get_path()?, vec![filter.clone()]).await?;
328 }
329 if events.is_empty() {
330 println!(
331 "finding repository events for this repository for npubs in maintainers.yaml"
332 );
333 events = client
334 .get_events(client.get_fallback_relays().clone(), vec![filter.clone()])
335 .await?;
336 }
337 if let Some(e) = events.first() {
338 if let Some(identifier) = e.identifier() {
339 for m in &repo_config.maintainers {
340 if let Ok(maintainer) = PublicKey::parse(m) {
341 repo_coordinates.insert(Coordinate {
342 kind: Kind::GitRepoAnnouncement,
343 public_key: maintainer,
344 identifier: identifier.to_string(),
345 relays: vec![],
346 });
347 }
348 }
349 }
350 } else {
351 let c = ask_for_naddr()?;
352 git_repo.save_git_config_item("nostr.repo", &c.to_bech32()?, false)?;
353 repo_coordinates.insert(c);
354 }
355 }
356 }
357 Ok(repo_coordinates)
358}
359
360fn get_repo_coordinates_from_user_prompt(git_repo: &Repo) -> Result<HashSet<Coordinate>> {
361 let mut repo_coordinates = HashSet::new();
362 // TODO: present list of events filter by root_commit
363 // TODO: fallback to search based on identifier
364 let c = ask_for_naddr()?;
365 // PROBLEM: we are saving this before checking whether it actually exists, which
366 // means next time the user won't be prompted and may not know how to
367 // change the selected repo
368 git_repo.save_git_config_item("nostr.repo", &c.to_bech32()?, false)?;
369 repo_coordinates.insert(c);
370 Ok(repo_coordinates)
371}
372
373fn ask_for_naddr() -> Result<Coordinate> {
374 let dim = Style::new().color256(247);
375 println!(
376 "{}",
377 dim.apply_to("hint: https://gitworkshop.dev/repos lists repositories and their naddr"),
378 );
379
380 Ok(loop {
381 if let Ok(c) = Coordinate::parse(
382 Interactor::default()
383 .input(PromptInputParms::default().with_prompt("repository naddr"))?,
384 ) {
385 break c;
386 }
387 println!("not a valid naddr");
388 })
389}
390
391#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
392pub struct RepoConfigYaml {
393 pub identifier: Option<String>,
394 pub maintainers: Vec<String>,
395 pub relays: Vec<String>,
396}
397
398pub fn get_repo_config_from_yaml(git_repo: &Repo) -> Result<RepoConfigYaml> {
399 let path = git_repo.get_path()?.join("maintainers.yaml");
400 let file = File::open(path)
401 .context("should open maintainers.yaml if it exists")
402 .context("maintainers.yaml doesnt exist")?;
403 let reader = BufReader::new(file);
404 let repo_config_yaml: RepoConfigYaml = serde_yaml::from_reader(reader)
405 .context("should read maintainers.yaml with serde_yaml")
406 .context("maintainers.yaml incorrectly formatted")?;
407 Ok(repo_config_yaml)
408}
409
410pub fn extract_pks(pk_strings: Vec<String>) -> Result<Vec<PublicKey>> {
411 let mut pks: Vec<PublicKey> = vec![];
412 for s in pk_strings {
413 pks.push(
414 nostr_sdk::prelude::PublicKey::from_bech32(s.clone())
415 .context(format!("cannot convert {s} into a valid nostr public key"))?,
416 );
417 }
418 Ok(pks)
419}
420
421pub fn save_repo_config_to_yaml(
422 git_repo: &Repo,
423 identifier: String,
424 maintainers: Vec<PublicKey>,
425 relays: Vec<String>,
426) -> Result<()> {
427 let path = git_repo.get_path()?.join("maintainers.yaml");
428 let file = if path.exists() {
429 std::fs::OpenOptions::new()
430 .create(true)
431 .write(true)
432 .truncate(true)
433 .open(path)
434 .context("cannot open maintainers.yaml file with write and truncate options")?
435 } else {
436 std::fs::File::create(path).context("cannot create maintainers.yaml file")?
437 };
438 let mut maintainers_npubs = vec![];
439 for m in maintainers {
440 maintainers_npubs.push(
441 m.to_bech32()
442 .context("cannot convert public key into npub")?,
443 );
444 }
445 serde_yaml::to_writer(
446 file,
447 &RepoConfigYaml {
448 identifier: Some(identifier),
449 maintainers: maintainers_npubs,
450 relays,
451 },
452 )
453 .context("cannot write maintainers to maintainers.yaml file serde_yaml")
454}
455
456#[cfg(test)]
457mod tests {
458 use test_utils::*;
459
460 use super::*;
461
462 async fn create() -> nostr::Event {
463 RepoRef {
464 identifier: "123412341".to_string(),
465 name: "test name".to_string(),
466 description: "test description".to_string(),
467 root_commit: "5e664e5a7845cd1373c79f580ca4fe29ab5b34d2".to_string(),
468 git_server: vec!["https://localhost:1000".to_string()],
469 web: vec![
470 "https://exampleproject.xyz".to_string(),
471 "https://gitworkshop.dev/123".to_string(),
472 ],
473 relays: vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()],
474 maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],
475 events: HashMap::new(),
476 }
477 .to_event(&TEST_KEY_1_SIGNER)
478 .await
479 .unwrap()
480 }
481 mod try_from {
482 use super::*;
483
484 #[tokio::test]
485 async fn identifier() {
486 assert_eq!(
487 RepoRef::try_from(create().await).unwrap().identifier,
488 "123412341",
489 )
490 }
491
492 #[tokio::test]
493 async fn name() {
494 assert_eq!(RepoRef::try_from(create().await).unwrap().name, "test name",)
495 }
496
497 #[tokio::test]
498 async fn description() {
499 assert_eq!(
500 RepoRef::try_from(create().await).unwrap().description,
501 "test description",
502 )
503 }
504
505 #[tokio::test]
506 async fn root_commit_is_r_tag() {
507 assert_eq!(
508 RepoRef::try_from(create().await).unwrap().root_commit,
509 "5e664e5a7845cd1373c79f580ca4fe29ab5b34d2",
510 )
511 }
512
513 mod root_commit_is_empty_if_no_r_tag_which_is_sha1_format {
514 use nostr::JsonUtil;
515
516 use super::*;
517 async fn create_with_incorrect_first_commit_ref(s: &str) -> nostr::Event {
518 nostr::Event::from_json(
519 create()
520 .await
521 .as_json()
522 .replace("5e664e5a7845cd1373c79f580ca4fe29ab5b34d2", s),
523 )
524 .unwrap()
525 }
526
527 #[tokio::test]
528 async fn less_than_40_characters() {
529 let s = "5e664e5a7845cd1373";
530 assert_eq!(
531 RepoRef::try_from(create_with_incorrect_first_commit_ref(s).await)
532 .unwrap()
533 .root_commit,
534 "",
535 )
536 }
537
538 #[tokio::test]
539 async fn more_than_40_characters() {
540 let s = "5e664e5a7845cd1373c79f580ca4fe29ab5b34d2111111111";
541 assert_eq!(
542 RepoRef::try_from(create_with_incorrect_first_commit_ref(s).await)
543 .unwrap()
544 .root_commit,
545 "",
546 )
547 }
548
549 #[tokio::test]
550 async fn not_hex_characters() {
551 let s = "xxx64e5a7845cd1373c79f580ca4fe29ab5b34d2";
552 assert_eq!(
553 RepoRef::try_from(create_with_incorrect_first_commit_ref(s).await)
554 .unwrap()
555 .root_commit,
556 "",
557 )
558 }
559 }
560
561 #[tokio::test]
562 async fn git_server() {
563 assert_eq!(
564 RepoRef::try_from(create().await).unwrap().git_server,
565 vec!["https://localhost:1000"],
566 )
567 }
568
569 #[tokio::test]
570 async fn web() {
571 assert_eq!(
572 RepoRef::try_from(create().await).unwrap().web,
573 vec![
574 "https://exampleproject.xyz".to_string(),
575 "https://gitworkshop.dev/123".to_string()
576 ],
577 )
578 }
579
580 #[tokio::test]
581 async fn relays() {
582 assert_eq!(
583 RepoRef::try_from(create().await).unwrap().relays,
584 vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()],
585 )
586 }
587
588 #[tokio::test]
589 async fn maintainers() {
590 assert_eq!(
591 RepoRef::try_from(create().await).unwrap().maintainers,
592 vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],
593 )
594 }
595 }
596
597 mod to_event {
598 use super::*;
599 mod tags {
600 use super::*;
601
602 #[tokio::test]
603 async fn identifier() {
604 assert!(
605 create()
606 .await
607 .tags
608 .iter()
609 .any(|t| t.as_vec()[0].eq("d") && t.as_vec()[1].eq("123412341"))
610 )
611 }
612
613 #[tokio::test]
614 async fn name() {
615 assert!(
616 create()
617 .await
618 .tags
619 .iter()
620 .any(|t| t.as_vec()[0].eq("name") && t.as_vec()[1].eq("test name"))
621 )
622 }
623
624 #[tokio::test]
625 async fn alt() {
626 assert!(
627 create().await.tags.iter().any(|t| t.as_vec()[0].eq("alt")
628 && t.as_vec()[1].eq("git repository: test name"))
629 )
630 }
631
632 #[tokio::test]
633 async fn description() {
634 assert!(create().await.tags.iter().any(
635 |t| t.as_vec()[0].eq("description") && t.as_vec()[1].eq("test description")
636 ))
637 }
638
639 #[tokio::test]
640 async fn root_commit_as_reference() {
641 assert!(create().await.tags.iter().any(|t| t.as_vec()[0].eq("r")
642 && t.as_vec()[1].eq("5e664e5a7845cd1373c79f580ca4fe29ab5b34d2")))
643 }
644
645 #[tokio::test]
646 async fn git_server() {
647 assert!(create().await.tags.iter().any(
648 |t| t.as_vec()[0].eq("clone") && t.as_vec()[1].eq("https://localhost:1000")
649 ))
650 }
651
652 #[tokio::test]
653 async fn relays() {
654 let event = create().await;
655 let relays_tag: &nostr::Tag = event
656 .tags
657 .iter()
658 .find(|t| t.as_vec()[0].eq("relays"))
659 .unwrap();
660 assert_eq!(relays_tag.as_vec().len(), 3);
661 assert_eq!(relays_tag.as_vec()[1], "ws://relay1.io");
662 assert_eq!(relays_tag.as_vec()[2], "ws://relay2.io");
663 }
664
665 #[tokio::test]
666 async fn web() {
667 let event = create().await;
668 let web_tag: &nostr::Tag =
669 event.tags.iter().find(|t| t.as_vec()[0].eq("web")).unwrap();
670 assert_eq!(web_tag.as_vec().len(), 3);
671 assert_eq!(web_tag.as_vec()[1], "https://exampleproject.xyz");
672 assert_eq!(web_tag.as_vec()[2], "https://gitworkshop.dev/123");
673 }
674
675 #[tokio::test]
676 async fn maintainers() {
677 let event = create().await;
678 let maintainers_tag: &nostr::Tag = event
679 .tags
680 .iter()
681 .find(|t| t.as_vec()[0].eq("maintainers"))
682 .unwrap();
683 assert_eq!(maintainers_tag.as_vec().len(), 3);
684 assert_eq!(
685 maintainers_tag.as_vec()[1],
686 TEST_KEY_1_KEYS.public_key().to_string()
687 );
688 assert_eq!(
689 maintainers_tag.as_vec()[2],
690 TEST_KEY_2_KEYS.public_key().to_string()
691 );
692 }
693
694 #[tokio::test]
695 async fn no_other_tags() {
696 assert_eq!(create().await.tags.len(), 9)
697 }
698 }
699 }
700}