upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src
diff options
context:
space:
mode:
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/purgatory.rs652
-rw-r--r--grasp-audit/src/specs/mod.rs2
3 files changed, 655 insertions, 1 deletions
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index 125594c..1694f58 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -19,6 +19,7 @@ pub mod git_clone;
19pub mod git_filter; 19pub mod git_filter;
20pub mod nip01_smoke; 20pub mod nip01_smoke;
21pub mod nip11_document; 21pub mod nip11_document;
22pub mod purgatory;
22pub mod push_authorization; 23pub mod push_authorization;
23pub mod repository_creation; 24pub mod repository_creation;
24pub mod spec_requirements; 25pub mod spec_requirements;
@@ -29,6 +30,7 @@ pub use git_clone::GitCloneTests;
29pub use git_filter::GitFilterTests; 30pub use git_filter::GitFilterTests;
30pub use nip01_smoke::Nip01SmokeTests; 31pub use nip01_smoke::Nip01SmokeTests;
31pub use nip11_document::Nip11DocumentTests; 32pub use nip11_document::Nip11DocumentTests;
33pub use purgatory::PurgatoryTests;
32pub use push_authorization::PushAuthorizationTests; 34pub use push_authorization::PushAuthorizationTests;
33pub use repository_creation::RepositoryCreationTests; 35pub use repository_creation::RepositoryCreationTests;
34pub use spec_requirements::{ 36pub use spec_requirements::{
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs
new file mode 100644
index 0000000..60b6096
--- /dev/null
+++ b/grasp-audit/src/specs/grasp01/purgatory.rs
@@ -0,0 +1,652 @@
1//! GRASP-01 Purgatory Tests
2//!
3//! Tests for the GRASP-01 purgatory mechanism where events are accepted but not
4//! served until corresponding git data arrives.
5//!
6//! ## Purgatory Behavior (GRASP-01 Line 22)
7//!
8//! "New repository announcements, repo state announcements, PRs and PR Updates
9//! SHOULD be accepted with message 'purgatory: won't be served until git data arrives'
10//! and kept in purgatory (not served) until the related git data arrives and otherwise
11//! discarded after 30 minutes."
12//!
13//! ## Test Categories
14//!
15//! ### Announcement Purgatory (feature not yet implemented)
16//! - `test_announcement_not_served_before_git_data`
17//! - `test_announcement_served_after_git_push`
18//! - `test_bare_repo_exists_for_purgatory_announcement`
19//! - `test_state_event_accepted_for_purgatory_announcement`
20//!
21//! ### State Event Purgatory (already implemented)
22//! - `test_state_event_not_served_before_git_data`
23//! - `test_state_event_served_after_git_push`
24//!
25//! ### PR Purgatory (already implemented)
26//! - `test_pr_event_not_served_before_git_data`
27//! - `test_pr_event_served_after_correct_push`
28
29use crate::specs::grasp01::SpecRef;
30use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
31use nostr_sdk::prelude::*;
32use std::time::Duration;
33
34/// Test suite for GRASP-01 purgatory behavior
35pub struct PurgatoryTests;
36
37impl PurgatoryTests {
38 /// Run all purgatory tests
39 pub async fn run_all(client: &AuditClient) -> AuditResult {
40 let mut results = AuditResult::new("GRASP-01 Purgatory Tests");
41
42 // Announcement purgatory tests (feature not yet implemented)
43 results.add(Self::test_announcement_not_served_before_git_data(client).await);
44 results.add(Self::test_announcement_served_after_git_push(client).await);
45 results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await);
46 results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await);
47
48 // State event purgatory tests (already implemented)
49 results.add(Self::test_state_event_not_served_before_git_data(client).await);
50 results.add(Self::test_state_event_served_after_git_push(client).await);
51
52 // PR purgatory tests (feature not yet implemented)
53 results.add(Self::test_pr_event_not_served_before_git_data(client).await);
54 results.add(Self::test_pr_event_served_after_correct_push(client).await);
55
56 results
57 }
58
59 // ============================================================
60 // Announcement Purgatory Tests (#[ignore] - feature not yet implemented)
61 // ============================================================
62
63 /// Test: Repository announcement not served before git data arrives
64 ///
65 /// Spec: GRASP-01 Line 22
66 /// "New repository announcements... SHOULD be accepted with message
67 /// 'purgatory: won't be served until git data arrives' and kept in purgatory
68 /// (not served) until the related git data arrives"
69 ///
70 /// This test verifies:
71 /// 1. Send a valid repository announcement
72 /// 2. Event is accepted (OK response)
73 /// 3. Event is NOT queryable from the relay (in purgatory)
74 ///
75 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
76 pub async fn test_announcement_not_served_before_git_data(client: &AuditClient) -> TestResult {
77 TestResult::new(
78 "announcement_not_served_before_git_data",
79 SpecRef::PurgatoryAcceptUntilGitData,
80 "Repository announcements SHOULD be accepted but not served until git data arrives",
81 )
82 .run(|| async {
83 let ctx = TestContext::new(client);
84
85 // Create a fresh repo announcement (not the served variant)
86 let repo = ctx
87 .get_fixture(FixtureKind::ValidRepoSent)
88 .await
89 .map_err(|e| format!("Failed to create repo announcement: {}", e))?;
90
91 let repo_id = repo
92 .tags
93 .iter()
94 .find(|t| t.kind() == TagKind::d())
95 .and_then(|t| t.content())
96 .ok_or("Missing d tag in repo announcement")?
97 .to_string();
98
99 // Query for the announcement - should NOT be served
100 let filter = Filter::new()
101 .kind(Kind::GitRepoAnnouncement)
102 .author(client.public_key())
103 .identifier(&repo_id);
104
105 tokio::time::sleep(Duration::from_millis(300)).await;
106
107 let events = client
108 .query(filter)
109 .await
110 .map_err(|e| format!("Failed to query relay: {}", e))?;
111
112 if events.iter().any(|e| e.id == repo.id) {
113 return Err(format!(
114 "Announcement was served immediately - purgatory not implemented. \
115 Event ID: {} should NOT be queryable until git data arrives",
116 repo.id
117 ));
118 }
119
120 Ok(())
121 })
122 .await
123 }
124
125 /// Test: Repository announcement served after git push
126 ///
127 /// Spec: GRASP-01 Line 22
128 /// "...kept in purgatory (not served) until the related git data arrives"
129 ///
130 /// This test verifies the full lifecycle:
131 /// 1. Send repository announcement (enters purgatory)
132 /// 2. Send state event (enters purgatory)
133 /// 3. Push git data matching state event
134 /// 4. Both announcement and state event are now served
135 ///
136 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
137 pub async fn test_announcement_served_after_git_push(client: &AuditClient) -> TestResult {
138 TestResult::new(
139 "announcement_served_after_git_push",
140 SpecRef::PurgatoryAcceptUntilGitData,
141 "Repository announcements SHOULD be served after git data arrives",
142 )
143 .run(|| async {
144 let ctx = TestContext::new(client);
145
146 // OwnerStateDataPushed fixture handles the full lifecycle:
147 // 1. Creates repo announcement (purgatory)
148 // 2. Creates state event (purgatory)
149 // 3. Pushes git data
150 // 4. Verifies events are served
151 let state_event = ctx
152 .get_fixture(FixtureKind::OwnerStateDataPushed)
153 .await
154 .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?;
155
156 // Extract repo_id from state event
157 let repo_id = state_event
158 .tags
159 .iter()
160 .find(|t| t.kind() == TagKind::d())
161 .and_then(|t| t.content())
162 .ok_or("Missing d tag in state event")?
163 .to_string();
164
165 // Verify announcement is now served
166 let announcement_filter = Filter::new()
167 .kind(Kind::GitRepoAnnouncement)
168 .author(client.public_key())
169 .identifier(&repo_id);
170
171 let announcements = client
172 .query(announcement_filter)
173 .await
174 .map_err(|e| format!("Failed to query announcements: {}", e))?;
175
176 if announcements.is_empty() {
177 return Err(format!(
178 "Announcement not served after git push. Repo ID: {}",
179 repo_id
180 ));
181 }
182
183 // Verify state event is served
184 let state_filter = Filter::new()
185 .kind(Kind::RepoState)
186 .author(client.public_key())
187 .identifier(&repo_id);
188
189 let state_events = client
190 .query(state_filter)
191 .await
192 .map_err(|e| format!("Failed to query state events: {}", e))?;
193
194 if !state_events.iter().any(|e| e.id == state_event.id) {
195 return Err(format!(
196 "State event not served after git push. Event ID: {}",
197 state_event.id
198 ));
199 }
200
201 Ok(())
202 })
203 .await
204 }
205
206 /// Test: Bare repository exists for purgatory announcement
207 ///
208 /// Spec: GRASP-01 Line 34
209 /// "MUST serve a git repository via an unauthenticated git smart http service
210 /// at `/<npub>/<identifier>.git` for each git repository announcement the relay
211 /// serves or has in purgatory."
212 ///
213 /// This test verifies that git HTTP service works even for repos in purgatory.
214 ///
215 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
216 pub async fn test_bare_repo_exists_for_purgatory_announcement(
217 client: &AuditClient,
218 ) -> TestResult {
219 TestResult::new(
220 "bare_repo_exists_for_purgatory_announcement",
221 SpecRef::GitServeRepository,
222 "Git HTTP service MUST work for repos in purgatory",
223 )
224 .run(|| async {
225 let ctx = TestContext::new(client);
226
227 // Get a repo announcement (in purgatory, no git data yet)
228 let repo = ctx
229 .get_fixture(FixtureKind::ValidRepoSent)
230 .await
231 .map_err(|e| format!("Failed to create repo announcement: {}", e))?;
232
233 let repo_id = repo
234 .tags
235 .iter()
236 .find(|t| t.kind() == TagKind::d())
237 .and_then(|t| t.content())
238 .ok_or("Missing d tag in repo announcement")?
239 .to_string();
240
241 let npub = client
242 .public_key()
243 .to_bech32()
244 .map_err(|e| format!("Failed to convert pubkey: {}", e))?;
245
246 // Get relay domain
247 let relay_url = client
248 .client()
249 .relays()
250 .await
251 .keys()
252 .next()
253 .ok_or("No relay connected")?
254 .to_string();
255 let relay_domain = relay_url
256 .replace("ws://", "")
257 .replace("wss://", "")
258 .replace(":8080", "");
259
260 // Check git HTTP service is available
261 let info_refs_url = format!(
262 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
263 relay_domain, npub, repo_id
264 );
265
266 let http_client = reqwest::Client::new();
267 let response = http_client
268 .get(&info_refs_url)
269 .send()
270 .await
271 .map_err(|e| format!("HTTP request failed: {}", e))?;
272
273 if !response.status().is_success() {
274 return Err(format!(
275 "Git HTTP service not available for purgatory repo. \
276 URL: {}, Status: {}",
277 info_refs_url,
278 response.status()
279 ));
280 }
281
282 Ok(())
283 })
284 .await
285 }
286
287 /// Test: State event accepted for purgatory announcement
288 ///
289 /// Spec: GRASP-01 Line 22
290 /// "New repository announcements, repo state announcements... SHOULD be accepted"
291 ///
292 /// This test verifies that state events are accepted even when the repo
293 /// announcement is in purgatory (no git data yet).
294 ///
295 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
296 pub async fn test_state_event_accepted_for_purgatory_announcement(
297 client: &AuditClient,
298 ) -> TestResult {
299 TestResult::new(
300 "state_event_accepted_for_purgatory_announcement",
301 SpecRef::PurgatoryAcceptUntilGitData,
302 "State events SHOULD be accepted for repos in purgatory",
303 )
304 .run(|| async {
305 let ctx = TestContext::new(client);
306
307 // Get a repo announcement (in purgatory)
308 let repo = ctx
309 .get_fixture(FixtureKind::ValidRepoSent)
310 .await
311 .map_err(|e| format!("Failed to create repo announcement: {}", e))?;
312
313 // Build a state event for this repo
314 let repo_id = repo
315 .tags
316 .iter()
317 .find(|t| t.kind() == TagKind::d())
318 .and_then(|t| t.content())
319 .ok_or("Missing d tag in repo announcement")?
320 .to_string();
321
322 let state_event = client
323 .event_builder(Kind::RepoState, "")
324 .tag(Tag::identifier(&repo_id))
325 .tag(Tag::custom(
326 TagKind::custom("refs/heads/main"),
327 vec!["abc123".to_string()],
328 ))
329 .tag(Tag::custom(
330 TagKind::custom("HEAD"),
331 vec!["ref: refs/heads/main".to_string()],
332 ))
333 .build(client.keys())
334 .map_err(|e| format!("Failed to build state event: {}", e))?;
335
336 // Send state event - should be accepted (even though repo is in purgatory)
337 let (_, in_purgatory) = client
338 .send_event_and_note_purgatory(state_event.clone())
339 .await
340 .map_err(|e| format!("Failed to send state event: {}", e))?;
341
342 // Event should be accepted (either in purgatory or served)
343 // We just verify it wasn't rejected
344 if !in_purgatory {
345 // Check if it's actually on the relay (might be served immediately)
346 let filter = Filter::new()
347 .kind(Kind::RepoState)
348 .author(client.public_key())
349 .identifier(&repo_id);
350
351 let events = client
352 .query(filter)
353 .await
354 .map_err(|e| format!("Failed to query: {}", e))?;
355
356 if events.iter().any(|e| e.id == state_event.id) {
357 return Err(format!(
358 "State event was served immediately - repo announcement purgatory not implemented. \
359 Event ID: {} should NOT be queryable until git data arrives",
360 state_event.id
361 ));
362 }
363
364 return Err(format!(
365 "State event was neither in purgatory nor served. \
366 Event ID: {}",
367 state_event.id
368 ));
369 }
370
371 // Feature IS implemented - state event in purgatory as expected
372 Ok(())
373 })
374 .await
375 }
376
377 // ============================================================
378 // State Event Purgatory Tests (non-ignored - already implemented)
379 // ============================================================
380
381 /// Test: State event not served before git data arrives
382 ///
383 /// Spec: GRASP-01 Line 22
384 /// "repo state announcements... SHOULD be accepted with message
385 /// 'purgatory: won't be served until git data arrives'"
386 ///
387 /// This test verifies:
388 /// 1. Send state event for a repo with git data
389 /// 2. State event points to a different commit than what's pushed
390 /// 3. State event is NOT queryable (in purgatory)
391 pub async fn test_state_event_not_served_before_git_data(client: &AuditClient) -> TestResult {
392 TestResult::new(
393 "state_event_not_served_before_git_data",
394 SpecRef::PurgatoryAcceptUntilGitData,
395 "State events SHOULD be accepted but not served until git data arrives",
396 )
397 .run(|| async {
398 let ctx = TestContext::new(client);
399
400 // Get a repo with git data already pushed
401 let existing_state = ctx
402 .get_fixture(FixtureKind::OwnerStateDataPushed)
403 .await
404 .map_err(|e| format!("Failed to get existing repo: {}", e))?;
405
406 let repo_id = existing_state
407 .tags
408 .iter()
409 .find(|t| t.kind() == TagKind::d())
410 .and_then(|t| t.content())
411 .ok_or("Missing d tag in state event")?
412 .to_string();
413
414 // Create a NEW state event pointing to a DIFFERENT commit
415 // This should enter purgatory since the commit doesn't exist
416 let new_state = client
417 .event_builder(Kind::RepoState, "")
418 .tag(Tag::identifier(&repo_id))
419 .tag(Tag::custom(
420 TagKind::custom("refs/heads/main"),
421 vec!["deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string()],
422 ))
423 .tag(Tag::custom(
424 TagKind::custom("HEAD"),
425 vec!["ref: refs/heads/main".to_string()],
426 ))
427 .build(client.keys())
428 .map_err(|e| format!("Failed to build state event: {}", e))?;
429
430 // Send the state event
431 let (_, in_purgatory) = client
432 .send_event_and_note_purgatory(new_state.clone())
433 .await
434 .map_err(|e| format!("Failed to send state event: {}", e))?;
435
436 if !in_purgatory {
437 return Err(format!(
438 "State event was served immediately despite pointing to \
439 non-existent commit. Event ID: {}",
440 new_state.id
441 ));
442 }
443
444 Ok(())
445 })
446 .await
447 }
448
449 /// Test: State event served after git push
450 ///
451 /// Spec: GRASP-01 Line 22
452 /// "...kept in purgatory (not served) until the related git data arrives"
453 ///
454 /// This test verifies the full lifecycle using OwnerStateDataPushed fixture:
455 /// 1. State event is sent (enters purgatory)
456 /// 2. Git data is pushed matching the state event
457 /// 3. State event is now served
458 pub async fn test_state_event_served_after_git_push(client: &AuditClient) -> TestResult {
459 TestResult::new(
460 "state_event_served_after_git_push",
461 SpecRef::PurgatoryAcceptUntilGitData,
462 "State events SHOULD be served after matching git data arrives",
463 )
464 .run(|| async {
465 let ctx = TestContext::new(client);
466
467 // OwnerStateDataPushed handles the full lifecycle
468 let state_event = ctx
469 .get_fixture(FixtureKind::OwnerStateDataPushed)
470 .await
471 .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?;
472
473 // Verify state event is now served
474 let repo_id = state_event
475 .tags
476 .iter()
477 .find(|t| t.kind() == TagKind::d())
478 .and_then(|t| t.content())
479 .ok_or("Missing d tag in state event")?
480 .to_string();
481
482 let filter = Filter::new()
483 .kind(Kind::RepoState)
484 .author(client.public_key())
485 .identifier(&repo_id);
486
487 let events = client
488 .query(filter)
489 .await
490 .map_err(|e| format!("Failed to query state events: {}", e))?;
491
492 if !events.iter().any(|e| e.id == state_event.id) {
493 return Err(format!(
494 "State event not served after git push. Event ID: {}",
495 state_event.id
496 ));
497 }
498
499 Ok(())
500 })
501 .await
502 }
503
504 // ============================================================
505 // PR Purgatory Tests
506 // ============================================================
507
508 /// Test: PR event not served before git data arrives
509 ///
510 /// Spec: GRASP-01 Line 22
511 /// "PRs and PR Updates SHOULD be accepted with message
512 /// 'purgatory: won't be served until git data arrives'"
513 ///
514 /// This test verifies:
515 /// 1. Send PR event for a repo
516 /// 2. PR event is NOT queryable (in purgatory)
517 /// 3. No git data exists at refs/nostr/<pr-event-id>
518 pub async fn test_pr_event_not_served_before_git_data(client: &AuditClient) -> TestResult {
519 TestResult::new(
520 "pr_event_not_served_before_git_data",
521 SpecRef::PurgatoryAcceptUntilGitData,
522 "PR events SHOULD be accepted but not served until git data arrives",
523 )
524 .run(|| async {
525 let ctx = TestContext::new(client);
526
527 // Get a repo announcement
528 let _repo = ctx
529 .get_fixture(FixtureKind::ValidRepoSent)
530 .await
531 .map_err(|e| format!("Failed to create repo: {}", e))?;
532
533 // Build PR event (not sent yet)
534 let pr_event = ctx
535 .build_fixture_only(FixtureKind::PREvent)
536 .await
537 .map_err(|e| format!("Failed to build PR event: {}", e))?;
538
539 // Send PR event
540 let (_, in_purgatory) = client
541 .send_event_and_note_purgatory(pr_event.clone())
542 .await
543 .map_err(|e| format!("Failed to send PR event: {}", e))?;
544
545 if !in_purgatory {
546 return Err(format!(
547 "PR event was served immediately - purgatory not implemented. \
548 Event ID: {} should NOT be queryable until git data arrives",
549 pr_event.id
550 ));
551 }
552
553 Ok(())
554 })
555 .await
556 }
557
558 /// Test: PR event served after correct push
559 ///
560 /// Spec: GRASP-01 Line 22
561 /// "...kept in purgatory (not served) until the related git data arrives"
562 ///
563 /// This test verifies:
564 /// 1. Send PR event (enters purgatory)
565 /// 2. Push git data to refs/nostr/<pr-event-id> with correct commit
566 /// 3. PR event is now served
567 pub async fn test_pr_event_served_after_correct_push(client: &AuditClient) -> TestResult {
568 TestResult::new(
569 "pr_event_served_after_correct_push",
570 SpecRef::PurgatoryAcceptUntilGitData,
571 "PR events SHOULD be served after matching git data arrives",
572 )
573 .run(|| async {
574 let ctx = TestContext::new(client);
575
576 // Get a repo with git data
577 let _existing_state = ctx
578 .get_fixture(FixtureKind::OwnerStateDataPushed)
579 .await
580 .map_err(|e| format!("Failed to get existing repo: {}", e))?;
581
582 // Build PR event
583 let pr_event = ctx
584 .build_fixture_only(FixtureKind::PREvent)
585 .await
586 .map_err(|e| format!("Failed to build PR event: {}", e))?;
587
588 // Send PR event (should enter purgatory)
589 let (_, _in_purgatory) = client
590 .send_event_and_note_purgatory(pr_event.clone())
591 .await
592 .map_err(|e| format!("Failed to send PR event: {}", e))?;
593
594 // TODO: Push git data to refs/nostr/<pr-event-id>
595 // This requires git operations similar to OwnerStateDataPushed
596
597 // For now, verify the PR event exists
598 let filter = Filter::new()
599 .kind(Kind::GitPullRequest)
600 .author(client.pr_author_keys().public_key())
601 .id(pr_event.id);
602
603 let events = client
604 .query(filter)
605 .await
606 .map_err(|e| format!("Failed to query PR events: {}", e))?;
607
608 if events.is_empty() {
609 return Err(format!(
610 "PR event not served after git push - purgatory release not implemented. \
611 Event ID: {} should be queryable after git data arrives",
612 pr_event.id
613 ));
614 }
615
616 Ok(())
617 })
618 .await
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use crate::AuditConfig;
626
627 #[tokio::test]
628 #[ignore] // Requires running relay
629 async fn test_grasp01_purgatory_against_relay() {
630 let relay_url = std::env::var("RELAY_URL").expect(
631 "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081",
632 );
633
634 let config = AuditConfig::isolated();
635 let client = AuditClient::new(&relay_url, config)
636 .await
637 .unwrap_or_else(|_| {
638 panic!(
639 "Failed to connect to relay at {}. Ensure relay is running and accessible.",
640 relay_url
641 )
642 });
643
644 let results = PurgatoryTests::run_all(&client).await;
645 results.print_report();
646
647 assert!(
648 results.all_passed(),
649 "Some purgatory tests failed. See report above."
650 );
651 }
652}
diff --git a/grasp-audit/src/specs/mod.rs b/grasp-audit/src/specs/mod.rs
index bf711fa..ceae684 100644
--- a/grasp-audit/src/specs/mod.rs
+++ b/grasp-audit/src/specs/mod.rs
@@ -7,5 +7,5 @@ pub mod grasp01;
7// Re-export all test structs from grasp01 module 7// Re-export all test structs from grasp01 module
8pub use grasp01::{ 8pub use grasp01::{
9 CorsTests, EventAcceptancePolicyTests, GitCloneTests, GitFilterTests, Nip01SmokeTests, 9 CorsTests, EventAcceptancePolicyTests, GitCloneTests, GitFilterTests, Nip01SmokeTests,
10 Nip11DocumentTests, PushAuthorizationTests, RepositoryCreationTests, 10 Nip11DocumentTests, PurgatoryTests, PushAuthorizationTests, RepositoryCreationTests,
11}; 11};