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
|
//! 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,
/// Mode: ci or production
#[arg(short, long, default_value = "ci")]
mode: String,
/// Spec to test (nip01-smoke, nip11, event-acceptance, cors, git-clone, push-auth, repo-creation, 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 } => {
// Early validation: check if --git-data-dir is required for the spec
let specs_requiring_git_dir = ["all", "git-clone", "push-auth", "repo-creation"];
if specs_requiring_git_dir.contains(&spec.as_str()) && git_data_dir.is_none() {
return Err(anyhow!(
"The '{}' spec requires --git-data-dir to be specified.\n\
\n\
This directory should point to the relay's git data storage.\n\
Example: --git-data-dir /path/to/relay/repos\n\
\n\
If using Docker, mount a volume and pass that path:\n\
docker run -v /tmp/repos:/srv/ngit-relay/repos ...\n\
cargo run -- audit --relay ws://localhost:8080 --git-data-dir /tmp/repos",
spec
));
}
let mut config = match mode.as_str() {
"ci" => AuditConfig::ci(),
"production" => AuditConfig::production(),
_ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", 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");
// Helper to check if git_data_dir is required for individual specs
let require_git_data_dir = |spec_name: &str| -> Result<PathBuf> {
git_data_dir.clone().ok_or_else(|| {
anyhow!(
"The '{}' spec requires --git-data-dir to be specified",
spec_name
)
})
};
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" => {
let dir = require_git_data_dir("git-clone")?;
println!("Running Git clone tests...\n");
specs::GitCloneTests::run_all(&client, &dir, &relay_domain).await
}
"push-auth" => {
let dir = require_git_data_dir("push-auth")?;
println!("Running push authorization tests...\n");
specs::PushAuthorizationTests::run_all(&client, &dir, &relay_domain).await
}
"repo-creation" => {
let dir = require_git_data_dir("repo-creation")?;
println!("Running repository creation tests...\n");
specs::RepositoryCreationTests::run_all(&client, &dir).await
}
"all" => {
// git_data_dir is guaranteed by early validation
let dir = git_data_dir.clone().expect("git_data_dir validated earlier");
println!("Running all tests...\n");
let mut all_results = AuditResult::new("All GRASP-01 Tests");
// NIP-01 smoke tests
println!(" → NIP-01 smoke tests...");
let nip01_results = specs::Nip01SmokeTests::run_all(&client).await;
all_results.merge(nip01_results);
// NIP-11 document tests
println!(" → NIP-11 document tests...");
let nip11_results = specs::Nip11DocumentTests::run_all(&client).await;
all_results.merge(nip11_results);
// Event acceptance policy tests
println!(" → Event acceptance policy tests...");
let event_results = specs::EventAcceptancePolicyTests::run_all(&client).await;
all_results.merge(event_results);
// CORS tests
println!(" → CORS tests...");
let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await;
all_results.merge(cors_results);
// Git clone tests
println!(" → Git clone tests...");
let clone_results = specs::GitCloneTests::run_all(&client, &dir, &relay_domain).await;
all_results.merge(clone_results);
// Push authorization tests
println!(" → Push authorization tests...");
let push_results = specs::PushAuthorizationTests::run_all(&client, &dir, &relay_domain).await;
all_results.merge(push_results);
// Repository creation tests
println!(" → Repository creation tests...");
let repo_results = specs::RepositoryCreationTests::run_all(&client, &dir).await;
all_results.merge(repo_results);
println!();
all_results
}
_ => {
return Err(anyhow!(
"Unknown spec: {}. Use 'nip01-smoke', 'nip11', 'event-acceptance', 'cors', 'git-clone', 'push-auth', 'repo-creation', or 'all'",
spec
))
}
};
results.print_report();
if !results.all_passed() {
println!("❌ Some tests failed");
std::process::exit(1);
} else {
println!("✅ All tests passed!");
}
}
}
Ok(())
}
|