upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/grasp01/repository_creation.rs
blob: 2eddb97265aa3d29e717f9570905946174fd9bc0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
//! GRASP-01 Repository Creation Tests
//!
//! Tests that verify bare Git repositories are created when repository announcements
//! are accepted by the relay.
//!
//! ## Test Coverage
//!
//! - Repository creation on valid announcement
//! - Repository accessibility via Smart HTTP service (git-upload-pack)
//! - URL format: http://domain/npub/identifier.git
//!
//! ## Running Tests
//!
//! ```bash
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
//! ```

use crate::{AuditClient, FixtureKind, TestContext, TestResult};
use nostr_sdk::prelude::*;

/// Test suite for repository creation
pub struct RepositoryCreationTests;

impl RepositoryCreationTests {
    /// Run all repository creation tests
    pub async fn run_all(client: &AuditClient, relay_domain: &str) -> crate::AuditResult {
        let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests");

        results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await);
        results.add(Self::test_webpage_served_for_existing_repo(client, relay_domain).await);
        results.add(Self::test_404_for_nonexistent_repo(client, relay_domain).await);

        results
    }

    /// Test that a bare repository is created when a valid announcement is accepted
    /// and is accessible via Smart HTTP service
    ///
    /// Spec: Line 28 of ../grasp/01.md
    /// This test verifies:
    /// 1. Sends a valid repository announcement via TestContext
    /// 2. Verifies the announcement was accepted
    /// 3. Repository responds to git-upload-pack service discovery
    /// 4. URL format follows http://domain/npub/identifier.git pattern
    pub async fn test_bare_repo_created_on_announcement(
        client: &AuditClient,
        relay_domain: &str,
    ) -> TestResult {
        let test_name = "test_bare_repo_created_on_announcement";
        let ctx = TestContext::new(client);

        // Use TestContext to create and send repository announcement
        let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
            Ok(r) => r,
            Err(e) => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:34",
                    "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
                )
                .fail(format!("Failed to create repo fixture: {}", e))
            }
        };

        // Wait a bit for repository creation
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;

        // Extract repo identifier and npub from announcement
        let repo_id = match repo
            .tags
            .iter()
            .find(|t| t.kind() == TagKind::d())
            .and_then(|t| t.content())
        {
            Some(id) => id.to_string(),
            None => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:34",
                    "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
                )
                .fail("Repository announcement missing d tag")
            }
        };

        let npub = match repo.pubkey.to_bech32() {
            Ok(n) => n,
            Err(e) => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:34",
                    "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
                )
                .fail(format!("Failed to convert pubkey to npub: {}", e))
            }
        };

        // Verify repository exists and is accessible via HTTP (info/refs endpoint)
        if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await {
            return TestResult::new(
                test_name,
                "GRASP-01:git-http:34",
                "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
            )
            .fail(format!("Repository not accessible via HTTP: {}", e));
        }

        TestResult::new(
            test_name,
            "GRASP-01:git-http:34",
            "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
        )
        .pass()
    }

    /// Test that a webpage is served for an existing repository
    ///
    /// Spec: Line 38 of ../grasp/01.md
    /// This test verifies:
    /// 1. Creates a valid repository announcement
    /// 2. Accesses the repository URL without git service parameters
    /// 3. Verifies a webpage is returned (any 2xx status with HTML content)
    ///
    /// GRASP-01: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s)"
    pub async fn test_webpage_served_for_existing_repo(
        client: &AuditClient,
        relay_domain: &str,
    ) -> TestResult {
        let test_name = "test_webpage_served_for_existing_repo";
        let ctx = TestContext::new(client);

        // Create a repository announcement
        let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
            Ok(r) => r,
            Err(e) => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:44",
                    "Relay SHOULD serve a webpage for existing repositories",
                )
                .fail(format!("Failed to create repo fixture: {}", e))
            }
        };

        // Wait for repository creation
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;

        // Extract repo identifier and npub
        let repo_id = match repo
            .tags
            .iter()
            .find(|t| t.kind() == TagKind::d())
            .and_then(|t| t.content())
        {
            Some(id) => id.to_string(),
            None => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:44",
                    "Relay SHOULD serve a webpage for existing repositories",
                )
                .fail("Repository announcement missing d tag")
            }
        };

        let npub = match repo.pubkey.to_bech32() {
            Ok(n) => n,
            Err(e) => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:44",
                    "Relay SHOULD serve a webpage for existing repositories",
                )
                .fail(format!("Failed to convert pubkey to npub: {}", e))
            }
        };

        // Check that a webpage is served at the repository URL
        if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await {
            return TestResult::new(
                test_name,
                "GRASP-01:git-http:44",
                "Relay SHOULD serve a webpage for existing repositories",
            )
            .fail(format!("Webpage not served: {}", e));
        }

        TestResult::new(
            test_name,
            "GRASP-01:git-http:44",
            "Relay SHOULD serve a webpage for existing repositories",
        )
        .pass()
    }

    /// Test that 404 is returned for non-existent repositories
    ///
    /// Spec: Line 38 of ../grasp/01.md
    /// This test verifies:
    /// 1. Accesses a URL for a repository that doesn't exist
    /// 2. Verifies a 404 status is returned
    ///
    /// GRASP-01: "...and a 404 page for repositories it doesn't host"
    pub async fn test_404_for_nonexistent_repo(
        client: &AuditClient,
        relay_domain: &str,
    ) -> TestResult {
        let test_name = "test_404_for_nonexistent_repo";

        let ctx = TestContext::new(client);

        let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
            Ok(r) => r,
            Err(e) => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:44",
                    "Relay SHOULD return 404 for repositories it doesn't host",
                )
                .fail(format!("Failed to create repo fixture: {}", e))
            }
        };

        let npub = match repo.pubkey.to_bech32() {
            Ok(n) => n,
            Err(e) => {
                return TestResult::new(
                    test_name,
                    "GRASP-01:git-http:44",
                    "Relay SHOULD return 404 for repositories it doesn't host",
                )
                .fail(format!("Failed to convert pubkey to npub: {}", e))
            }
        };
        // Use a clearly non-existent repo id but real npub
        let fake_repo_id = "nonexistent-repo-12345";

        // Check that 404 is returned
        if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await {
            return TestResult::new(
                test_name,
                "GRASP-01:git-http:44",
                "Relay SHOULD return 404 for repositories it doesn't host",
            )
            .fail(format!("Expected 404, got: {}", e));
        }

        TestResult::new(
            test_name,
            "GRASP-01:git-http:44",
            "Relay SHOULD return 404 for repositories it doesn't host",
        )
        .pass()
    }
}

