upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/bin/grasp-audit.rs
blob: d192f0425d7fda793ac1f7336b275e8eb899655e (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
//! GRASP Audit CLI Tool

use clap::{Parser, Subcommand};
use grasp_audit::*;
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "grasp-audit")]
#[command(about = "GRASP audit and compliance testing tool", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Run audit tests against a server
    Audit {
        /// Relay URL (e.g., ws://localhost:7000)
        #[arg(short, long)]
        relay: String,

        /// Fixture mode: shared (default) or isolated
        ///
        /// - shared: Fixtures are cached and reused across tests (efficient for sequential test runs)
        /// - isolated: Each test creates fresh fixtures (for parallel tests like cargo test)
        #[arg(short, long, default_value = "shared")]
        mode: String,

        /// Spec to test (nip01-smoke, nip11, event-acceptance, cors, git-clone, git-filter, push-auth, repo-creation, purgatory, all)
        #[arg(short, long, default_value = "all")]
        spec: String,

        /// Git data directory (required for cors, git-clone, push-auth, repo-creation specs)
        #[arg(short, long)]
        git_data_dir: Option<PathBuf>,
    },
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive(tracing::Level::INFO.into()),
        )
        .init();

    let cli = Cli::parse();

    match cli.command {
        Commands::Audit {
            relay,
            mode,
            spec,
            git_data_dir,
        } => {
            let mut config = match mode.as_str() {
                "shared" => AuditConfig::shared(),
                "isolated" => AuditConfig::isolated(),
                // Backwards compatibility aliases
                "ci" => AuditConfig::isolated(),
                "production" => AuditConfig::shared(),
                _ => {
                    return Err(anyhow!(
                        "Invalid mode: {}. Use 'shared' or 'isolated'",
                        mode
                    ))
                }
            };

            // Audit needs to create events to test the relay, so disable read-only mode
            config.read_only = false;

            // Derive relay_domain from relay URL (e.g., "ws://localhost:8081" -> "localhost:8081")
            let relay_domain = relay
                .replace("ws://", "")
                .replace("wss://", "")
                .trim_end_matches('/')
                .to_string();

            println!("🔍 GRASP Audit Tool");
            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            println!("Relay:   {}", relay);
            println!("Mode:    {}", mode);
            println!("Spec:    {}", spec);
            println!("Run ID:  {}", config.run_id);
            if let Some(ref dir) = git_data_dir {
                println!("Git Dir: {}", dir.display());
            }
            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            println!();

            println!("Connecting to relay...");
            let client = AuditClient::new(&relay, config)
                .await
                .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;

            if !client.is_connected().await {
                return Err(anyhow!("Could not establish connection to relay"));
            }

            println!("✓ Connected\n");

            let results = match spec.as_str() {
                "nip01-smoke" => {
                    println!("Running NIP-01 smoke tests...\n");
                    specs::Nip01SmokeTests::run_all(&client).await
                }
                "nip11" => {
                    println!("Running NIP-11 document tests...\n");
                    specs::Nip11DocumentTests::run_all(&client).await
                }
                "event-acceptance" => {
                    println!("Running event acceptance policy tests...\n");
                    specs::EventAcceptancePolicyTests::run_all(&client).await
                }
                "cors" => {
                    println!("Running CORS tests...\n");
                    specs::CorsTests::run_all(&client, &relay_domain).await
                }
                "git-clone" => {
                    println!("Running Git clone tests...\n");
                    specs::GitCloneTests::run_all(&client, &relay_domain).await
                }
                "git-filter" => {
                    println!("Running Git filter capability tests...\n");
                    specs::GitFilterTests::run_all(&client, &relay_domain).await
                }
                "push-auth" => {
                    println!("Running push authorization tests...\n");
                    specs::PushAuthorizationTests::run_all(&client, &relay_domain).await
                }
                "repo-creation" => {
                    println!("Running repository creation tests...\n");
                    specs::RepositoryCreationTests::run_all(&client, &relay_domain).await
                }
                "purgatory" => {
                    println!("Running purgatory tests...\n");
                    specs::PurgatoryTests::run_all(&client).await
                }
                "all" => {
                    println!("Running all tests...\n");
                    let mut all_results = AuditResult::new("All GRASP-01 Tests");

                    // NIP-01 smoke tests (stateless - no shared fixture dependencies)
                    println!("  → NIP-01 smoke tests...");
                    let nip01_results = specs::Nip01SmokeTests::run_all(&client).await;
                    all_results.merge(nip01_results);

                    // NIP-11 document tests (stateless)
                    println!("  → NIP-11 document tests...");
                    let nip11_results = specs::Nip11DocumentTests::run_all(&client).await;
                    all_results.merge(nip11_results);

                    // CORS tests (stateless HTTP checks)
                    println!("  → CORS tests...");
                    let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await;
                    all_results.merge(cors_results);

                    // Repository creation tests (uses ValidRepoSent only - no state events)
                    println!("  → Repository creation tests...");
                    let repo_results = specs::RepositoryCreationTests::run_all(&client, &relay_domain).await;
                    all_results.merge(repo_results);

                    // Git clone tests (uses ValidRepoSent only - no state events)
                    println!("  → Git clone tests...");
                    let clone_results = specs::GitCloneTests::run_all(&client, &relay_domain).await;
                    all_results.merge(clone_results);

                    // Git filter capability tests (uses ValidRepoSent only - no state events)
                    println!("  → Git filter capability tests...");
                    let filter_results = specs::GitFilterTests::run_all(&client, &relay_domain).await;
                    all_results.merge(filter_results);

                    // Event acceptance policy tests (uses ValidRepoServed - no extra state events)
                    println!("  → Event acceptance policy tests...");
                    let event_results = specs::EventAcceptancePolicyTests::run_all(&client).await;
                    all_results.merge(event_results);

                    // Purgatory tests MUST run before push-auth.
                    // Push-auth sends new replaceable state events (kind 30618) for the same
                    // repo_id as OwnerStateDataPushed (e.g. test_head_set_after_git_push_with_required_oids
                    // sends a develop1 state event that displaces the original). If purgatory ran
                    // after push-auth, is_event_on_relay(original_id) would return false because
                    // the original state event has been replaced on the relay.
                    println!("  → Purgatory tests...");
                    let purgatory_results = specs::PurgatoryTests::run_all(&client).await;
                    all_results.merge(purgatory_results);

                    // Push authorization tests (mutates shared state - must run last among git specs)
                    println!("  → Push authorization tests...");
                    let push_results = specs::PushAuthorizationTests::run_all(&client, &relay_domain).await;
                    all_results.merge(push_results);

                    println!();
                    all_results
                }
                _ => {
                    return Err(anyhow!(
                        "Unknown spec: {}. Use 'nip01-smoke', 'nip11', 'event-acceptance', 'cors', 'git-clone', 'git-filter', 'push-auth', 'repo-creation', 'purgatory', or 'all'",
                        spec
                    ))
                }
            };

            results.print_report();

            if !results.all_passed() {
                println!("❌ Some tests failed");
                std::process::exit(1);
            } else {
                println!("✅ All tests passed!");
            }
        }
    }

    Ok(())
}