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