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