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