/// Helper function to check if a repository is accessible via Smart HTTP service
///
/// Verifies that the repository responds correctly to git-upload-pack service discovery
/// at the URL: http://domain/npub/identifier.git/info/refs?service=git-upload-pack
async fn check_repo_accessible_via_http(
    relay_domain: &str,
    npub: &str,
    repo_id: &str,
) -> Result<(), String> {
    let info_refs_url = format!(
        "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
        relay_domain, npub, repo_id
    );

    let http_client = reqwest::Client::new();
    let response = http_client
        .get(&info_refs_url)
        .send()
        .await
        .map_err(|e| format!("HTTP request failed: {}", e))?;

    if !response.status().is_success() {
        return Err(format!(
            "info/refs returned status {} for URL: {}",
            response.status(),
            info_refs_url
        ));
    }

    // Verify Content-Type indicates git-upload-pack service
    let content_type = response
        .headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");

    if !content_type.contains("application/x-git-upload-pack-advertisement") {
        return Err(format!(
            "Expected Content-Type: application/x-git-upload-pack-advertisement, got: {}",
            content_type
        ));
    }

    Ok(())
}

/// Helper function to check if a webpage is served for an existing repository
///
/// Verifies that accessing the repository URL returns a webpage (2xx status)
/// URL format: http://domain/npub/identifier.git
async fn check_webpage_served(relay_domain: &str, npub: &str, repo_id: &str) -> Result<(), String> {
    let repo_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);

    let http_client = reqwest::Client::new();
    let response = http_client
        .get(&repo_url)
        .send()
        .await
        .map_err(|e| format!("HTTP request failed: {}", e))?;

    if !response.status().is_success() {
        return Err(format!(
            "Expected 2xx status for existing repo webpage, got {} for URL: {}",
            response.status(),
            repo_url
        ));
    }

    Ok(())
}

/// Helper function to check that 404 is returned for non-existent repository
///
/// Verifies that accessing a non-existent repository URL returns 404
async fn check_404_for_nonexistent_repo(
    relay_domain: &str,
    npub: &str,
    repo_id: &str,
) -> Result<(), String> {
    let repo_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);

    let http_client = reqwest::Client::new();
    let response = http_client
        .get(&repo_url)
        .send()
        .await
        .map_err(|e| format!("HTTP request failed: {}", e))?;

    if response.status().as_u16() != 404 {
        return Err(format!(
            "Expected 404 status for non-existent repo, got {} for URL: {}",
            response.status(),
            repo_url
        ));
    }

    Ok(())
}