14530604 Andy McCall 1 month ago ↶
1
2
3
4
5
6
use axum:: body:: Body;
7
use axum:: http:: { header, Request, StatusCode} ;
8
use http_body_util:: BodyExt;
9
use oxy_core:: repo:: { IdentityScope, Repo} ;
10
use oxygenhub:: config:: Config;
11
use oxygenhub:: db;
12
use oxygenhub:: repos:: RepoName;
13
use oxygenhub:: Shared;
14
use std:: path:: Path;
15
use tower:: ServiceExt;
16
17
fn hermetic ( ) {
18
static ONCE : std:: sync:: Once = std:: sync:: Once:: new( ) ;
19
static DIR : std:: sync:: OnceLock< tempfile:: TempDir> = std:: sync:: OnceLock:: new( ) ;
20
ONCE .call_once ( | | {
21
let d = tempfile:: tempdir( ) .unwrap ( ) ;
22
std:: env:: set_var( " OXY_HOME" , d.path ( ) ) ;
23
DIR .set ( d) .ok ( ) ;
24
} ) ;
25
}
26
27
fn write ( root : & Path, rel : & str , content : & [u8 ]) {
28
let p = root.join ( rel) ;
29
std:: fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) .unwrap ( ) ;
30
std:: fs:: write( p, content) .unwrap ( ) ;
31
}
32
33
struct Res {
34
status : StatusCode,
35
body : String,
36
cookie : Option< String> ,
37
location : Option< String> ,
38
}
39
40
async fn send ( app : & Shared, method : & str , path : & str , cookie : Option< & str > , form : Option< & str > ) -> Res {
41
let mut req = Request:: builder( ) .method ( method) .uri ( path) ;
42
if let Some ( c) = cookie {
43
req = req.header ( header:: COOKIE , format! ( " oxyhub={c} " ) ) ;
44
}
45
let body = match form {
46
Some ( f) => {
47
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
48
Body:: from( f.to_string ( ) )
49
}
50
None => Body:: empty( ) ,
51
} ;
52
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req.body ( body) .unwrap ( ) ) .await.unwrap ( ) ;
53
let status = res.status ( ) ;
54
let cookie = res
55
.headers ( )
56
.get ( header:: SET_COOKIE )
57
.and_then ( | v | v.to_str ( ) .ok ( ) )
58
.and_then ( | v | v.split ( ' ;' ) .next ( ) )
59
.and_then ( | kv | kv.split_once ( ' =' ) )
60
.map ( | ( _, v ) | v.to_string ( ) ) ;
61
let location = res.headers ( ) .get ( header:: LOCATION ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .map ( String :: from) ;
62
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
63
Res { status, body: String :: from_utf8_lossy( & bytes) .into_owned ( ) , cookie, location }
64
}
65
5e6d6484 Andy McCall 1 month ago ↶
66
67
async fn next_event ( body : & mut Body, kind : & str ) -> String {
68
let mut buf = String :: new( ) ;
69
loop {
70
let frame = tokio:: time:: timeout( std:: time:: Duration:: from_secs( 5 ) , body.frame ( ) )
71
.await
72
.unwrap_or_else ( | _| panic! ( " no {kind} event; got {buf}" ) )
73
.expect ( " the stream ended" )
74
.unwrap ( ) ;
75
if let Ok ( data) = frame.into_data ( ) {
76
buf.push_str ( & String :: from_utf8_lossy( & data) ) ;
77
}
78
while let Some ( end) = buf.find ( " \n \n " ) {
79
let event: String = buf.drain ( .. end + 2 ) .collect ( ) ;
80
if event.contains ( & format! ( " event: {kind} \n " ) ) {
81
return event;
82
}
83
}
84
}
85
}
86
14530604 Andy McCall 1 month ago ↶
87
fn csrf_of ( page : & str ) -> String {
88
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
89
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
90
}
91
92
fn enc ( s : & str ) -> String {
93
let mut out = String :: new( ) ;
94
for b in s.bytes ( ) {
95
match b {
96
b ' A' .. = b ' Z' | b ' a' .. = b ' z' | b ' 0' .. = b ' 9' | b ' -' | b ' _' | b ' .' | b ' ~' => out.push ( b as char ) ,
97
_ => out.push_str ( & format! ( " %{b:02X} " ) ) ,
98
}
99
}
100
out
101
}
102
103
# [ tokio ::test ( flavor = " multi_thread" ) ]
104
async fn web_edits_under_a_policy ( ) {
105
hermetic ( ) ;
106
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
107
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
c8684d44 Andy McCall 21 days ago
108
109
config.registration.setup_code = " test-setup" .into ( ) ;
14530604 Andy McCall 1 month ago ↶
110
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
111
config.registration.mode = " open" .into ( ) ;
112
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
113
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
114
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
115
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
116
let router = oxygenhub:: router( app.clone ( ) ) ;
117
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
118
let base = format! ( " http://127.0.0.1:{port} " ) ;
119
let app = & app;
120
c8684d44 Andy McCall 21 days ago
121
let r =
122
send ( app, " POST" , " /register" , None , Some ( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
123
.await;
14530604 Andy McCall 1 month ago ↶
124
let andy = r.cookie.unwrap ( ) ;
125
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
126
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
127
app.repos.create ( & repo, None ) .unwrap ( ) ;
128
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
129
130
131
let work = tempfile:: tempdir( ) .unwrap ( ) ;
132
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
133
client.create_identity ( " Andy McCall" , " [email protected] " , IdentityScope:: Repo) .unwrap ( ) ;
134
client.config.set ( " core.sign" , " change" ) .unwrap ( ) ;
135
client.config.set ( " remote.origin.url" , & format! ( " {base} /andy/demo" ) ) .unwrap ( ) ;
136
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
137
client.save_config ( ) .unwrap ( ) ;
138
client.policy_init ( ) .unwrap ( ) ;
139
write ( work.path ( ) , " a.txt" , b " one\n two\n " ) ;
140
client.snapshot ( " t" ) .unwrap ( ) ;
141
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
142
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
143
let key = client.config.author_key.unwrap ( ) ;
144
let id = client.identity ( & key) .unwrap ( ) ;
145
db:: accounts:: link_identity( & app.db, andy_id, & key.to_hex ( ) , & id.name, & id.email, & id.encode ( ) ) .await.unwrap ( ) ;
146
147
148
let page = send ( app, " GET" , " /andy/demo/edit/main/a.txt" , Some ( & andy) , None ) .await;
149
assert_eq! ( page.status, StatusCode:: OK ) ;
150
assert! ( page.body.contains ( " not delegated" ) , " {}" , page.body) ;
151
assert! ( page.body.contains ( " textarea class=\" code\" " ) ) ;
152
let csrf = csrf_of ( & page.body) ;
153
let r = send (
154
app,
155
" POST" ,
156
" /andy/demo/edit/main" ,
157
Some ( & andy) ,
158
Some ( & format! ( " _csrf={csrf} &path=a.txt&original=a.txt&content=x&message=m" ) ) ,
159
)
160
.await;
161
assert! ( r.body.contains ( " not delegated" ) , " refused: {}" , r.body) ;
162
163
b43a9ab0 Andy McCall 1 month ago ↶
164
client.delegate_add ( app.hub_key.identity.clone ( ) , oxy_core:: identity:: Scope:: CHANGES | oxy_core:: identity:: Scope:: REFS , None ) .unwrap ( ) ;
14530604 Andy McCall 1 month ago ↶
165
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) , " policy update pushed" ) ;
166
let key2 = client.config.author_key.unwrap ( ) ;
167
let id2 = client.identity ( & key2) .unwrap ( ) ;
b43a9ab0 Andy McCall 1 month ago ↶
168
assert! ( id2.delegate ( & app.hub_key.key) .is_some ( ) ) ;
8933faff Andy McCall 1 month ago ↶
169
170
14530604 Andy McCall 1 month ago ↶
171
db:: accounts:: link_identity( & app.db, andy_id, & key2.to_hex ( ) , & id2.name, & id2.email, & id2.encode ( ) ) .await.unwrap ( ) ;
172
173
174
let r = send (
175
app,
176
" POST" ,
177
" /andy/demo/edit/main" ,
178
Some ( & andy) ,
179
Some ( & format! (
180
" _csrf={csrf} &path=a.txt&original=a.txt&content={} &message={} &mode=commit" ,
181
enc ( " one\n two\n three\n " ) ,
182
enc ( " Add three" )
183
) ) ,
184
)
185
.await;
186
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
187
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/file/main/a.txt" ) ) ;
188
let raw = send ( app, " GET" , " /andy/demo/file/main/a.txt?plain=1" , None , None ) .await;
189
assert_eq! ( raw.body, " one\n two\n three\n " ) ;
190
let hist = send ( app, " GET" , " /andy/demo/changes/main" , Some ( & andy) , None ) .await.body;
191
assert! ( hist.contains ( " Add three" ) ) ;
192
assert! ( send ( app, " GET" , " /andy/demo/pushes" , None , None ) .await.body.contains ( " web edit" ) ) ;
193
let trust = send ( app, " GET" , " /andy/demo/trust" , None , None ) .await.body;
194
assert! ( ! trust.contains ( " INVALID" ) , " every change verifies: {trust}" ) ;
195
196
197
let r = send (
198
app,
199
" POST" ,
200
" /andy/demo/edit/main" ,
201
Some ( & andy) ,
202
Some ( & format! ( " _csrf={csrf} &path=docs/guide.md&content={} &message=Guide&mode=commit" , enc ( " # Guide\n " ) ) ) ,
203
)
204
.await;
205
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/file/main/docs/guide.md" ) , " {}" , r.body) ;
206
207
208
let csrf_i = csrf_of ( & send ( app, " GET" , " /andy/demo/issues/new" , Some ( & andy) , None ) .await.body) ;
209
send ( app, " POST" , " /andy/demo/issues/new" , Some ( & andy) , Some ( & format! ( " _csrf={csrf_i} &title=Typo&body=" ) ) ) .await;
210
let r = send (
211
app,
212
" POST" ,
213
" /andy/demo/edit/main" ,
214
Some ( & andy) ,
215
Some ( & format! (
216
" _csrf={csrf} &path=docs/guide.md&original=docs/guide.md&content={} &message={} &mode=commit" ,
217
enc ( " # The guide\n " ) ,
218
enc ( " Fix typo, closes #1" )
219
) ) ,
220
)
221
.await;
222
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
223
let row = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
224
assert_eq! ( db:: issues:: get( & app.db, row.id, 1 ) .await.unwrap ( ) .unwrap ( ) .state, " closed" ) ;
225
226
227
let r = send (
228
app,
229
" POST" ,
230
" /andy/demo/edit/main" ,
231
Some ( & andy) ,
232
Some ( & format! (
233
" _csrf={csrf} &path=a.txt&original=a.txt&content={} &message={} &mode=propose&new_bookmark=tweak" ,
234
enc ( " ONE\n two\n three\n " ) ,
235
enc ( " Shout one" )
236
) ) ,
237
)
238
.await;
239
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/2" ) , " {}" , r.body) ;
240
let page = send ( app, " GET" , " /andy/demo/proposal/2" , Some ( & andy) , None ) .await;
241
assert! ( page.body.contains ( " ready to land" ) & & page.body.contains ( " >LAND<" ) , " {}" , page.body) ;
0b20f1ec Andy McCall 1 month ago ↶
242
243
assert! ( page.body.contains ( " settings</a> require none." ) , " {}" , page.body) ;
244
assert! ( page.body.contains ( " without any approval?" ) , " {}" , page.body) ;
14530604 Andy McCall 1 month ago ↶
245
let r = send ( app, " POST" , " /andy/demo/proposal/2/land" , Some ( & andy) , Some ( & format! ( " _csrf={} " , csrf_of ( & page.body) ) ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
246
247
let back = r.location.clone ( ) .unwrap_or_default ( ) ;
248
assert! ( back.starts_with ( " /andy/demo/proposal/2?" ) & & back.contains ( " notice=Landed." ) , " {back}" ) ;
249
assert! ( send ( app, " GET" , & back, Some ( & andy) , None ) .await.body.contains ( " Landed." ) ) ;
14530604 Andy McCall 1 month ago ↶
250
assert_eq! ( send ( app, " GET" , " /andy/demo/file/main/a.txt?plain=1" , None , None ) .await.body, " ONE\n two\n three\n " ) ;
251
252
253
db:: proposals:: set_requirements(
254
& app.db,
255
row.id,
256
db:: proposals:: Requirements { required_approvals: 0 , require_owners: false , allow_conflicts: true } ,
257
)
258
.await
259
.unwrap ( ) ;
260
for ( b, text) in [ ( " left" , " Left\n two\n three\n " ) , ( " right" , " Right\n two\n three\n " ) ] {
261
let r = send (
262
app,
263
" POST" ,
264
" /andy/demo/edit/main" ,
265
Some ( & andy) ,
266
Some ( & format! ( " _csrf={csrf} &path=a.txt&original=a.txt&content={} &message={b} &mode=propose&new_bookmark={b} " , enc ( text) ) ) ,
267
)
268
.await;
269
assert! ( r.location.as_deref ( ) .is_some_and ( | l | l.contains ( " /proposal/" ) ) , " {}" , r.body) ;
270
}
271
for n in [ 3 , 4 ] {
272
let page = send ( app, " GET" , & format! ( " /andy/demo/proposal/{n} " ) , Some ( & andy) , None ) .await;
273
let r =
274
send ( app, " POST" , & format! ( " /andy/demo/proposal/{n} /land" ) , Some ( & andy) , Some ( & format! ( " _csrf={} " , csrf_of ( & page.body) ) ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
275
assert! ( r.location.as_deref ( ) .is_some_and ( | l | l.contains ( " notice=Landed." ) ) , " #{n}: {:?}" , r.location) ;
14530604 Andy McCall 1 month ago ↶
276
}
277
let conflicts = send ( app, " GET" , " /andy/demo/conflicts" , Some ( & andy) , None ) .await.body;
c5f3bde4 Andy McCall 1 month ago ↶
278
assert! ( conflicts.contains ( " resolve region by region" ) & & conflicts.contains ( " /andy/demo/resolve/main/a.txt" ) , " {conflicts}" ) ;
14530604 Andy McCall 1 month ago ↶
279
let page = send ( app, " GET" , " /andy/demo/edit/main/a.txt" , Some ( & andy) , None ) .await;
280
assert! ( page.body.contains ( " Resolve a.txt" ) & & page.body.contains ( " <<<<<<< oxy conflict" ) , " {}" , page.body) ;
281
282
let r = send (
283
app,
284
" POST" ,
285
" /andy/demo/edit/main" ,
286
Some ( & andy) ,
287
Some ( & format! (
288
" _csrf={csrf} &path=a.txt&original=a.txt&content={} &message=half&mode=commit" ,
289
enc ( " <<<<<<< oxy conflict 1 of 1, side 1 of 2: change x \" y\" \n Left\n " )
290
) ) ,
291
)
292
.await;
293
assert_ne! ( r.status, StatusCode:: SEE_OTHER ) ;
c5f3bde4 Andy McCall 1 month ago ↶
294
295
let page = send ( app, " GET" , " /andy/demo/resolve/main/a.txt" , Some ( & andy) , None ) .await;
296
assert! (
297
page.body.contains ( " id=\" region-1\" " ) & & page.body.contains ( " keep side 2" ) & & page.body.contains ( " keep every side" ) ,
298
" {}" ,
299
page.body
300
) ;
16cb2b3a Andy McCall 1 month ago ↶
301
assert! (
302
page.body.contains ( " vscode://oxygen.oxygen-vscode/resolve?url=" ) & & page.body.contains ( " &bookmark=main&path=a.txt" ) ,
303
" {}" ,
304
page.body
305
) ;
c5f3bde4 Andy McCall 1 month ago ↶
306
let state = {
307
let i = page.body.find ( " name=\" state\" value=\" " ) .unwrap ( ) + " name=\" state\" value=\" " .len ( ) ;
308
page.body[ i.. i + 64 ] .to_string ( )
309
} ;
5e6d6484 Andy McCall 1 month ago ↶
310
311
let live_req = Request:: builder( )
312
.uri ( " /andy/demo/resolve-live/main/a.txt?client=tab-a" )
313
.header ( header:: COOKIE , format! ( " oxyhub={andy} " ) )
314
.body ( Body:: empty( ) )
315
.unwrap ( ) ;
316
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( live_req) .await.unwrap ( ) ;
317
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
318
let mut stream = res.into_body ( ) ;
319
let snapshot = next_event ( & mut stream, " snapshot" ) .await;
320
assert! ( snapshot.contains ( " \" present\" :[\" andy\" ]" ) , " {snapshot}" ) ;
321
let choose = | region : & str , choice : & str | {
322
let body = serde_json:: json! ( { " _csrf" : csrf, " client" : " tab-b" , " region" : region, " choice" : choice, " text" : " " } ) .to_string ( ) ;
323
Request:: builder( )
324
.method ( " POST" )
325
.uri ( " /andy/demo/resolve-live/main/a.txt" )
326
.header ( header:: COOKIE , format! ( " oxyhub={andy} " ) )
327
.header ( header:: CONTENT_TYPE , " application/json" )
328
.body ( Body:: from( body) )
329
.unwrap ( )
330
} ;
331
let r = oxygenhub:: router( app.clone ( ) ) .oneshot ( choose ( " 1" , " side2" ) ) .await.unwrap ( ) ;
332
assert_eq! ( r.status ( ) , StatusCode:: NO_CONTENT ) ;
333
let shared = next_event ( & mut stream, " choice" ) .await;
334
assert! ( shared.contains ( " \" side2\" " ) & & shared.contains ( " \" tab-b\" " ) , " {shared}" ) ;
335
assert_ne! ( oxygenhub:: router( app.clone ( ) ) .oneshot ( choose ( " 1" , " rm -rf" ) ) .await.unwrap ( ) .status ( ) , StatusCode:: NO_CONTENT ) ;
336
c5f3bde4 Andy McCall 1 month ago ↶
337
let stale = send (
338
app,
339
" POST" ,
340
" /andy/demo/resolve/main/a.txt" ,
341
Some ( & andy) ,
342
Some ( & format! ( " _csrf={csrf} &state={} &r1=side2&message=x" , " 0" .repeat ( 64 ) ) ) ,
343
)
344
.await;
345
assert! ( stale.body.contains ( " has moved since this page was loaded" ) , " {}" , stale.body) ;
346
let unchosen =
347
send ( app, " POST" , " /andy/demo/resolve/main/a.txt" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &state={state} &message=x" ) ) ) .await;
348
assert! ( unchosen.body.contains ( " choose how to resolve conflict 1" ) , " {}" , unchosen.body) ;
14530604 Andy McCall 1 month ago ↶
349
let r = send (
350
app,
351
" POST" ,
c5f3bde4 Andy McCall 1 month ago ↶
352
" /andy/demo/resolve/main/a.txt" ,
14530604 Andy McCall 1 month ago ↶
353
Some ( & andy) ,
c5f3bde4 Andy McCall 1 month ago ↶
354
Some ( & format! ( " _csrf={csrf} &state={state} &r1=text&t1={} &message={} " , enc ( " Both" ) , enc ( " Resolve a.txt" ) ) ) ,
14530604 Andy McCall 1 month ago ↶
355
)
356
.await;
357
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
c5f3bde4 Andy McCall 1 month ago ↶
358
let change = r.location.unwrap ( ) ;
359
assert! ( change.contains ( " /andy/demo/change/" ) , " {change}" ) ;
5e6d6484 Andy McCall 1 month ago ↶
360
let landed = next_event ( & mut stream, " landed" ) .await;
361
assert! ( landed.contains ( & change) , " the other tab hears the resolution land: {landed}" ) ;
362
drop ( stream) ;
14530604 Andy McCall 1 month ago ↶
363
assert! ( send ( app, " GET" , " /andy/demo/conflicts" , None , None ) .await.body.contains ( " No bookmark carries a conflict" ) ) ;
364
assert_eq! ( send ( app, " GET" , " /andy/demo/file/main/a.txt?plain=1" , None , None ) .await.body, " Both\n two\n three\n " ) ;
c5f3bde4 Andy McCall 1 month ago ↶
365
let shown = send ( app, " GET" , & change, Some ( & andy) , None ) .await.body;
366
assert! ( shown.to_lowercase ( ) .contains ( " resolution" ) , " a resolution change" ) ;
14530604 Andy McCall 1 month ago ↶
367
fd958fbc Andy McCall 1 month ago ↶
368
369
for ( b, text) in [ ( " first-new" , " first\n " ) , ( " second-new" , " second\n " ) ] {
370
let r = send (
371
app,
372
" POST" ,
373
" /andy/demo/edit/main" ,
374
Some ( & andy) ,
375
Some ( & format! ( " _csrf={csrf} &path=new.txt&content={} &message={b} &mode=propose&new_bookmark={b} " , enc ( text) ) ) ,
376
)
377
.await;
378
assert! ( r.location.as_deref ( ) .is_some_and ( | l | l.contains ( " /proposal/" ) ) , " {}" , r.body) ;
379
}
380
for n in [ 5 , 6 ] {
381
let page = send ( app, " GET" , & format! ( " /andy/demo/proposal/{n} " ) , Some ( & andy) , None ) .await;
382
let r =
383
send ( app, " POST" , & format! ( " /andy/demo/proposal/{n} /land" ) , Some ( & andy) , Some ( & format! ( " _csrf={} " , csrf_of ( & page.body) ) ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
384
assert! ( r.location.as_deref ( ) .is_some_and ( | l | l.contains ( " notice=Landed." ) ) , " #{n}: {:?} {}" , r.location, r.body) ;
fd958fbc Andy McCall 1 month ago ↶
385
}
386
let conflicts = send ( app, " GET" , " /andy/demo/conflicts" , Some ( & andy) , None ) .await.body;
387
assert! ( conflicts.contains ( " choose which to keep" ) & & conflicts.contains ( " /andy/demo/resolve/main/new.txt" ) , " {conflicts}" ) ;
388
let page = send ( app, " GET" , " /andy/demo/resolve/main/new.txt" , Some ( & andy) , None ) .await;
389
assert! ( page.body.contains ( " new.txt.oxy-conflict-1" ) & & page.body.contains ( " new.txt.oxy-conflict-2" ) , " {}" , page.body) ;
390
let state = {
391
let i = page.body.find ( " name=\" state\" value=\" " ) .unwrap ( ) + " name=\" state\" value=\" " .len ( ) ;
392
page.body[ i.. i + 64 ] .to_string ( )
393
} ;
394
16cb2b3a Andy McCall 1 month ago ↶
395
let preview = | text : & str | page.body.find ( & format! ( " <pre class=\" marked\" >{text} " ) ) .expect ( " a candidate's preview" ) ;
396
let second = if preview ( " second" ) < preview ( " first" ) { 0 } else { 1 } ;
fd958fbc Andy McCall 1 month ago ↶
397
let r = send (
398
app,
399
" POST" ,
400
" /andy/demo/resolve/main/new.txt" ,
401
Some ( & andy) ,
402
Some ( & format! ( " _csrf={csrf} &state={state} &keep={second} &message={} " , enc ( " Keep the second new.txt" ) ) ) ,
403
)
404
.await;
405
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
406
407
let hash = r.location.unwrap ( ) .rsplit ( ' /' ) .next ( ) .unwrap ( ) .to_string ( ) ;
408
let detail = send ( app, " GET" , & format! ( " /api/v1/repos/andy/demo/changes/{hash} " ) , Some ( & andy) , None ) .await.body;
409
assert! ( detail.contains ( " \" kind\" :\" renamed\" " ) , " {detail}" ) ;
410
assert! ( send ( app, " GET" , " /andy/demo/conflicts" , None , None ) .await.body.contains ( " No bookmark carries a conflict" ) ) ;
411
assert_eq! ( send ( app, " GET" , " /andy/demo/file/main/new.txt?plain=1" , None , None ) .await.body, " second\n " ) ;
ffb67a81 Andy McCall 23 days ago
412
let tree = send ( app, " GET" , " /andy/demo?at=main" , None , None ) .await.body;
fd958fbc Andy McCall 1 month ago ↶
413
assert! ( ! tree.contains ( " oxy-conflict" ) , " the candidates are gone" ) ;
414
9e5ef6c2 Andy McCall 1 month ago ↶
415
416
417
let r = send (
418
app,
419
" POST" ,
420
" /andy/demo/edit/main" ,
421
Some ( & andy) ,
422
Some ( & format! ( " _csrf={csrf} &path=img.bin&content=base%00&message=img&mode=commit" ) ) ,
423
)
424
.await;
425
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
426
for ( b, text) in [ ( " img-a" , " alpha%00" ) , ( " img-b" , " beta%00" ) ] {
427
let r = send (
428
app,
429
" POST" ,
430
" /andy/demo/edit/main" ,
431
Some ( & andy) ,
432
Some ( & format! ( " _csrf={csrf} &path=img.bin&original=img.bin&content={text} &message={b} &mode=propose&new_bookmark={b} " ) ) ,
433
)
434
.await;
435
assert! ( r.location.as_deref ( ) .is_some_and ( | l | l.contains ( " /proposal/" ) ) , " {}" , r.body) ;
436
}
437
for n in [ 7 , 8 ] {
438
let page = send ( app, " GET" , & format! ( " /andy/demo/proposal/{n} " ) , Some ( & andy) , None ) .await;
439
let r =
440
send ( app, " POST" , & format! ( " /andy/demo/proposal/{n} /land" ) , Some ( & andy) , Some ( & format! ( " _csrf={} " , csrf_of ( & page.body) ) ) ) .await;
a1b98695 Andy McCall 1 month ago ↶
441
assert! ( r.location.as_deref ( ) .is_some_and ( | l | l.contains ( " notice=Landed." ) ) , " #{n}: {:?} {}" , r.location, r.body) ;
9e5ef6c2 Andy McCall 1 month ago ↶
442
}
443
let conflicts = send ( app, " GET" , " /andy/demo/conflicts" , Some ( & andy) , None ) .await.body;
444
assert! ( conflicts.contains ( " replace (2 candidates)" ) & & conflicts.contains ( " /andy/demo/resolve/main/img.bin" ) , " {conflicts}" ) ;
445
let page = send ( app, " GET" , " /andy/demo/resolve/main/img.bin" , Some ( & andy) , None ) .await;
446
assert! ( page.body.contains ( " has 2 versions" ) & & page.body.contains ( " version 2 from change" ) , " {}" , page.body) ;
447
let state = {
448
let i = page.body.find ( " name=\" state\" value=\" " ) .unwrap ( ) + " name=\" state\" value=\" " .len ( ) ;
449
page.body[ i.. i + 64 ] .to_string ( )
450
} ;
451
let version = | text : & str | page.body.find ( & format! ( " <pre class=\" marked\" >{text} " ) ) .expect ( " a version's preview" ) ;
452
let beta = if version ( " beta" ) < version ( " alpha" ) { 0 } else { 1 } ;
453
let r = send (
454
app,
455
" POST" ,
456
" /andy/demo/resolve/main/img.bin" ,
457
Some ( & andy) ,
458
Some ( & format! ( " _csrf={csrf} &state={state} &keep={beta} &message={} " , enc ( " Keep beta" ) ) ) ,
459
)
460
.await;
461
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
462
assert! ( send ( app, " GET" , " /andy/demo/conflicts" , None , None ) .await.body.contains ( " No bookmark carries a conflict" ) ) ;
463
assert_eq! ( send ( app, " GET" , " /andy/demo/raw/main/img.bin" , None , None ) .await.body, " beta\u{0} " ) ;
464
let change = send ( app, " GET" , r.location.as_deref ( ) .unwrap ( ) , Some ( & andy) , None ) .await.body;
465
assert! ( change.contains ( " >resolution<" ) , " a resolution change" ) ;
466
14530604 Andy McCall 1 month ago ↶
467
468
let r = send (
469
app,
470
" POST" ,
471
" /andy/demo/edit/main" ,
472
Some ( & andy) ,
473
Some ( & format! ( " _csrf={csrf} &path=docs/guide.md&original=docs/guide.md&message=Drop&delete=1" ) ) ,
474
)
475
.await;
476
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
477
assert_eq! ( send ( app, " GET" , " /andy/demo/file/main/docs/guide.md" , None , None ) .await.status, StatusCode:: NOT_FOUND ) ;
478
479
480
client.pull ( " origin" ) .unwrap ( ) ;
481
let v: Vec< _ > =
482
client.log ( ) .unwrap ( ) .into_iter ( ) .filter ( | e | ! e.is_working_copy) .map ( | e | client.authenticate_change ( & e.hash) .unwrap ( ) ) .collect ( ) ;
483
assert! ( v.iter ( ) .all ( | s | matches! ( s, oxy_core:: repo:: SignatureStatus:: Valid { .. } ) ) , " {v:?}" ) ;
484
}
8933faff Andy McCall 1 month ago ↶
485
486
487
488
489
490
# [ tokio ::test ( flavor = " multi_thread" ) ]
491
async fn a_rotated_identity_links_itself_and_an_agent_s_change_says_via ( ) {
492
hermetic ( ) ;
493
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
494
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
c8684d44 Andy McCall 21 days ago
495
496
config.registration.setup_code = " test-setup" .into ( ) ;
8933faff Andy McCall 1 month ago ↶
497
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
498
config.registration.mode = " open" .into ( ) ;
499
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
500
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
501
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
502
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
503
let router = oxygenhub:: router( app.clone ( ) ) ;
504
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
505
let base = format! ( " http://127.0.0.1:{port} " ) ;
506
let app = & app;
c8684d44 Andy McCall 21 days ago
507
let r =
508
send ( app, " POST" , " /register" , None , Some ( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
509
.await;
8933faff Andy McCall 1 month ago ↶
510
assert! ( r.cookie.is_some ( ) ) ;
511
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
512
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
513
app.repos.create ( & repo, None ) .unwrap ( ) ;
514
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
515
let work = tempfile:: tempdir( ) .unwrap ( ) ;
516
let home = tempfile:: tempdir( ) .unwrap ( ) ;
517
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) .with_home ( home.path ( ) ) ;
518
client.create_identity ( " Andy" , " [email protected] " , IdentityScope:: Repo) .unwrap ( ) ;
519
client.config.set ( " core.sign" , " change" ) .unwrap ( ) ;
520
client.config.set ( " remote.origin.url" , & format! ( " {base} /andy/demo" ) ) .unwrap ( ) ;
521
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
522
client.save_config ( ) .unwrap ( ) ;
523
write ( work.path ( ) , " a.txt" , b " one\n " ) ;
524
client.snapshot ( " t" ) .unwrap ( ) ;
525
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
526
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
527
let key1 = client.config.author_key.unwrap ( ) ;
528
let id1 = client.identity ( & key1) .unwrap ( ) ;
529
db:: accounts:: link_identity( & app.db, andy_id, & key1.to_hex ( ) , & id1.name, & id1.email, & id1.encode ( ) ) .await.unwrap ( ) ;
530
531
532
let agent_home = tempfile:: tempdir( ) .unwrap ( ) ;
533
let mut agent_repo = Repo:: init( & work.path ( ) .join ( " agent-scratch" ) ) .unwrap ( ) .with_home ( agent_home.path ( ) ) ;
534
let ( agent_key, agent_id) = agent_repo.create_identity ( " Antigravity" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
535
client.delegate_add ( agent_id, oxy_core:: identity:: Scope:: CHANGES , None ) .unwrap ( ) ;
536
let key2 = client.config.author_key.unwrap ( ) ;
537
assert_ne! ( key1, key2) ;
538
write ( work.path ( ) , " a.txt" , b " one\n two\n " ) ;
539
client.snapshot ( " t" ) .unwrap ( ) ;
540
let by_andy = client.commit ( " second, by Andy" , & [ ] ) .unwrap ( ) ;
541
542
let mut as_agent = Repo:: open( work.path ( ) ) .unwrap ( ) .with_home ( agent_home.path ( ) ) ;
543
as_agent.set_user_config ( " author.signer" , & agent_key.to_hex ( ) ) .unwrap ( ) ;
544
as_agent.set_user_config ( " core.sign" , " change" ) .unwrap ( ) ;
545
write ( work.path ( ) , " b.txt" , b " by the agent\n " ) ;
546
as_agent.snapshot ( " t" ) .unwrap ( ) ;
547
let by_agent = as_agent.commit ( " third, by the agent" , & [ ] ) .unwrap ( ) ;
7c06f5b2 Andy McCall 19 days ago ↶
548
549
550
client.refresh ( ) .unwrap ( ) ;
8933faff Andy McCall 1 month ago ↶
551
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
552
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
553
554
555
let linked = db:: accounts:: identity_by_key( & app.db, & key2.to_hex ( ) ) .await.unwrap ( ) ;
556
assert_eq! ( linked.map ( | l | l.account_id ) , Some ( andy_id) , " the rotated identity links itself" ) ;
557
assert! ( db:: accounts:: identity_by_key( & app.db, & agent_key.to_hex ( ) ) .await.unwrap ( ) .is_none ( ) , " an agent's key is nobody's account" ) ;
558
559
560
let page = send ( app, " GET" , & format! ( " /andy/demo/change/{} " , by_agent.to_hex ( ) ) , None , None ) .await.body;
561
assert! ( page.contains ( " <dt>via</dt>" ) , " {page}" ) ;
562
assert! ( page.contains ( " Antigravity" ) & & page.contains ( " delegate for changes" ) , " {page}" ) ;
563
assert! ( page.contains ( " href=\" /andy\" " ) , " the author is Andy's account: {page}" ) ;
564
let andy_page = send ( app, " GET" , & format! ( " /andy/demo/change/{} " , by_andy.to_hex ( ) ) , None , None ) .await.body;
565
assert! ( ! andy_page.contains ( " <dt>via</dt>" ) , " {andy_page}" ) ;
566
567
let rows = send ( app, " GET" , " /andy/demo/changes/main" , None , None ) .await.body;
568
assert! ( rows.contains ( " via Antigravity" ) , " {rows}" ) ;
569
let hist = send ( app, " GET" , " /api/v1/repos/andy/demo/history/main" , None , None ) .await.body;
570
let v: serde_json:: Value = serde_json:: from_str( & hist) .unwrap ( ) ;
571
let agent_change = v[ " changes" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | c | c[ " hash" ] = = by_agent.to_hex ( ) ) .unwrap ( ) ;
572
assert_eq! ( agent_change[ " via" ] [ " name" ] , " Antigravity" ) ;
573
assert_eq! ( agent_change[ " via" ] [ " scope" ] , " changes" ) ;
574
assert_eq! ( agent_change[ " author" ] [ " account" ] , " andy" ) ;
575
576
let overview = send ( app, " GET" , " /andy/demo" , None , None ) .await.body;
206c1fc1 Andy McCall 1 month ago ↶
577
assert! ( overview.contains ( " /avatar/agent/Antigravity" ) & & overview.contains ( " Antigravity <span class=\" mute small\" title=\" changes signed by this agent for them\" >· 1</span>" ) , " {overview}" ) ;
578
assert_eq! ( overview.matches ( " class=\" avatar sm\" src=\" /avatar/andy\" " ) .count ( ) , 1 , " one row for Andy, whatever his keys" ) ;
579
let mark = send ( app, " GET" , " /avatar/agent/Claude%20Code" , None , None ) .await;
580
assert! ( mark.body.contains ( " <title>Claude</title>" ) , " a known agent gets its brand mark" ) ;
581
assert! ( send ( app, " GET" , " /avatar/agent/HAL" , None , None ) .await.body.contains ( " <svg" ) , " an unknown one its identicon" ) ;
8933faff Andy McCall 1 month ago ↶
582
583
let badge = send ( app, " GET" , " /andy/demo/flow/badge.svg" , None , None ) .await;
584
assert_eq! ( badge.status, StatusCode:: OK ) ;
585
assert! ( badge.body.contains ( " <svg" ) & & badge.body.contains ( " no runs" ) , " {}" , badge.body) ;
586
}
d0fbea2e Andy McCall 1 month ago ↶
587
588
# [ tokio ::test ( flavor = " multi_thread" ) ]
589
async fn a_repository_is_transferred_and_archived_from_advanced_settings ( ) {
590
hermetic ( ) ;
591
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
592
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
c8684d44 Andy McCall 21 days ago
593
594
config.registration.setup_code = " test-setup" .into ( ) ;
d0fbea2e Andy McCall 1 month ago ↶
595
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
596
config.registration.mode = " open" .into ( ) ;
597
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
598
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
599
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
600
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
601
let router = oxygenhub:: router( app.clone ( ) ) ;
602
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
603
let base = format! ( " http://127.0.0.1:{port} " ) ;
604
let app = & app;
c8684d44 Andy McCall 21 days ago
605
let r =
606
send ( app, " POST" , " /register" , None , Some ( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
607
.await;
d0fbea2e Andy McCall 1 month ago ↶
608
let andy = r.cookie.unwrap ( ) ;
609
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
610
db:: accounts:: create_org( & app.db, " doomsday" , " Doomsday One" , andy_id) .await.unwrap ( ) ;
611
let repo = RepoName:: parse( " andy/vk" ) .unwrap ( ) ;
612
app.repos.create ( & repo, None ) .unwrap ( ) ;
613
db:: repos:: create( & app.db, " andy" , " vk" , " a port" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
614
let work = tempfile:: tempdir( ) .unwrap ( ) ;
615
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
616
client.create_identity ( " Andy McCall" , " [email protected] " , IdentityScope:: Repo) .unwrap ( ) ;
617
client.config.set ( " core.sign" , " change" ) .unwrap ( ) ;
618
client.config.set ( " remote.origin.url" , & format! ( " {base} /andy/vk" ) ) .unwrap ( ) ;
619
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
620
client.save_config ( ) .unwrap ( ) ;
621
write ( work.path ( ) , " a.txt" , b " one\n " ) ;
622
client.snapshot ( " t" ) .unwrap ( ) ;
623
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
624
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
625
626
627
let settings = send ( app, " GET" , " /andy/vk/settings" , Some ( & andy) , None ) .await.body;
628
assert! (
629
settings.contains ( " Advanced" ) & & settings.contains ( " Transfer ownership" ) & & settings.contains ( " Delete wiki data" ) ,
630
" {settings}"
631
) ;
632
let csrf = csrf_of ( & settings) ;
633
let r = send ( app, " POST" , " /andy/vk/settings/transfer" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &owner=doomsday&confirm=wrong" ) ) ) .await;
634
assert! ( r.body.contains ( " full name to confirm" ) , " {}" , r.body) ;
635
let r =
636
send ( app, " POST" , " /andy/vk/settings/transfer" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &owner=nobody&confirm=andy%2Fvk" ) ) ) .await;
637
assert! ( r.body.contains ( " no user or organisation" ) , " {}" , r.body) ;
638
639
640
let r =
641
send ( app, " POST" , " /andy/vk/settings/transfer" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &owner=doomsday&confirm=andy%2Fvk" ) ) ) .await;
642
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
643
let page = send ( app, " GET" , " /doomsday/vk" , None , None ) .await;
644
assert_eq! ( page.status, StatusCode:: OK ) ;
645
assert! ( page.body.contains ( " a port" ) , " {}" , page.body) ;
646
let old = send ( app, " GET" , " /andy/vk/changes/main" , None , None ) .await;
647
assert_eq! ( old.status, StatusCode:: SEE_OTHER , " {}" , old.body) ;
648
assert! ( app.repos.exists ( & RepoName:: parse( " doomsday/vk" ) .unwrap ( ) ) & & ! app.repos.exists ( & repo) ) ;
649
write ( work.path ( ) , " a.txt" , b " one\n two\n " ) ;
650
client.snapshot ( " t" ) .unwrap ( ) ;
651
client.commit ( " second" , & [ ] ) .unwrap ( ) ;
652
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) , " the old remote address still resolves" ) ;
653
let audit = db:: accounts:: audit_rows( & app.db, 10 ) .await.unwrap ( ) ;
654
assert! ( audit.iter ( ) .any ( | a | a.action = = " repo.transferred" & & a.target = = " andy/vk" & & a.detail = = " doomsday/vk" ) ) ;
655
656
657
let settings = send ( app, " GET" , " /doomsday/vk/settings" , Some ( & andy) , None ) .await.body;
658
let csrf = csrf_of ( & settings) ;
659
let r =
660
send ( app, " POST" , " /doomsday/vk/settings/archive" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &action=archive&reason=Superseded" ) ) )
661
.await;
662
assert! ( r.body.contains ( " is archived" ) & & r.body.contains ( " Bring this repository back" ) & & r.body.contains ( " Superseded" ) , " {}" , r.body) ;
663
assert! ( send ( app, " GET" , " /doomsday/vk" , None , None ) .await.body.contains ( " >archived<" ) ) ;
664
write ( work.path ( ) , " a.txt" , b " one\n two\n three\n " ) ;
665
client.snapshot ( " t" ) .unwrap ( ) ;
666
client.commit ( " third" , & [ ] ) .unwrap ( ) ;
667
let pushed = client.push ( " origin" , false ) ;
668
assert! ( pushed.is_err ( ) | | ! pushed.unwrap ( ) .iter ( ) .all ( | r | r.ok ) , " an archived repository takes no pushes" ) ;
669
let r = send ( app, " POST" , " /doomsday/vk/issues/new" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &title=x&body=y" ) ) ) .await;
670
assert! ( r.body.contains ( " archived and read-only" ) , " {} {}" , r.status, r.body) ;
671
let r = send ( app, " POST" , " /doomsday/vk/settings/archive" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} &action=unarchive" ) ) ) .await;
672
assert! ( r.body.contains ( " The repository is back" ) , " {}" , r.body) ;
673
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
674
}
66fd9d6b Andy McCall 1 month ago ↶
675
676
# [ tokio ::test ( flavor = " multi_thread" ) ]
677
async fn a_wiki_is_edited_on_the_web_and_cloned_and_pushed_as_name_dot_wiki ( ) {
678
hermetic ( ) ;
679
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
680
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
c8684d44 Andy McCall 21 days ago
681
682
config.registration.setup_code = " test-setup" .into ( ) ;
66fd9d6b Andy McCall 1 month ago ↶
683
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
684
config.registration.mode = " open" .into ( ) ;
685
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
686
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
687
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
688
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
689
let router = oxygenhub:: router( app.clone ( ) ) ;
690
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
691
let base = format! ( " http://127.0.0.1:{port} " ) ;
692
let app = & app;
c8684d44 Andy McCall 21 days ago
693
let r =
694
send ( app, " POST" , " /register" , None , Some ( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
695
.await;
66fd9d6b Andy McCall 1 month ago ↶
696
let andy = r.cookie.unwrap ( ) ;
697
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
698
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
699
app.repos.create ( & repo, None ) .unwrap ( ) ;
700
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
701
702
let work = tempfile:: tempdir( ) .unwrap ( ) ;
703
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
704
client.create_identity ( " Andy McCall" , " [email protected] " , IdentityScope:: Repo) .unwrap ( ) ;
705
client.config.set ( " core.sign" , " change" ) .unwrap ( ) ;
706
client.config.set ( " remote.origin.url" , & format! ( " {base} /andy/demo" ) ) .unwrap ( ) ;
707
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
708
client.save_config ( ) .unwrap ( ) ;
709
write ( work.path ( ) , " a.txt" , b " one\n " ) ;
710
client.snapshot ( " t" ) .unwrap ( ) ;
711
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
712
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
713
client.delegate_add ( app.hub_key.identity.clone ( ) , oxy_core:: identity:: Scope:: CHANGES | oxy_core:: identity:: Scope:: REFS , None ) .unwrap ( ) ;
714
let key = client.config.author_key.unwrap ( ) ;
715
let id = client.identity ( & key) .unwrap ( ) ;
716
db:: accounts:: link_identity( & app.db, andy_id, & key.to_hex ( ) , & id.name, & id.email, & id.encode ( ) ) .await.unwrap ( ) ;
717
718
719
let page = send ( app, " GET" , " /andy/demo/wiki" , Some ( & andy) , None ) .await;
720
assert_eq! ( page.status, StatusCode:: OK , " {}" , page.body) ;
721
assert! ( page.body.contains ( " No wiki yet" ) & & page.body.contains ( " >Wiki</a>" ) & & page.body.contains ( " andy/demo.wiki" ) , " {}" , page.body) ;
722
assert! ( ! send ( app, " GET" , " /andy/demo" , None , None ) .await.body.contains ( " >Wiki</a>" ) , " no tab for a reader without a wiki" ) ;
723
724
let csrf_n = csrf_of ( & send ( app, " GET" , " /new" , Some ( & andy) , None ) .await.body) ;
725
let r = send ( app, " POST" , " /new" , Some ( & andy) , Some ( & format! ( " _csrf={csrf_n} &owner=andy&name=demo.wiki&visibility=public" ) ) ) .await;
726
assert! ( r.body.contains ( " cannot end in .wiki" ) , " {}" , r.body) ;
727
728
729
let csrf = csrf_of ( & page.body) ;
730
let r = send (
731
app,
732
" POST" ,
733
" /andy/demo/wiki/_new/edit" ,
734
Some ( & andy) ,
735
Some ( & format! ( " _csrf={csrf} &title=Home&content={} " , enc ( " # Welcome\n \n See [the guide](Guide).\n " ) ) ) ,
736
)
737
.await;
738
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
739
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/wiki/Home" ) ) ;
740
let home = send ( app, " GET" , " /andy/demo/wiki" , None , None ) .await.body;
741
assert! (
ffb67a81 Andy McCall 23 days ago
742
home.contains ( " Welcome" )
743
& & home.contains ( " href=\" /andy/demo/wiki/Guide\" " )
744
& & home.contains ( " Andy McCall via" )
745
& & home.contains ( " edited this page" )
746
& & home.contains ( " 1 revision" ) ,
66fd9d6b Andy McCall 1 month ago ↶
747
" {home}"
748
) ;
749
assert! ( send ( app, " GET" , " /andy/demo" , None , None ) .await.body.contains ( " >Wiki</a>" ) , " the tab shows to everyone now" ) ;
750
let missing = send ( app, " GET" , " /andy/demo/wiki/Guide" , Some ( & andy) , None ) .await.body;
751
assert! ( missing.contains ( " no page called <strong>Guide</strong>" ) & & missing.contains ( " _new?title=Guide" ) , " {missing}" ) ;
752
753
let r = send (
754
app,
755
" POST" ,
756
" /andy/demo/wiki/Home/edit" ,
757
Some ( & andy) ,
758
Some ( & format! ( " _csrf={csrf} &title=Home&content={} &message=Say+more" , enc ( " # Welcome\n \n More.\n " ) ) ) ,
759
)
760
.await;
761
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/wiki/Home" ) , " {}" , r.body) ;
762
let hist = send ( app, " GET" , " /andy/demo/wiki/Home/history" , None , None ) .await.body;
763
assert! ( hist.contains ( " Say more" ) & & hist.contains ( " Create Home" ) , " {hist}" ) ;
764
let r = send (
765
app,
766
" POST" ,
767
" /andy/demo/wiki/Home/edit" ,
768
Some ( & andy) ,
769
Some ( & format! ( " _csrf={csrf} &title=Start+Here&content={} " , enc ( " # Start\n " ) ) ) ,
770
)
771
.await;
772
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/wiki/Start-Here" ) , " {}" , r.body) ;
773
assert! ( send ( app, " GET" , " /andy/demo/wiki/Start-Here" , None , None ) .await.body.contains ( " <h1>Start Here</h1>" ) ) ;
774
assert! ( send ( app, " GET" , " /andy/demo/wiki" , None , None ) .await.body.contains ( " no page called <strong>Home</strong>" ) ) ;
775
let pages = send ( app, " GET" , " /andy/demo/wiki/_pages?q=start" , None , None ) .await.body;
776
assert! ( pages.contains ( " href=\" /andy/demo/wiki/Start-Here\" " ) & & pages.contains ( " mentions" ) , " {pages}" ) ;
777
778
779
let wdir = tempfile:: tempdir( ) .unwrap ( ) ;
780
let mut w = Repo:: clone( & format! ( " {base} /andy/demo.wiki" ) , wdir.path ( ) ) .unwrap ( ) ;
781
assert_eq! ( std:: fs:: read_to_string( wdir.path ( ) .join ( " Start-Here.md" ) ) .unwrap ( ) , " # Start\n " ) ;
782
w.create_identity ( " Andy McCall" , " [email protected] " , IdentityScope:: Repo) .unwrap ( ) ;
783
w.config.set ( " core.sign" , " change" ) .unwrap ( ) ;
784
w.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
785
w.save_config ( ) .unwrap ( ) ;
786
write ( wdir.path ( ) , " Guide.md" , b " # Guide\n \n Pushed from a clone.\n " ) ;
787
w.snapshot ( " t" ) .unwrap ( ) ;
788
w.commit ( " Add the guide" , & [ ] ) .unwrap ( ) ;
789
assert! ( w.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) , " pushed to the wiki" ) ;
790
let guide = send ( app, " GET" , " /andy/demo/wiki/Guide" , None , None ) .await.body;
791
assert! ( guide.contains ( " Pushed from a clone" ) , " {guide}" ) ;
792
assert! (
793
db:: flow:: runs( & app.db, db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) .id, 10 , None )
794
.await
795
.map ( | r | r.is_empty ( ) )
796
.unwrap_or ( true ) ,
797
" a wiki push starts no run"
798
) ;
799
800
801
let r = send ( app, " POST" , " /andy/demo/wiki/Guide/delete" , Some ( & andy) , Some ( & format! ( " _csrf={csrf} " ) ) ) .await;
802
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
803
assert! ( send ( app, " GET" , " /andy/demo/wiki/Guide" , None , None ) .await.body.contains ( " no page called" ) ) ;
804
let settings = send ( app, " GET" , " /andy/demo/settings" , Some ( & andy) , None ) .await.body;
805
assert! ( settings.contains ( " DELETE WIKI DATA" ) , " {settings}" ) ;
806
let r = send (
807
app,
808
" POST" ,
809
" /andy/demo/settings/wiki/delete" ,
810
Some ( & andy) ,
811
Some ( & format! ( " _csrf={} &confirm=andy%2Fdemo" , csrf_of ( & settings) ) ) ,
812
)
813
.await;
814
assert! ( r.body.contains ( " The wiki is in the trash" ) , " {}" , r.body) ;
815
assert! ( send ( app, " GET" , " /andy/demo/wiki" , Some ( & andy) , None ) .await.body.contains ( " No wiki yet" ) ) ;
816
}
6e7dfa0a Andy McCall 1 month ago ↶
817
818
819
820
821
# [ tokio ::test ( flavor = " multi_thread" ) ]
822
async fn a_wiki_can_start_with_a_push ( ) {
823
hermetic ( ) ;
824
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
825
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
c8684d44 Andy McCall 21 days ago
826
827
config.registration.setup_code = " test-setup" .into ( ) ;
6e7dfa0a Andy McCall 1 month ago ↶
828
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
829
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
830
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
831
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
832
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
833
let router = oxygenhub:: router( app.clone ( ) ) ;
834
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
835
let base = format! ( " http://127.0.0.1:{port} " ) ;
836
let repo = RepoName:: parse( " andy/docs" ) .unwrap ( ) ;
837
app.repos.create ( & repo, None ) .unwrap ( ) ;
838
db:: repos:: create( & app.db, " andy" , " docs" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
839
let url = format! ( " {base} /andy/docs.wiki" ) ;
840
841
842
let anon = tempfile:: tempdir( ) .unwrap ( ) ;
843
let u = url.clone ( ) ;
844
let p = anon.path ( ) .join ( " w" ) ;
845
assert! ( tokio:: task:: spawn_blocking( move | | Repo:: clone( & u, & p) ) .await.unwrap ( ) .is_err ( ) ) ;
846
assert! ( ! app.wikis.exists ( & repo) ) ;
847
848
849
let work = tempfile:: tempdir( ) .unwrap ( ) ;
850
let mut w = Repo:: init( work.path ( ) ) .unwrap ( ) ;
851
w.config.set ( " remote.origin.url" , & url) .unwrap ( ) ;
852
w.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
853
w.save_config ( ) .unwrap ( ) ;
854
write ( work.path ( ) , " Home.md" , b " # Docs\n \n See [the overview](Overview).\n " ) ;
855
write ( work.path ( ) , " Overview.md" , b " # Overview\n \n ```mermaid\n flowchart LR\n A --> B\n ```\n " ) ;
856
w.snapshot ( " t" ) .unwrap ( ) ;
857
w.commit ( " First pages" , & [ ] ) .unwrap ( ) ;
858
let pushed = tokio:: task:: spawn_blocking( move | | w.push ( " origin" , false ) .unwrap ( ) ) .await.unwrap ( ) ;
859
assert! ( pushed.iter ( ) .all ( | r | r.ok ) , " {pushed:?}" ) ;
860
assert! ( app.wikis.exists ( & repo) ) ;
861
let home = send ( & app, " GET" , " /andy/docs/wiki" , None , None ) .await.body;
ffb67a81 Andy McCall 23 days ago
862
assert! ( home.contains ( " <h1 id=\" docs\" >Docs</h1>" ) & & home.contains ( " href=\" /andy/docs/wiki/Overview\" " ) , " {home}" ) ;
6e7dfa0a Andy McCall 1 month ago ↶
863
let overview = send ( & app, " GET" , " /andy/docs/wiki/Overview" , None , None ) .await.body;
864
assert! ( overview.contains ( " class=\" language-mermaid\" " ) , " a diagram block for the page's script to draw: {overview}" ) ;
865
}