18e381ea Andy McCall 1 month ago ↶
1
2
3
4
5
use axum:: body:: Body;
6
use axum:: http:: { header, Request, StatusCode} ;
7
use http_body_util:: BodyExt;
8
use oxy_core:: repo:: Repo;
9
use oxygenhub:: config:: Config;
10
use oxygenhub:: db;
11
use oxygenhub:: repos:: RepoName;
12
use oxygenhub:: Shared;
13
use std:: path:: Path;
14
use tower:: ServiceExt;
15
16
fn hermetic ( ) {
17
static ONCE : std:: sync:: Once = std:: sync:: Once:: new( ) ;
18
static DIR : std:: sync:: OnceLock< tempfile:: TempDir> = std:: sync:: OnceLock:: new( ) ;
19
ONCE .call_once ( | | {
20
let d = tempfile:: tempdir( ) .unwrap ( ) ;
21
std:: env:: set_var( " OXY_HOME" , d.path ( ) ) ;
22
DIR .set ( d) .ok ( ) ;
23
} ) ;
24
}
25
26
fn write ( root : & Path, rel : & str , content : & [u8 ]) {
27
let p = root.join ( rel) ;
28
std:: fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) .unwrap ( ) ;
29
std:: fs:: write( p, content) .unwrap ( ) ;
30
}
31
32
struct Hub {
33
app : Shared,
34
base : String,
35
_dir : tempfile:: TempDir,
36
}
37
38
async fn hub ( ) -> Hub {
39
hermetic ( ) ;
40
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
41
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
42
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
43
config.registration.mode = " open" .into ( ) ;
44
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
45
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
46
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
47
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
48
let router = oxygenhub:: router( app.clone ( ) ) ;
49
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
50
Hub { app, base: format! ( " http://127.0.0.1:{port} " ) , _dir: dir }
51
}
52
53
struct Res {
54
status : StatusCode,
55
body : String,
56
cookie : Option< String> ,
57
location : Option< String> ,
58
}
59
60
async fn send ( app : & Shared, method : & str , path : & str , cookie : Option< & str > , form : Option< & str > ) -> Res {
61
let mut req = Request:: builder( ) .method ( method) .uri ( path) ;
62
if let Some ( c) = cookie {
63
req = req.header ( header:: COOKIE , format! ( " oxyhub={c} " ) ) ;
64
}
65
let body = match form {
66
Some ( f) => {
67
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
68
Body:: from( f.to_string ( ) )
69
}
70
None => Body:: empty( ) ,
71
} ;
72
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req.body ( body) .unwrap ( ) ) .await.unwrap ( ) ;
73
let status = res.status ( ) ;
74
let cookie = res
75
.headers ( )
76
.get ( header:: SET_COOKIE )
77
.and_then ( | v | v.to_str ( ) .ok ( ) )
78
.and_then ( | v | v.split ( ' ;' ) .next ( ) )
79
.and_then ( | kv | kv.split_once ( ' =' ) )
80
.map ( | ( _, v ) | v.to_string ( ) ) ;
81
let location = res.headers ( ) .get ( header:: LOCATION ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .map ( String :: from) ;
82
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
83
Res { status, body: String :: from_utf8_lossy( & bytes) .into_owned ( ) , cookie, location }
84
}
85
86
async fn get ( app : & Shared, path : & str , cookie : Option< & str > ) -> Res {
87
send ( app, " GET" , path, cookie, None ) .await
88
}
89
90
async fn post ( app : & Shared, path : & str , cookie : Option< & str > , form : & str ) -> Res {
91
send ( app, " POST" , path, cookie, Some ( form) ) .await
92
}
93
94
async fn register ( app : & Shared, user : & str ) -> String {
95
let r = post ( app, " /register" , None , & format! ( " username={user} &email={user} %40example.com&password=correct-horse-battery" ) ) .await;
96
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
97
r.cookie.unwrap ( )
98
}
99
100
fn csrf_of ( page : & str ) -> String {
101
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
102
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
103
}
104
105
# [ tokio ::test ( flavor = " multi_thread" ) ]
106
async fn issues_notifications_and_search ( ) {
107
let h = hub ( ) .await;
108
let app = & h.app;
109
let andy = register ( app, " andy" ) .await;
110
let bob = register ( app, " bob" ) .await;
111
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
112
app.repos.create ( & repo, None ) .unwrap ( ) ;
113
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
114
115
116
let labels_page = get ( app, " /andy/demo/labels" , Some ( & andy) ) .await;
117
let csrf_a = csrf_of ( & labels_page.body) ;
118
assert! ( post ( app, " /andy/demo/labels" , Some ( & andy) , & format! ( " _csrf={csrf_a} &name=bug&colour=%23ff0000&description=Something+broke" ) )
119
.await
120
.body
121
.contains ( " Saved bug" ) ) ;
122
assert! ( post ( app, " /andy/demo/milestones" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=v1&description=First&due=2027-01-01" ) )
123
.await
124
.body
125
.contains ( " v1" ) ) ;
126
assert_eq! ( get ( app, " /andy/demo/labels" , Some ( & bob) ) .await.status, StatusCode:: OK , " readers see labels" ) ;
127
assert! ( ! get ( app, " /andy/demo/labels" , Some ( & bob) ) .await.body.contains ( " Add or change" ) , " but cannot edit them" ) ;
128
129
130
let overview = get ( app, " /andy/demo" , Some ( & bob) ) .await;
131
let csrf_b = csrf_of ( & overview.body) ;
132
assert_eq! ( post ( app, " /andy/demo/watch" , Some ( & bob) , & format! ( " _csrf={csrf_b} " ) ) .await.status, StatusCode:: SEE_OTHER ) ;
133
assert! ( get ( app, " /andy/demo" , Some ( & bob) ) .await.body.contains ( " UNWATCH" ) ) ;
134
let new_page = get ( app, " /andy/demo/issues/new" , Some ( & andy) ) .await;
135
let csrf_a = csrf_of ( & new_page.body) ;
136
let r = post (
137
app,
138
" /andy/demo/issues/new" ,
139
Some ( & andy) ,
140
& format! ( " _csrf={csrf_a} &title=Crash+on+start&body=cc+%40bob+see+%23999+and+deadbeefcafe1234" ) ,
141
)
142
.await;
143
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
144
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/issue/1" ) ) ;
145
let page = get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await;
146
assert! ( page.body.contains ( " Crash on start" ) ) ;
147
assert! ( page.body.contains ( " href=\" /bob\" " ) , " mention linked: {}" , page.body) ;
148
assert! ( page.body.contains ( " /andy/demo/change/deadbeefcafe1234" ) , " hash linked: {}" , page.body) ;
149
assert! ( page.body.contains ( " #999" ) & & ! page.body.contains ( " issue/999" ) , " unknown numbers are not linked" ) ;
150
let unread = db:: notify:: unread( & app.db, db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id) .await.unwrap ( ) ;
151
assert_eq! ( unread, 1 , " one notification, not one per reason" ) ;
ea14c4ec Andy McCall 21 days ago ↶
152
153
154
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
155
let filter = db:: notify:: Filter { done: false , unread_only: false , reason: None , kind: None , repo_id: None } ;
156
let t = db:: notify:: threads( & app.db, bob_id, & filter, 10 ) .await.unwrap ( ) ;
157
assert_eq! ( ( t[ 0 ] .count, t[ 0 ] .unread) , ( 1 , 1 ) ) ;
158
let unread_only = db:: notify:: Filter { unread_only: true , .. filter.clone ( ) } ;
159
assert_eq! ( db:: notify:: threads( & app.db, bob_id, & unread_only, 10 ) .await.unwrap ( ) .len ( ) , 1 ) ;
18e381ea Andy McCall 1 month ago ↶
160
let inbox = get ( app, " /notifications" , Some ( & bob) ) .await;
161
assert! ( inbox.body.contains ( " andy/demo#1: Crash on start" ) , " {}" , inbox.body) ;
162
assert! ( get ( app, " /andy/demo" , Some ( & bob) ) .await.body.contains ( " class=\" count\" >1<" ) , " bell shows the count" ) ;
163
164
165
let page = get ( app, " /andy/demo/issue/1" , Some ( & andy) ) .await;
166
let csrf_a = csrf_of ( & page.body) ;
167
let label_id: i64 = sqlx:: query_scalar( " SELECT id FROM labels WHERE name = 'bug'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
168
let r = post ( app, " /andy/demo/issue/1/labels" , Some ( & andy) , & format! ( " _csrf={csrf_a} &label={label_id} " ) ) .await;
169
assert! ( r.body.contains ( " >bug<" ) , " {}" , r.body) ;
170
let r = post ( app, " /andy/demo/issue/1/assign" , Some ( & andy) , & format! ( " _csrf={csrf_a} &username=bob&action=add" ) ) .await;
cb71e18e Andy McCall 1 month ago ↶
171
assert! ( r.body.contains ( " <li><a href=\" /bob\" ><img class=\" avatar xs\" " ) , " {}" , r.body) ;
172
173
assert! ( r.body.contains ( " Assign people to this issue" ) & & r.body.contains ( " data-menu-filter" ) , " the assignees menu" ) ;
174
let r = post ( app, " /andy/demo/issue/1/assign" , Some ( & andy) , & format! ( " _csrf={csrf_a} &action=set&username=andy&username=bob" ) ) .await;
3b1bc27f Andy McCall 21 days ago
175
let assigned = | db : oxygenhub:: db:: Pool| async move {
cb71e18e Andy McCall 1 month ago ↶
176
let id: i64 = sqlx:: query_scalar( " SELECT id FROM issues WHERE number = 1" ) .fetch_one ( & db) .await.unwrap ( ) ;
177
let mut a = db:: issues:: assignees( & db, id) .await.unwrap ( ) ;
178
a.sort ( ) ;
179
a
180
} ;
181
assert_eq! ( assigned ( app.db.clone ( ) ) .await, [ " andy" , " bob" ] , " {}" , r.status) ;
182
post ( app, " /andy/demo/issue/1/assign" , Some ( & andy) , & format! ( " _csrf={csrf_a} &action=set&username=bob" ) ) .await;
183
assert_eq! ( assigned ( app.db.clone ( ) ) .await, [ " bob" ] , " set takes off who is not listed" ) ;
18e381ea Andy McCall 1 month ago ↶
184
let ms_id: i64 = sqlx:: query_scalar( " SELECT id FROM milestones WHERE title = 'v1'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
185
let r = post ( app, " /andy/demo/issue/1/milestone" , Some ( & andy) , & format! ( " _csrf={csrf_a} &milestone={ms_id} " ) ) .await;
186
assert! ( r.body.contains ( " >v1<" ) , " {}" , r.body) ;
cb71e18e Andy McCall 1 month ago ↶
187
assert! ( post ( app, " /andy/demo/issue/1/state" , Some ( & andy) , & format! ( " _csrf={csrf_a} &action=pin" ) ) .await.body.contains ( " Unpin issue" ) ) ;
18e381ea Andy McCall 1 month ago ↶
188
assert_eq! (
189
post (
190
app,
191
" /andy/demo/issue/1/state" ,
192
Some ( & bob) ,
193
& format! ( " _csrf={} &action=lock" , csrf_of ( & get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await.body) )
194
)
195
.await
196
.status,
197
StatusCode:: NOT_FOUND ,
198
" bob cannot lock"
199
) ;
200
201
let list = get ( app, " /andy/demo/issues?q=label:bug+assignee:bob+milestone:v1" , None ) .await;
202
assert! ( list.body.contains ( " Crash on start" ) , " {}" , list.body) ;
203
assert! ( ! get ( app, " /andy/demo/issues?q=label:feature" , None ) .await.body.contains ( " Crash on start" ) ) ;
204
assert! ( get ( app, " /andy/demo/milestones" , None ) .await.body.contains ( " 1 open, 0 closed" ) ) ;
205
206
207
let csrf_b = csrf_of ( & get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await.body) ;
208
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & bob) , & format! ( " _csrf={csrf_b} &body=Reproduced+with+the+flux+capacitor" ) ) .await;
209
assert! ( r.body.contains ( " flux capacitor" ) , " {}" , r.body) ;
210
assert! ( get ( app, " /notifications" , Some ( & andy) ) .await.body.contains ( " andy/demo#1" ) ) ;
211
let s = get ( app, " /search?q=capacitor" , None ) .await;
212
assert! ( s.body.contains ( " Crash on start" ) & & s.body.contains ( " <mark>capacitor</mark>" ) , " {}" , s.body) ;
213
let s = get ( app, " /search?q=demo" , None ) .await;
214
assert! ( s.body.contains ( " /andy/demo\" " ) , " repository hit: {}" , s.body) ;
215
216
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & bob) , & format! ( " _csrf={csrf_b} &body=closing&close=completed" ) ) .await;
217
assert! ( r.body.contains ( " >open<" ) | | r.body.contains ( " head\" >open" ) , " still open: {}" , r.body) ;
218
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=fixed&close=completed" ) ) .await;
219
assert! ( r.body.contains ( " closed" ) , " {}" , r.body) ;
220
assert! ( get ( app, " /andy/demo/issues?state=closed" , None ) .await.body.contains ( " Crash on start" ) ) ;
221
222
223
let csrf_a = csrf_of ( & get ( app, " /andy/demo/issues/new" , Some ( & andy) ) .await.body) ;
224
assert_eq! (
225
post ( app, " /andy/demo/issues/new" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=Typo&body=" ) ) .await.location.as_deref ( ) ,
226
Some ( " /andy/demo/issue/2" )
227
) ;
228
let work = tempfile:: tempdir( ) .unwrap ( ) ;
229
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
230
client.config.set ( " remote.origin.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
231
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
232
client.save_config ( ) .unwrap ( ) ;
233
write ( work.path ( ) , " a.txt" , b " a\n " ) ;
234
client.snapshot ( " t" ) .unwrap ( ) ;
235
client.commit ( " Fix the typo\n \n Closes #2" , & [ ] ) .unwrap ( ) ;
236
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
237
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
238
let issue2 = db:: issues:: get( & app.db, db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) .id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
239
assert_eq! ( issue2.state, " closed" , " closed by the landed change" ) ;
240
assert! ( get ( app, " /andy/demo/issue/2" , None ) .await.body.contains ( " closed by change" ) ) ;
241
242
243
client.bookmark_create ( " feat" , None ) .unwrap ( ) ;
244
client.bookmark_switch ( " feat" , false ) .unwrap ( ) ;
245
write ( work.path ( ) , " b.txt" , b " b\n " ) ;
246
client.snapshot ( " t" ) .unwrap ( ) ;
247
client.commit ( " feat" , & [ ] ) .unwrap ( ) ;
248
assert! ( client.push_bookmark ( " origin" , " feat" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
249
let csrf_a = csrf_of ( & get ( app, " /andy/demo/proposals/new?source=feat&target=main" , Some ( & andy) ) .await.body) ;
250
let r = post ( app, " /andy/demo/proposals/new" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=Feat&target=main&source=feat" ) ) .await;
251
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) , " {}" , r.body) ;
252
assert_eq! ( get ( app, " /andy/demo/issue/3" , None ) .await.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) ) ;
253
254
let r = post ( app, " /andy/demo/issue/2/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=see+%233" ) ) .await;
255
assert! ( r.body.contains ( " href=\" /andy/demo/proposal/3\" " ) , " {}" , r.body) ;
78001759 Andy McCall 1 month ago ↶
256
257
client.bookmark_switch ( " main" , false ) .unwrap ( ) ;
258
write ( work.path ( ) , " c.txt" , b " c\n " ) ;
259
client.snapshot ( " t" ) .unwrap ( ) ;
260
let r = client.commit ( " Tidy up (refs #1, see #3)" , & [ ] ) .unwrap ( ) ;
261
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
262
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
263
let short = & r.to_hex ( ) [ .. 12 ] ;
264
let one = get ( app, " /andy/demo/issue/1" , None ) .await.body;
265
assert! (
266
one.contains ( " referenced by change" ) & & one.contains ( & format! ( " href=\" /andy/demo/change/{short} \" " ) ) & & one.contains ( " on main" ) ,
267
" {one}"
268
) ;
269
let three = get ( app, " /andy/demo/proposal/3" , Some ( & andy) ) .await.body;
270
assert! ( three.contains ( " referenced by change" ) & & three.contains ( short) , " proposals too" ) ;
271
assert! ( ! get ( app, " /andy/demo/issue/2" , None ) .await.body.contains ( " referenced by change" ) , " a closing change records only that" ) ;
272
eb0c44a9 Andy McCall 1 month ago ↶
273
274
let part = get ( app, " /notifications?reason=participating" , Some ( & bob) ) .await.body;
275
assert! ( part.contains ( " andy/demo#1" ) & & ! part.contains ( " andy/demo#3: Feat" ) , " {part}" ) ;
276
let watch = get ( app, " /notifications?reason=watching" , Some ( & bob) ) .await.body;
277
assert! ( watch.contains ( " andy/demo#3: Feat" ) & & ! watch.contains ( " Crash on start" ) , " {watch}" ) ;
278
let props = get ( app, " /notifications?kind=proposal" , Some ( & bob) ) .await.body;
279
assert! ( props.contains ( " andy/demo#3: Feat" ) & & ! props.contains ( " andy/demo#1" ) , " kind filter" ) ;
280
assert! ( get ( app, " /notifications?repo=andy/demo" , Some ( & bob) ) .await.body.contains ( " andy/demo#3: Feat" ) ) ;
281
assert! ( get ( app, " /notifications?repo=nobody/none" , Some ( & bob) ) .await.body.contains ( " Nothing matches these filters" ) ) ;
282
283
let inbox = get ( app, " /notifications" , Some ( & bob) ) .await.body;
284
assert_eq! ( inbox.matches ( " data-row-thread=\" /andy/demo/issue/1\" " ) .count ( ) , 1 , " {inbox}" ) ;
285
assert! ( inbox.contains ( " updates" ) , " the count of a busy thread" ) ;
286
287
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
288
let newest: i64 = sqlx:: query_scalar(
3b1bc27f Andy McCall 21 days ago
289
" SELECT id FROM notifications WHERE account_id = $1 AND thread = '/andy/demo/proposal/3' ORDER BY id DESC LIMIT 1" ,
eb0c44a9 Andy McCall 1 month ago ↶
290
)
291
.bind ( bob_id)
292
.fetch_one ( & app.db)
293
.await
294
.unwrap ( ) ;
295
let before = db:: notify:: unread( & app.db, bob_id) .await.unwrap ( ) ;
296
let r = get ( app, & format! ( " /notifications/open/{newest} " ) , Some ( & bob) ) .await;
297
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) ) ;
298
assert! ( db:: notify:: unread( & app.db, bob_id) .await.unwrap ( ) < before) ;
299
assert_eq! ( get ( app, & format! ( " /notifications/open/{newest} " ) , Some ( & andy) ) .await.status, StatusCode:: NOT_FOUND , " only your own" ) ;
300
301
let csrf_b = csrf_of ( & get ( app, " /notifications" , Some ( & bob) ) .await.body) ;
302
let r = post (
303
app,
304
" /notifications/thread" ,
305
Some ( & bob) ,
306
& format! ( " _csrf={csrf_b} &thread=%2Fandy%2Fdemo%2Fissue%2F1&action=done&back=%2Fnotifications" ) ,
307
)
308
.await;
309
assert_eq! ( r.location.as_deref ( ) , Some ( " /notifications" ) ) ;
310
assert! ( ! get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) ) ;
311
assert! ( get ( app, " /notifications?view=done" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) ) ;
312
post ( app, " /andy/demo/issue/1/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=one+more+thing" ) ) .await;
313
assert! ( get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) , " back with the new comment" ) ;
314
let r = post (
315
app,
316
" /notifications/thread" ,
317
Some ( & bob) ,
318
& format! ( " _csrf={csrf_b} &thread=%2Fandy%2Fdemo%2Fissue%2F1&action=read&back=https%3A%2F%2Fevil.example" ) ,
319
)
320
.await;
321
assert_eq! ( r.location.as_deref ( ) , Some ( " /notifications" ) , " only back to the inbox" ) ;
322
18e381ea Andy McCall 1 month ago ↶
323
324
assert! ( get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " andy/demo#3: Feat" ) ) ;
325
let csrf_b = csrf_of ( & get ( app, " /notifications" , Some ( & bob) ) .await.body) ;
326
let r = post ( app, " /notifications/read" , Some ( & bob) , & format! ( " _csrf={csrf_b} " ) ) .await;
327
assert! ( r.body.contains ( " 0 unread" ) , " {}" , r.body) ;
328
329
let r = post ( app, " /settings/notifications" , Some ( & bob) , & format! ( " _csrf={csrf_b} &email=1" ) ) .await;
330
assert! ( r.body.contains ( " Email notifications are on" ) ) ;
331
let key: String = sqlx:: query_scalar( " SELECT unsubscribe_key FROM accounts WHERE username = 'bob'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
332
assert_eq! ( get ( app, & format! ( " /settings/notifications?unsubscribe={key} " ) , None ) .await.status, StatusCode:: OK ) ;
333
let on: bool = sqlx:: query_scalar( " SELECT email_notifications FROM accounts WHERE username = 'bob'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
334
assert! ( ! on) ;
335
}
25ab8c73 Andy McCall 1 month ago ↶
336
337
# [ tokio ::test ( flavor = " multi_thread" ) ]
338
async fn dashboard_shows_what_needs_the_viewer ( ) {
339
let h = hub ( ) .await;
340
let app = & h.app;
341
let andy = register ( app, " andy" ) .await;
342
let bob = register ( app, " bob" ) .await;
343
for ( name, vis) in [ ( " demo" , hub_api:: Visibility:: Public) , ( " secret" , hub_api:: Visibility:: Private) ] {
344
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
345
db:: repos:: create( & app.db, " andy" , name, " " , vis) .await.unwrap ( ) ;
346
}
347
let demo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
348
let secret = db:: repos:: get( & app.db, " andy" , " secret" ) .await.unwrap ( ) .unwrap ( ) ;
349
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
350
for ( repo, title) in [ ( demo.id, " Faster startup" ) , ( secret.id, " Hidden work" ) ] {
351
let p = db:: proposals:: create(
352
& app.db,
353
db:: proposals:: NewProposal {
354
repo_id: repo,
355
title,
356
body: " " ,
357
author_id: Some ( bob_id) ,
358
author: " bob" ,
359
draft: false ,
360
target: " main" ,
361
source_kind: " bookmark" ,
362
source_repo_id: Some ( repo) ,
363
source_bookmark: " fast" ,
364
source_changes: " " ,
365
} ,
366
)
367
.await
368
.unwrap ( ) ;
369
db:: proposals:: add_event( & app.db, p.id, " bob" , " opened" , " " ) .await.unwrap ( ) ;
370
}
371
372
373
let anon = get ( app, " /" , None ) .await;
374
assert! ( anon.body.contains ( " andy /" ) & & ! anon.body.contains ( " Dashboard <small>" ) , " {}" , anon.body) ;
375
376
377
let page = get ( app, " /" , Some ( & andy) ) .await;
378
assert_eq! ( page.status, StatusCode:: OK ) ;
f8cf54e9 Andy McCall 23 days ago ↶
379
assert! ( page.body.contains ( " Waiting for your review</h2><span class=\" count-chip\" >2</span>" ) , " {}" , page.body) ;
25ab8c73 Andy McCall 1 month ago ↶
380
assert! ( page.body.contains ( " /andy/demo/proposal/1" ) & & page.body.contains ( " /andy/secret/proposal/1" ) ) ;
381
assert! ( page.body.contains ( " Your repositories" ) ) ;
382
assert! ( page.body.contains ( " opened proposal" ) , " activity feed: {}" , page.body) ;
383
17ca6cdd Andy McCall 23 days ago ↶
384
385
386
assert! ( page.body.contains ( r # "<canvas id="backdrop" aria-hidden="true">"# ) ) ;
387
let start = page.body.find ( r # "id="backdrop-data">"# ) .unwrap ( ) + r # "id="backdrop-data">"# .len ( ) ;
388
let data: serde_json:: Value = serde_json:: from_str( & page.body[ start.. start + page.body[ start.. ] .find ( " </script>" ) .unwrap ( ) ] ) .unwrap ( ) ;
389
let repos: Vec< & str > = data[ " repos" ] .as_array ( ) .unwrap ( ) .iter ( ) .filter_map ( | r | r.as_str ( ) ) .collect ( ) ;
390
assert! ( repos.contains ( & " andy/demo" ) & & repos.contains ( & " andy/secret" ) , " {data}" ) ;
391
let events = data[ " events" ] .as_array ( ) .unwrap ( ) ;
392
assert! ( events.iter ( ) .any ( | e | e[ 1 ] = = 1 ) , " the proposals opened are marks: {data}" ) ;
393
assert! ( events.iter ( ) .all ( | e | e[ 0 ] .as_u64 ( ) .unwrap ( ) < repos.len ( ) as u64 ) ) ;
33bd0bd4 Andy McCall 23 days ago ↶
394
395
let res = oxygenhub:: router( app.clone ( ) )
396
.oneshot ( Request:: builder( ) .uri ( " /" ) .header ( header:: COOKIE , format! ( " oxyhub={andy} " ) ) .body ( Body:: empty( ) ) .unwrap ( ) )
397
.await
398
.unwrap ( ) ;
399
assert_eq! ( res.headers ( ) [ header:: CACHE_CONTROL ] , " private, no-store" ) ;
6246d1e5 Andy McCall 23 days ago ↶
400
401
assert! ( get ( app, " /login" , None )
402
.await
403
.body
404
.contains ( r # "<script type="application/json" id="backdrop-data">{"scene":"mark"}</script>"# ) ) ;
405
let lost = get ( app, " /no/such/page/here" , None ) .await;
406
assert_eq! ( lost.status, StatusCode:: NOT_FOUND ) ;
407
assert! ( lost.body.contains ( r # "{"scene":"adrift"}"# ) ) ;
0a3b244b Andy McCall 23 days ago ↶
408
409
let sky_of = | body : & str | {
410
let start = body.find ( r # "id="backdrop-data">"# ) .unwrap ( ) + r # "id="backdrop-data">"# .len ( ) ;
411
body[ start.. start + body[ start.. ] .find ( " </script>" ) .unwrap ( ) ] .to_string ( )
412
} ;
413
let hub_sky = sky_of ( & anon.body) ;
414
assert! ( hub_sky.contains ( " andy/demo" ) & & ! hub_sky.contains ( " andy/secret" ) , " {hub_sky}" ) ;
415
416
417
let explore = get ( app, " /explore" , Some ( & andy) ) .await;
418
assert! ( explore.body.contains ( " andy/secret" ) , " the list shows him his private repository" ) ;
419
let hub_sky = sky_of ( & explore.body) ;
420
assert! ( hub_sky.contains ( " andy/demo" ) & & ! hub_sky.contains ( " andy/secret" ) , " {hub_sky}" ) ;
421
assert! ( ! get ( app, " /andy" , None ) .await.body.contains ( " backdrop-data" ) ) ;
17ca6cdd Andy McCall 23 days ago ↶
422
423
let res = oxygenhub:: router( app.clone ( ) )
424
.oneshot ( Request:: builder( ) .uri ( " /static/backdrop/backdrop_bg.wasm" ) .body ( Body:: empty( ) ) .unwrap ( ) )
425
.await
426
.unwrap ( ) ;
427
assert_eq! ( res.headers ( ) [ header:: CONTENT_TYPE ] , " application/wasm" ) ;
428
assert! ( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .starts_with ( b " \0 asm" ) ) ;
da8f7826 Andy McCall 23 days ago ↶
429
430
assert! ( sky_of ( & page.body) .contains ( r # ""live":"mine""# ) & & hub_sky.contains ( r # ""live":"hub""# ) ) ;
431
let since = db:: now_ms( ) - 3_600_000 ;
432
let arrivals = | res : Res| -> Vec< String> {
433
assert_eq! ( res.status, StatusCode:: OK , " {}" , res.body) ;
434
let v: serde_json:: Value = serde_json:: from_str( & res.body) .unwrap ( ) ;
435
v[ " events" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | e | e[ 0 ] .as_str ( ) .unwrap ( ) .to_string ( ) ) .collect ( )
436
} ;
437
let mine = arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={since} " ) , Some ( & andy) ) .await) ;
438
assert! ( mine.iter ( ) .any ( | r | r = = " andy/demo" ) & & mine.iter ( ) .any ( | r | r = = " andy/secret" ) , " {mine:?}" ) ;
439
let hub = arrivals ( get ( app, & format! ( " /sky/arrivals?scene=hub&since={since} " ) , Some ( & andy) ) .await) ;
440
assert! ( hub.iter ( ) .any ( | r | r = = " andy/demo" ) & & ! hub.iter ( ) .any ( | r | r = = " andy/secret" ) , " the hub's sky is public only: {hub:?}" ) ;
441
let anon_hub = arrivals ( get ( app, & format! ( " /sky/arrivals?scene=hub&since={since} " ) , None ) .await) ;
442
assert! ( ! anon_hub.iter ( ) .any ( | r | r = = " andy/secret" ) , " {anon_hub:?}" ) ;
443
assert! ( arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={since} " ) , None ) .await) .is_empty ( ) , " signed out has no `mine`" ) ;
444
assert! (
445
arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={since} " ) , Some ( & bob) ) .await) .is_empty ( ) ,
446
" Bob's sky has none of Andy's"
447
) ;
448
assert! (
449
arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={} " , db:: now_ms( ) ) , Some ( & andy) ) .await) .is_empty ( ) ,
450
" nothing since now"
451
) ;
17ca6cdd Andy McCall 23 days ago ↶
452
25ab8c73 Andy McCall 1 month ago ↶
453
454
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
455
let p = db:: proposals:: get( & app.db, demo.id, 1 ) .await.unwrap ( ) .unwrap ( ) ;
456
db:: proposals:: add_review( & app.db, p.id, andy_id, " andy" , " approve" , " " ) .await.unwrap ( ) ;
f8cf54e9 Andy McCall 23 days ago ↶
457
assert! ( get ( app, " /" , Some ( & andy) ) .await.body.contains ( " Waiting for your review</h2><span class=\" count-chip\" >1</span>" ) ) ;
25ab8c73 Andy McCall 1 month ago ↶
458
459
460
let page = get ( app, " /" , Some ( & bob) ) .await;
33bd0bd4 Andy McCall 23 days ago ↶
461
462
463
let start = page.body.find ( r # "id="backdrop-data">"# ) .unwrap ( ) + r # "id="backdrop-data">"# .len ( ) ;
464
let sky = & page.body[ start.. start + page.body[ start.. ] .find ( " </script>" ) .unwrap ( ) ] ;
465
assert! ( ! sky.contains ( " andy/secret" ) & & ! sky.contains ( " andy/demo" ) , " {sky}" ) ;
f8cf54e9 Andy McCall 23 days ago ↶
466
assert! ( page.body.contains ( " Your proposals</h2><span class=\" count-chip\" >1</span>" ) , " {}" , page.body) ;
25ab8c73 Andy McCall 1 month ago ↶
467
assert! ( ! page.body.contains ( " Hidden work" ) ) ;
f8cf54e9 Andy McCall 23 days ago ↶
468
assert! ( page.body.contains ( " Waiting for your review</h2><span class=\" count-chip\" >0</span>" ) ) ;
25ab8c73 Andy McCall 1 month ago ↶
469
assert! ( page.body.contains ( " Faster startup" ) ) ;
470
let feed = & page.body[ page.body.find ( " Recent activity" ) .unwrap ( ) .. ] ;
471
assert! ( ! feed.contains ( " andy/secret" ) , " private activity leaked: {feed}" ) ;
472
473
474
assert! ( get ( app, " /explore" , Some ( & bob) ) .await.body.contains ( " andy /" ) ) ;
475
}
4edcf240 Andy McCall 1 month ago ↶
476
477
# [ tokio ::test ( flavor = " multi_thread" ) ]
478
async fn empty_repository_shows_a_quickstart ( ) {
479
let h = hub ( ) .await;
480
let app = & h.app;
481
let andy = register ( app, " andy" ) .await;
482
let bob = register ( app, " bob" ) .await;
483
app.repos.create ( & RepoName:: parse( " andy/fresh" ) .unwrap ( ) , None ) .unwrap ( ) ;
484
db:: repos:: create( & app.db, " andy" , " fresh" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
485
486
let page = get ( app, " /andy/fresh" , Some ( & andy) ) .await;
487
assert! ( page.body.contains ( " This repository is empty" ) , " {}" , page.body) ;
488
let clone_url = format! ( " {} /andy/fresh" , h.base) ;
489
assert! ( page.body.contains ( & format! ( " oxy remote add hub {clone_url} " ) ) , " {}" , page.body) ;
490
assert! ( page.body.contains ( " oxy config remote.hub.token YOUR-TOKEN" ) ) ;
491
assert! ( page.body.contains ( " oxy push hub" ) ) ;
492
assert! (
493
page.body.contains ( " mkdir fresh && cd fresh" ) | | page.body.contains ( " mkdir fresh && cd fresh" ) ,
494
" {}" ,
495
page.body
496
) ;
497
assert! ( page.body.contains ( " oxy git import --branch main" ) ) ;
498
assert! ( ! page.body.contains ( " oxyhub-ca.pem" ) , " no certificate step without a self-signed certificate" ) ;
499
500
501
let page = get ( app, " /andy/fresh" , Some ( & bob) ) .await;
502
assert! ( page.body.contains ( " Nothing has been pushed yet" ) & & ! page.body.contains ( " oxy push hub" ) , " {}" , page.body) ;
503
504
505
let work = tempfile:: tempdir( ) .unwrap ( ) ;
506
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
507
client.config.set ( " remote.hub.url" , & clone_url) .unwrap ( ) ;
508
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
509
client.save_config ( ) .unwrap ( ) ;
510
write ( work.path ( ) , " README.md" , b " # fresh\n " ) ;
511
client.commit ( " First change" , & [ ] ) .unwrap ( ) ;
512
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
513
let page = get ( app, " /andy/fresh" , Some ( & andy) ) .await;
514
assert! ( ! page.body.contains ( " This repository is empty" ) & & page.body.contains ( " README.md" ) , " {}" , page.body) ;
515
}
c40c2d3e Andy McCall 1 month ago ↶
516
517
# [ tokio ::test ( flavor = " multi_thread" ) ]
518
async fn theme_is_chosen_in_settings_and_remembered_in_a_cookie ( ) {
519
let h = hub ( ) .await;
520
let app = & h.app;
521
let andy = register ( app, " andy" ) .await;
522
let page = get ( app, " /settings" , Some ( & andy) ) .await;
523
assert! ( page.body.contains ( " Appearance" ) ) ;
d98a93c1 Andy McCall 1 month ago ↶
524
assert! ( page.body.contains ( r # "value="light" checked"# ) , " light by default: {}" , page.body) ;
c40c2d3e Andy McCall 1 month ago ↶
525
assert! ( page.body.contains ( " data-theme-toggle" ) , " the header has the switch" ) ;
526
let csrf = csrf_of ( & page.body) ;
d98a93c1 Andy McCall 1 month ago ↶
527
let r = post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=dark" ) ) .await;
c40c2d3e Andy McCall 1 month ago ↶
528
assert_eq! ( r.status, StatusCode:: SEE_OTHER ) ;
d98a93c1 Andy McCall 1 month ago ↶
529
assert_eq! ( r.cookie.as_deref ( ) , Some ( " dark" ) ) ;
c40c2d3e Andy McCall 1 month ago ↶
530
assert_eq! ( r.location.as_deref ( ) , Some ( " /settings" ) ) ;
d98a93c1 Andy McCall 1 month ago ↶
531
let page = send_with_theme ( app, & andy, " dark" ) .await;
532
assert! ( page.contains ( r # "value="dark" checked"# ) , " the form shows the cookie's theme" ) ;
c40c2d3e Andy McCall 1 month ago ↶
533
assert_eq! ( post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=purple" ) ) .await.status, StatusCode:: BAD_REQUEST ) ;
17ca6cdd Andy McCall 23 days ago ↶
534
535
536
537
assert! ( get ( app, " /settings" , Some ( & andy) ) .await.body.contains ( r # "name="backdrop" value="moving" checked"# ) ) ;
538
let res = oxygenhub:: router( app.clone ( ) )
539
.oneshot (
540
Request:: builder( )
541
.method ( " POST" )
542
.uri ( " /settings/appearance" )
543
.header ( header:: COOKIE , format! ( " oxyhub={andy} " ) )
544
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
545
.body ( Body:: from( format! ( " _csrf={csrf} &theme=light&backdrop=still" ) ) )
546
.unwrap ( ) ,
547
)
548
.await
549
.unwrap ( ) ;
550
let cookies: Vec< & str > = res.headers ( ) .get_all ( header:: SET_COOKIE ) .iter ( ) .filter_map ( | v | v.to_str ( ) .ok ( ) ) .collect ( ) ;
551
assert! (
552
cookies.iter ( ) .any ( | c | c.starts_with ( " theme=light;" ) ) & & cookies.iter ( ) .any ( | c | c.starts_with ( " backdrop=still;" ) ) ,
553
" {cookies:?}"
554
) ;
555
assert_eq! (
556
post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=light&backdrop=wild" ) ) .await.status,
557
StatusCode:: BAD_REQUEST
558
) ;
c40c2d3e Andy McCall 1 month ago ↶
559
assert_eq! ( post ( app, " /settings/appearance" , Some ( & andy) , " _csrf=nope&theme=light" ) .await.status, StatusCode:: BAD_REQUEST ) ;
560
}
561
562
async fn send_with_theme ( app : & Shared, session : & str , theme : & str ) -> String {
563
let req =
564
Request:: builder( ) .uri ( " /settings" ) .header ( header:: COOKIE , format! ( " oxyhub={session} ; theme={theme} " ) ) .body ( Body:: empty( ) ) .unwrap ( ) ;
565
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
566
String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( )
567
}
10174045 Andy McCall 1 month ago ↶
568
569
# [ tokio ::test ( flavor = " multi_thread" ) ]
570
async fn profile_and_organisation_pages ( ) {
571
let h = hub ( ) .await;
572
let app = & h.app;
573
let andy = register ( app, " andy" ) .await;
574
let bob = register ( app, " bob" ) .await;
575
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
576
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
577
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
578
db:: repos:: create( & app.db, " andy" , " demo" , " A demo" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
579
app.repos.create ( & RepoName:: parse( " andy/hidden" ) .unwrap ( ) , None ) .unwrap ( ) ;
580
db:: repos:: create( & app.db, " andy" , " hidden" , " " , hub_api:: Visibility:: Private) .await.unwrap ( ) ;
581
582
583
let csrf = csrf_of ( & get ( app, " /settings" , Some ( & andy) ) .await.body) ;
584
let r = post ( app, " /settings" , Some ( & andy) , & format! ( " _csrf={csrf} &display_name=Andy+McCall&email=a%40example.com&bio=Builds+%3Cb%3Eoxy%3C%2Fb%3E&website=javascript%3Aalert(1)&location=Earth" ) ) .await;
585
assert! ( r.body.contains ( " must be an http" ) , " {}" , r.body) ;
586
let r = post ( app, " /settings" , Some ( & andy) , & format! ( " _csrf={csrf} &display_name=Andy+McCall&email=a%40example.com&bio=Builds+%3Cb%3Eoxy%3C%2Fb%3E&website=https%3A%2F%2Fexample.org&location=Earth" ) ) .await;
587
assert! ( r.body.contains ( " Profile saved" ) , " {}" , r.body) ;
588
589
let org = db:: accounts:: create_org( & app.db, " retro" , " Retro Club" , andy_id) .await.unwrap ( ) ;
590
db:: accounts:: set_org_member( & app.db, org.id, bob_id, " member" ) .await.unwrap ( ) ;
591
let team = db:: accounts:: create_team( & app.db, org.id, " core" ) .await.unwrap ( ) ;
592
db:: accounts:: add_team_member( & app.db, team.id, bob_id) .await.unwrap ( ) ;
593
594
let page = get ( app, " /andy" , Some ( & bob) ) .await;
595
assert_eq! ( page.status, StatusCode:: OK ) ;
596
for want in [ " Andy McCall" , " Earth" , r # "href="https://example.org""# , " /avatar/andy" , " Signing keys" , " /retro" , " demo" ] {
597
assert! ( page.body.contains ( want) , " profile lacks {want}: {}" , page.body) ;
598
}
599
assert! ( ! page.body.contains ( " <b>oxy</b>" ) , " the bio is escaped" ) ;
600
assert! ( ! page.body.contains ( " [email protected] " ) , " the email stays private" ) ;
601
assert! ( ! page.body.contains ( " /andy/hidden" ) , " bob cannot see the private repository" ) ;
602
assert! ( ! page.body.contains ( " edit profile" ) , " only the owner gets the edit link" ) ;
603
assert! ( get ( app, " /andy" , Some ( & andy) ) .await.body.contains ( " /andy/hidden" ) , " andy sees his own private repository" ) ;
604
605
let page = get ( app, " /retro" , Some ( & bob) ) .await;
606
assert_eq! ( page.status, StatusCode:: OK ) ;
61ecbd29 Andy McCall 23 days ago ↶
607
for want in [ " Retro Club" , " organisation" , r # "Members <span class="count-chip">2</span>"# , " core" , " 1 member" ] {
10174045 Andy McCall 1 month ago ↶
608
assert! ( page.body.contains ( want) , " org page lacks {want}: {}" , page.body) ;
609
}
610
assert! ( ! page.body.contains ( " /orgs/retro/settings" ) , " bob is not an owner" ) ;
611
assert! ( get ( app, " /retro" , Some ( & andy) ) .await.body.contains ( " /orgs/retro/settings" ) ) ;
612
613
let svg = get ( app, " /avatar/andy" , None ) .await;
614
assert_eq! ( svg.status, StatusCode:: OK ) ;
615
assert! ( svg.body.starts_with ( " <svg" ) ) ;
616
assert_eq! ( svg.body, get ( app, " /avatar/ANDY" , None ) .await.body) ;
617
assert_eq! ( get ( app, " /nobody-here" , None ) .await.status, StatusCode:: NOT_FOUND ) ;
618
}
9df855f2 Andy McCall 1 month ago ↶
619
620
# [ tokio ::test ( flavor = " multi_thread" ) ]
621
async fn diffs_mark_words_hide_whitespace_and_collapse ( ) {
622
let h = hub ( ) .await;
623
let app = & h.app;
624
let andy = register ( app, " andy" ) .await;
625
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
626
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
627
let work = tempfile:: tempdir( ) .unwrap ( ) ;
628
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
629
client.config.set ( " remote.hub.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
630
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
631
client.save_config ( ) .unwrap ( ) ;
632
write ( work.path ( ) , " main.rs" , b " fn main() {\n setup();\n let answer = 42;\n }\n " ) ;
633
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
634
write ( work.path ( ) , " main.rs" , b " fn main() {\n setup();\n let answer = 43;\n }\n " ) ;
635
let second = client.commit ( " reindent and change the answer" , & [ ] ) .unwrap ( ) ;
636
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
637
638
let url = format! ( " /andy/demo/change/{} " , second.to_hex ( ) ) ;
639
let page = get ( app, & url, Some ( & andy) ) .await;
640
assert_eq! ( page.status, StatusCode:: OK ) ;
641
assert! ( page.body.contains ( r # "<details class="file-diff""# ) , " files collapse: {}" , page.body) ;
642
assert! ( page.body.contains ( r # "<span class="wx">43</span>"# ) , " the changed word is marked: {}" , page.body) ;
643
assert! ( page.body.contains ( " hide whitespace changes" ) ) ;
644
assert! ( page.body.contains ( " data-diff-mode=\" split\" " ) ) ;
645
let adds = | b : & str | b.matches ( r # "<tr class="add">"# ) .count ( ) ;
646
assert_eq! ( adds ( & page.body) , 2 , " re-indent and answer both changed" ) ;
647
648
let quiet = get ( app, & format! ( " {url} ?w=1" ) , Some ( & andy) ) .await;
649
assert_eq! ( adds ( & quiet.body) , 1 , " only the answer changed ignoring whitespace: {}" , quiet.body) ;
650
assert! ( quiet.body.contains ( " show whitespace changes" ) ) ;
651
652
assert! ( quiet.body.contains ( " ?w=0" ) , " the toggle leads back" ) ;
653
}
07b1a5bb Andy McCall 1 month ago ↶
654
655
# [ tokio ::test ( flavor = " multi_thread" ) ]
656
async fn fork_and_propose_upstream ( ) {
657
let h = hub ( ) .await;
658
let app = & h.app;
659
let andy = register ( app, " andy" ) .await;
660
let bob = register ( app, " bob" ) .await;
661
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
662
db:: repos:: create( & app.db, " andy" , " demo" , " The demo" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
663
let work = tempfile:: tempdir( ) .unwrap ( ) ;
664
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
665
client.config.set ( " remote.hub.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
666
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
667
client.save_config ( ) .unwrap ( ) ;
668
write ( work.path ( ) , " README.md" , b " # demo\n " ) ;
669
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
670
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
671
672
673
let page = get ( app, " /andy/demo" , Some ( & bob) ) .await;
674
assert! ( page.body.contains ( " /andy/demo/fork" ) , " a fork button: {}" , page.body) ;
675
let csrf = csrf_of ( & get ( app, " /andy/demo/fork" , Some ( & bob) ) .await.body) ;
676
let r = post ( app, " /andy/demo/fork" , Some ( & bob) , & format! ( " _csrf={csrf} &owner=andy&name=other" ) ) .await;
677
assert! ( r.body.contains ( " cannot create repositories under that owner" ) , " {}" , r.body) ;
678
let r = post ( app, " /andy/demo/fork" , Some ( & bob) , & format! ( " _csrf={csrf} &owner=bob&name=demo" ) ) .await;
679
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
680
assert_eq! ( r.location.as_deref ( ) , Some ( " /bob/demo" ) ) ;
681
let forked = get ( app, " /bob/demo" , Some ( & bob) ) .await;
682
assert! ( forked.body.contains ( " README.md" ) & & forked.body.contains ( " forked from" ) , " {}" , forked.body) ;
683
assert! ( forked.body.contains ( " propose to andy/demo" ) ) ;
684
assert! ( get ( app, " /andy/demo" , None ) .await.body.contains ( " 1 fork" ) ) ;
685
assert! ( get ( app, " /andy/demo/forks" , None ) .await.body.contains ( " /bob/demo" ) ) ;
686
let fork_row = db:: repos:: get( & app.db, " bob" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
687
let up_row = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
688
assert_eq! ( fork_row.forked_from, Some ( up_row.id) ) ;
689
assert_eq! ( fork_row.description, " The demo" ) ;
690
let fork_repo = app.repos.open ( & RepoName:: parse( " bob/demo" ) .unwrap ( ) ) .unwrap ( ) ;
691
let up_repo = app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
692
assert_ne! ( fork_repo.id ( ) .unwrap ( ) , up_repo.id ( ) .unwrap ( ) , " a fork is a repository of its own" ) ;
693
assert_eq! ( fork_repo.bookmark_heads ( ) .unwrap ( ) , up_repo.bookmark_heads ( ) .unwrap ( ) ) ;
694
assert_eq! ( fork_repo.fsck ( ) .unwrap ( ) .errors ( ) , 0 ) ;
695
696
697
let bw = tempfile:: tempdir( ) .unwrap ( ) ;
698
let mut mine = Repo:: clone( & format! ( " {} /bob/demo" , h.base) , & bw.path ( ) .join ( " c" ) ) .unwrap ( ) ;
699
mine.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
700
mine.save_config ( ) .unwrap ( ) ;
701
mine.bookmark_create ( " feat" , None ) .unwrap ( ) ;
702
mine.bookmark_switch ( " feat" , false ) .unwrap ( ) ;
703
write ( & bw.path ( ) .join ( " c" ) , " feature.txt" , b " new\n " ) ;
704
mine.commit ( " Add a feature" , & [ ] ) .unwrap ( ) ;
705
assert! ( mine.push_bookmark ( " origin" , " feat" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
706
let csrf = csrf_of ( & get ( app, " /andy/demo/proposals/new?source=bob/demo:feat" , Some ( & bob) ) .await.body) ;
707
let r =
708
post ( app, " /andy/demo/proposals/new" , Some ( & bob) , & format! ( " _csrf={csrf} &title=From+my+fork&target=main&source=bob%2Fdemo%3Afeat" ) )
709
.await;
710
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
711
let p = get ( app, & r.location.unwrap ( ) , Some ( & andy) ) .await;
712
assert! ( p.body.contains ( " From my fork" ) & & p.body.contains ( " fork bob/demo" ) , " {}" , p.body) ;
713
let files = get ( app, " /andy/demo/proposal/1?tab=files" , Some ( & andy) ) .await;
714
assert! ( files.body.contains ( " feature.txt" ) , " {}" , files.body) ;
715
}
1553bfc9 Andy McCall 1 month ago ↶
716
717
# [ tokio ::test ( flavor = " multi_thread" ) ]
718
async fn stars_topics_and_explore ( ) {
719
let h = hub ( ) .await;
720
let app = & h.app;
721
let andy = register ( app, " andy" ) .await;
722
let bob = register ( app, " bob" ) .await;
723
for ( name, desc) in [ ( " alpha" , " An emulator" ) , ( " beta" , " A compiler" ) , ( " gamma" , " Retro games" ) ] {
724
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
725
db:: repos:: create( & app.db, " andy" , name, desc, hub_api:: Visibility:: Public) .await.unwrap ( ) ;
726
}
727
728
729
let csrf = csrf_of ( & get ( app, " /andy/gamma/settings" , Some ( & andy) ) .await.body) ;
730
let r = post (
731
app,
732
" /andy/gamma/settings" ,
733
Some ( & andy) ,
734
& format! ( " _csrf={csrf} &description=Retro+games&default_bookmark=main&visibility=public&topics=c%2B%2B" ) ,
735
)
736
.await;
737
assert! ( r.body.contains ( " is not a topic" ) , " {}" , r.body) ;
738
let r = post (
739
app,
740
" /andy/gamma/settings" ,
741
Some ( & andy) ,
742
& format! ( " _csrf={csrf} &description=Retro+games&default_bookmark=main&visibility=public&topics=Retro%2C+6502" ) ,
743
)
744
.await;
745
assert! ( r.body.contains ( " Saved" ) , " {}" , r.body) ;
746
assert_eq! ( db:: repos:: get( & app.db, " andy" , " gamma" ) .await.unwrap ( ) .unwrap ( ) .topics, " retro 6502" ) ;
747
let page = get ( app, " /andy/gamma" , None ) .await;
748
assert! ( page.body.contains ( r # "href="/explore?topic=retro""# ) , " {}" , page.body) ;
749
750
751
for ( who, repo) in [ ( & bob, " beta" ) , ( & bob, " gamma" ) , ( & andy, " gamma" ) , ( & andy, " alpha" ) ] {
752
let csrf = csrf_of ( & get ( app, & format! ( " /andy/{repo} " ) , Some ( who) ) .await.body) ;
753
assert_eq! ( post ( app, & format! ( " /andy/{repo} /star" ) , Some ( who) , & format! ( " _csrf={csrf} " ) ) .await.status, StatusCode:: SEE_OTHER ) ;
754
}
755
let csrf = csrf_of ( & get ( app, " /andy/alpha" , Some ( & andy) ) .await.body) ;
756
post ( app, " /andy/alpha/star" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
757
assert! ( get ( app, " /andy/gamma" , Some ( & bob) ) .await.body.contains ( " ★ UNSTAR" ) ) ;
7f283f8e Andy McCall 23 days ago ↶
758
assert! ( get ( app, " /andy/gamma" , None ) .await.body.contains ( r # "title="2 stars">"# ) ) ;
759
assert! ( get ( app, " /andy/alpha" , None ) .await.body.contains ( r # "title="0 stars">"# ) , " unstarred again" ) ;
1553bfc9 Andy McCall 1 month ago ↶
760
761
762
let order = | b : & str | {
763
[ " alpha" , " beta" , " gamma" ]
764
.into_iter ( )
765
.map ( | n | ( b.find ( & format! ( " /andy/{n} \" " ) ) .unwrap_or ( usize :: MAX ) , n) )
766
.collect:: < std:: collections:: BTreeMap< _ , _ > > ( )
767
.into_values ( )
768
.collect:: < Vec< _ > > ( )
769
} ;
770
assert_eq! ( order ( & get ( app, " /explore?sort=stars" , None ) .await.body) [ .. 2 ] , [ " gamma" , " beta" ] ) ;
771
assert_eq! ( order ( & get ( app, " /explore?sort=name" , None ) .await.body) , [ " alpha" , " beta" , " gamma" ] ) ;
772
let filtered = get ( app, " /explore?q=compiler" , None ) .await.body;
773
assert! ( filtered.contains ( " /andy/beta\" " ) & & ! filtered.contains ( " /andy/alpha\" " ) , " {filtered}" ) ;
774
let by_topic = get ( app, " /explore?topic=6502" , None ) .await.body;
775
assert! ( by_topic.contains ( " /andy/gamma\" " ) & & ! by_topic.contains ( " /andy/beta\" " ) , " {by_topic}" ) ;
776
}
1ac84c62 Andy McCall 1 month ago ↶
777
778
# [ tokio ::test ( flavor = " multi_thread" ) ]
779
async fn lists_filter_sort_page_and_count ( ) {
780
let h = hub ( ) .await;
781
let app = & h.app;
782
let andy = register ( app, " andy" ) .await;
783
let _bob = register ( app, " bob" ) .await;
784
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
785
let repo = db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
786
for n in 0 .. 30 {
787
let author = if n % 3 = = 0 { " bob" } else { " andy" } ;
788
db:: issues:: create(
789
& app.db,
790
db:: issues:: NewIssue { repo_id: repo.id, title: & format! ( " Issue number {n} " ) , body: " " , author_id: None , author } ,
791
)
792
.await
793
.unwrap ( ) ;
794
}
795
let ui = db:: issues:: upsert_label( & app.db, repo.id, " ui" , " #00f0ff" , " " ) .await.unwrap ( ) ;
796
for ( title, author, target) in [ ( " Faster menus" , " bob" , " main" ) , ( " New theme" , " andy" , " main" ) , ( " Release prep" , " andy" , " stable" ) ] {
797
let p = db:: proposals:: create(
798
& app.db,
799
db:: proposals:: NewProposal {
800
repo_id: repo.id,
801
title,
802
body: " " ,
803
author_id: None ,
804
author,
805
draft: false ,
806
target,
807
source_kind: " changes" ,
808
source_repo_id: None ,
809
source_bookmark: " " ,
810
source_changes: " " ,
811
} ,
812
)
813
.await
814
.unwrap ( ) ;
815
if title = = " New theme" {
816
db:: issues:: set_proposal_label( & app.db, p.id, ui.id, true ) .await.unwrap ( ) ;
817
}
818
}
819
820
821
let page = get ( app, " /andy/demo/issues" , Some ( & andy) ) .await;
822
assert! ( page.body.contains ( r # "Issues <span class="count-chip">30</span>"# ) , " {}" , page.body) ;
823
assert! ( page.body.contains ( r # "Proposals <span class="count-chip">3</span>"# ) ) ;
824
825
826
let rows = | b : & str | b.matches ( " /andy/demo/issue/" ) .count ( ) ;
827
assert_eq! ( rows ( & page.body) , 25 ) ;
828
assert! ( page.body.contains ( " page 1 of 2" ) & & page.body.contains ( " page=2" ) , " a pager" ) ;
829
let second = get ( app, " /andy/demo/issues?page=2" , Some ( & andy) ) .await;
830
assert_eq! ( rows ( & second.body) , 5 ) ;
831
assert! ( second.body.contains ( " Issue number 0<" ) , " the oldest are last" ) ;
832
let oldest = get ( app, " /andy/demo/issues?sort=oldest" , Some ( & andy) ) .await;
833
assert! ( oldest.body.find ( " Issue number 0<" ) .unwrap ( ) < oldest.body.find ( " Issue number 1<" ) .unwrap ( ) ) ;
834
let bobs = get ( app, " /andy/demo/issues?q=author:bob" , Some ( & andy) ) .await;
835
assert_eq! ( rows ( & bobs.body) , 10 , " {}" , bobs.body) ;
836
837
838
let by_bob = get ( app, " /andy/demo/proposals?q=author:bob" , Some ( & andy) ) .await.body;
839
assert! ( by_bob.contains ( " Faster menus" ) & & ! by_bob.contains ( " New theme" ) ) ;
840
let ui_only = get ( app, " /andy/demo/proposals?q=label:ui" , Some ( & andy) ) .await.body;
841
assert! ( ui_only.contains ( " New theme" ) & & ! ui_only.contains ( " Faster menus" ) & & ui_only.contains ( r # "class="label""# ) ) ;
842
let stable = get ( app, " /andy/demo/proposals?q=target:stable" , Some ( & andy) ) .await.body;
843
assert! ( stable.contains ( " Release prep" ) & & ! stable.contains ( " New theme" ) ) ;
844
}
e1ec9fbf Andy McCall 1 month ago ↶
845
846
# [ tokio ::test ( flavor = " multi_thread" ) ]
847
async fn project_boards ( ) {
848
let h = hub ( ) .await;
849
let app = & h.app;
850
let andy = register ( app, " andy" ) .await;
851
let bob = register ( app, " bob" ) .await;
852
for ( name, vis) in [ ( " demo" , hub_api:: Visibility:: Public) , ( " secret" , hub_api:: Visibility:: Private) ] {
853
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
854
let row = db:: repos:: create( & app.db, " andy" , name, " " , vis) .await.unwrap ( ) ;
855
db:: issues:: create(
856
& app.db,
857
db:: issues:: NewIssue { repo_id: row.id, title: & format! ( " {name} issue" ) , body: " " , author_id: None , author: " andy" } ,
858
)
859
.await
860
.unwrap ( ) ;
861
}
a024e800 Andy McCall 27 days ago ↶
862
863
864
let home = get ( app, " /andy/demo" , Some ( & andy) ) .await.body;
865
assert! (
c9aacc0e Andy McCall 24 days ago ↶
866
home.contains ( " href=\" /projects/andy\" ><svg class=\" ic\" " ) & & home.contains ( " title=\" andy's project boards\" >Projects</a>" ) ,
a024e800 Andy McCall 27 days ago ↶
867
" site bar and tab"
868
) ;
869
let list = get ( app, " /projects/andy" , Some ( & andy) ) .await.body;
870
assert! ( list.contains ( " href=\" #new\" >+ NEW PROJECT</a>" ) & & list.contains ( " id=\" new\" " ) , " the button and the form" ) ;
871
assert! ( get ( app, " /projects/andy" , Some ( & bob) ) .await.body.contains ( " Only andy" ) , " who can make one" ) ;
872
let csrf = csrf_of ( & list) ;
e1ec9fbf Andy McCall 1 month ago ↶
873
let r = post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap&description=Next+steps" ) ) .await;
874
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1" ) , " {}" , r.body) ;
7b26e611 Andy McCall 26 days ago ↶
875
876
let list = get ( app, " /projects/andy" , Some ( & andy) ) .await.body;
877
assert! (
878
list.contains ( " >Roadmap</a>" ) & & list.contains ( " Next steps" ) & & list.contains ( " Open <span class=\" count-chip\" >1</span>" ) ,
879
" {list}"
880
) ;
881
let filtered = get ( app, " /projects/andy?q=roadm" , Some ( & andy) ) .await.body;
882
assert! ( filtered.contains ( " >Roadmap</a>" ) , " a filter matches part of the title, any case" ) ;
883
let none = get ( app, " /projects/andy?q=nothing+like+it" , Some ( & andy) ) .await.body;
884
assert! ( none.contains ( " No open project matches" ) & & ! none.contains ( " >Roadmap</a>" ) , " {none}" ) ;
885
let closed = get ( app, " /projects/andy?state=closed" , Some ( & andy) ) .await.body;
886
assert! ( closed.contains ( " No closed projects." ) & & closed.contains ( " Closed <span class=\" count-chip\" >0</span>" ) , " {closed}" ) ;
e1ec9fbf Andy McCall 1 month ago ↶
887
let board = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
888
for col in [ " Backlog" , " Doing" , " Done" ] {
889
assert! ( board.contains ( & format! ( " >{col} <span" ) ) , " {col}" ) ;
890
}
891
let cols: Vec< i64 > = sqlx:: query_scalar( " SELECT id FROM project_columns ORDER BY position" ) .fetch_all ( & app.db) .await.unwrap ( ) ;
892
let act = | form : String| {
893
let andy = andy.clone ( ) ;
894
async move { post ( app, " /projects/andy/1" , Some ( & andy) , & form) .await }
895
} ;
896
897
assert_eq! (
898
act ( format! ( " _csrf={csrf} &action=add&column={} &reference=demo%231" , cols[ 0 ] ) ) .await.location.as_deref ( ) ,
899
Some ( " /projects/andy/1" )
900
) ;
901
act ( format! ( " _csrf={csrf} &action=add&column={} &repo=secret&reference=%231" , cols[ 0 ] ) ) .await;
902
act ( format! ( " _csrf={csrf} &action=add&column={} ¬e=Write+the+docs" , cols[ 1 ] ) ) .await;
903
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} &reference=demo%231" , cols[ 1 ] ) ) .await.body.contains ( " already on this project" ) ) ;
904
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} &reference=demo%2399" , cols[ 0 ] ) ) .await.body.contains ( " no issue or proposal #99" ) ) ;
905
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} &reference=nothere%231" , cols[ 0 ] ) ) .await.body.contains ( " no repository nothere" ) ) ;
906
907
let demo_card: i64 =
908
sqlx:: query_scalar( " SELECT c.id FROM project_cards c JOIN repositories r ON r.id = c.repo_id WHERE r.name = 'demo'" )
909
.fetch_one ( & app.db)
910
.await
911
.unwrap ( ) ;
912
act ( format! ( " _csrf={csrf} &action=move&card={demo_card} &column={} " , cols[ 2 ] ) ) .await;
913
let issue = get ( app, " /andy/demo/issue/1" , None ) .await.body;
914
assert! ( issue.contains ( " href=\" /projects/andy/1\" >Roadmap</a> <span class=\" mute\" >· Done" ) , " the issue shows where it is" ) ;
915
916
917
let anon = get ( app, " /projects/andy/1" , None ) .await.body;
918
assert! ( anon.contains ( " demo issue" ) & & anon.contains ( " Write the docs" ) & & ! anon.contains ( " secret issue" ) , " {anon}" ) ;
919
assert! ( ! anon.contains ( " name=\" action\" " ) , " and cannot change anything" ) ;
920
assert! ( get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body.contains ( " secret issue" ) ) ;
921
922
let csrf_b = csrf_of ( & get ( app, " /settings" , Some ( & bob) ) .await.body) ;
923
assert_eq! (
924
post ( app, " /projects/andy/1" , Some ( & bob) , & format! ( " _csrf={csrf_b} &action=remove&card={demo_card} " ) ) .await.status,
925
StatusCode:: NOT_FOUND
926
) ;
927
act ( format! ( " _csrf={csrf} &action=settings&title=Roadmap&description=&private=1" ) ) .await;
928
assert_eq! ( get ( app, " /projects/andy/1" , Some ( & bob) ) .await.status, StatusCode:: NOT_FOUND ) ;
929
assert! ( ! get ( app, " /projects/andy" , None ) .await.body.contains ( " Roadmap" ) , " not even listed" ) ;
930
assert! ( ! get ( app, " /andy/demo/issue/1" , None ) .await.body.contains ( " Roadmap" ) , " nor on the issue" ) ;
931
932
933
act ( format! ( " _csrf={csrf} &action=remove_column&column={} " , cols[ 1 ] ) ) .await;
934
let note_col: i64 = sqlx:: query_scalar( " SELECT column_id FROM project_cards WHERE kind = 'note'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
935
assert_eq! ( note_col, cols[ 0 ] ) ;
936
act ( format! ( " _csrf={csrf} &action=remove_column&column={} " , cols[ 2 ] ) ) .await;
937
assert! ( act ( format! ( " _csrf={csrf} &action=remove_column&column={} " , cols[ 0 ] ) ) .await.body.contains ( " at least one column" ) ) ;
938
939
act ( format! ( " _csrf={csrf} &action=settings&title=Roadmap&closed=1" ) ) .await;
940
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} ¬e=late" , cols[ 0 ] ) ) .await.body.contains ( " This project is closed" ) ) ;
941
let r = act ( format! ( " _csrf={csrf} &action=delete" ) ) .await;
942
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy" ) ) ;
943
assert_eq! ( get ( app, " /projects/andy/1" , Some ( & andy) ) .await.status, StatusCode:: NOT_FOUND ) ;
944
assert_eq! ( get ( app, " /andy/demo/issue/1" , None ) .await.status, StatusCode:: OK ) ;
945
assert_eq! ( get ( app, " /projects/nobody" , None ) .await.status, StatusCode:: NOT_FOUND ) ;
946
}
c79a1111 Andy McCall 1 month ago ↶
947
948
# [ tokio ::test ( flavor = " multi_thread" ) ]
949
async fn issue_markdown_follows_the_default_bookmark ( ) {
950
let h = hub ( ) .await;
951
let app = & h.app;
952
register ( app, " andy" ) .await;
953
app.repos.create ( & RepoName:: parse( " andy/trunky" ) .unwrap ( ) , None ) .unwrap ( ) ;
954
let row = db:: repos:: create( & app.db, " andy" , " trunky" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
3b1bc27f Andy McCall 21 days ago
955
sqlx:: query( " UPDATE repositories SET default_bookmark = 'trunk' WHERE id = $1" ) .bind ( row.id) .execute ( & app.db) .await.unwrap ( ) ;
c79a1111 Andy McCall 1 month ago ↶
956
db:: issues:: create(
957
& app.db,
958
db:: issues:: NewIssue {
959
repo_id: row.id,
960
title: " Docs" ,
961
body: " See [the guide](docs/guide.md) and ." ,
962
author_id: None ,
963
author: " andy" ,
964
} ,
965
)
966
.await
967
.unwrap ( ) ;
968
let page = get ( app, " /andy/trunky/issue/1" , None ) .await.body;
969
assert! ( page.contains ( " href=\" /andy/trunky/file/trunk/docs/guide.md\" " ) , " {page}" ) ;
970
assert! ( page.contains ( " src=\" /andy/trunky/raw/trunk/img/a.png\" " ) ) ;
971
assert! ( ! page.contains ( " /file/main/" ) ) ;
972
}
b1e92a62 Andy McCall 26 days ago ↶
973
974
975
976
977
# [ tokio ::test ( flavor = " multi_thread" ) ]
978
async fn a_project_transfers_to_an_organisation ( ) {
979
let h = hub ( ) .await;
980
let app = & h.app;
981
982
983
let _first = register ( app, " ada" ) .await;
984
let andy = register ( app, " andy" ) .await;
985
let _bob = register ( app, " bob" ) .await;
986
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
987
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
988
db:: accounts:: create_org( & app.db, " retro" , " Retro Club" , andy_id) .await.unwrap ( ) ;
989
db:: accounts:: create_org( & app.db, " other" , " Someone else's" , bob_id) .await.unwrap ( ) ;
990
let list = get ( app, " /projects/andy" , Some ( & andy) ) .await.body;
991
let csrf = csrf_of ( & list) ;
992
let r = post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
993
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1" ) ) ;
994
post ( app, " /projects/andy/1" , Some ( & andy) , & format! ( " _csrf={csrf} &action=add¬e=Keep+me" ) ) .await;
995
996
997
let board = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
998
assert! ( board.contains ( " <option value=\" retro\" >retro</option>" ) & & ! board.contains ( " value=\" other\" " ) , " {board}" ) ;
999
let refused = post ( app, " /projects/andy/1" , Some ( & andy) , & format! ( " _csrf={csrf} &action=transfer&to=other" ) ) .await;
1000
assert! ( refused.body.contains ( " You cannot make projects under other" ) , " {}" , refused.body) ;
1001
1002
let r = post ( app, " /projects/andy/1" , Some ( & andy) , & format! ( " _csrf={csrf} &action=transfer&to=retro" ) ) .await;
1003
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/retro/1" ) , " {}" , r.body) ;
1004
let moved = get ( app, " /projects/retro/1" , Some ( & andy) ) .await.body;
1005
assert! ( moved.contains ( " Roadmap" ) & & moved.contains ( " Keep me" ) , " the cards come too" ) ;
1006
let old = get ( app, " /projects/andy/1" , Some ( & andy) ) .await;
1007
assert_eq! ( old.status, StatusCode:: PERMANENT_REDIRECT ) ;
1008
assert_eq! ( old.location.as_deref ( ) , Some ( " /projects/retro/1" ) ) ;
1009
1010
1011
let r = post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Fresh" ) ) .await;
1012
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1" ) ) ;
1013
let fresh = get ( app, " /projects/andy/1" , Some ( & andy) ) .await;
1014
assert_eq! ( fresh.status, StatusCode:: OK ) ;
1015
assert! ( fresh.body.contains ( " Fresh" ) ) ;
1016
}
d7d18246 Andy McCall 23 days ago ↶
1017
1018
1019
1020
# [ tokio ::test ( flavor = " multi_thread" ) ]
1021
async fn backups_are_set_up_in_settings ( ) {
1022
let h = hub ( ) .await;
1023
let app = & h.app;
1024
let andy = register ( app, " andy" ) .await;
1025
let page = get ( app, " /settings/backups" , Some ( & andy) ) .await;
1026
assert_eq! ( page.status, StatusCode:: OK ) ;
1027
assert! ( page.body.contains ( " href=\" /settings/backups\" class=\" on\" " ) & & page.body.contains ( " No runs yet." ) , " {}" , page.body) ;
1028
let csrf = csrf_of ( & page.body) ;
1029
1030
let bad = post (
1031
app,
1032
" /settings/backups" ,
1033
Some ( & andy) ,
1034
& format! ( " _csrf={csrf} &every=hourly&destination=gcs&bucket=andy-backup&provider=nope&keep_days=30" ) ,
1035
)
1036
.await;
1037
assert! ( bad.body.contains ( " workload identity provider" ) , " {}" , bad.body) ;
1038
let dir =
1039
post ( app, " /settings/backups" , Some ( & andy) , & format! ( " _csrf={csrf} &every=hourly&destination=dir&dir=%2Ftmp&keep_days=30" ) ) .await;
1040
assert! ( dir.body.contains ( " Pick one of the directories this hub allows." ) ) ;
1041
let provider =
1042
" %2F%2Fiam.googleapis.com%2Fprojects%2F1%2Flocations%2Fglobal%2FworkloadIdentityPools%2Foxygenhub%2Fproviders%2Foxygenhub-io" ;
1043
let ok = post (
1044
app,
1045
" /settings/backups" ,
1046
Some ( & andy) ,
1047
& format! ( " _csrf={csrf} &every=hourly&destination=gcs&bucket=gs%3A%2F%2Fandy-backup&provider={provider} &keep_days=7" ) ,
1048
)
1049
.await;
1050
assert! ( ok.body.contains ( " Saved." ) , " {}" , ok.body) ;
1051
let s = oxygenhub:: backups:: setting( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
1052
assert_eq! ( ( s.target.as_str ( ) , s.keep_days, s.every.as_str ( ) ) , ( " andy-backup" , 7 , " hourly" ) ) ;
7b9f4570 Andy McCall 23 days ago ↶
1053
1054
let bad = post (
1055
app,
1056
" /settings/backups" ,
1057
Some ( & andy) ,
1058
& format! ( " _csrf={csrf} &every=hourly&destination=s3&s3_bucket=andy-backup&s3_region=eu-west-2&s3_role=admin&keep_days=30" ) ,
1059
)
1060
.await;
1061
assert! ( bad.body.contains ( " the IAM role" ) , " {}" , bad.body) ;
1062
let role = " arn%3Aaws%3Aiam%3A%3A123456789012%3Arole%2Foxygenhub-backup" ;
1063
let ok = post (
1064
app,
1065
" /settings/backups" ,
1066
Some ( & andy) ,
1067
& format! (
1068
" _csrf={csrf} &every=daily&destination=s3&s3_bucket=s3%3A%2F%2Fandy-backup&s3_region=eu-west-2&s3_role={role} &keep_days=30"
1069
) ,
1070
)
1071
.await;
1072
assert! ( ok.body.contains ( " Saved." ) & & ok.body.contains ( r # "value="s3" checked"# ) , " {}" , ok.body) ;
1073
let s = oxygenhub:: backups:: setting( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
1074
assert_eq! ( ( s.destination.as_str ( ) , s.target.as_str ( ) , s.region.as_str ( ) ) , ( " s3" , " andy-backup" , " eu-west-2" ) ) ;
1075
assert_eq! ( s.provider, " arn:aws:iam::123456789012:role/oxygenhub-backup" ) ;
1076
1077
let ok = post (
1078
app,
1079
" /settings/backups" ,
1080
Some ( & andy) ,
1081
& format! ( " _csrf={csrf} &every=hourly&destination=gcs&bucket=gs%3A%2F%2Fandy-backup&provider={provider} &keep_days=7" ) ,
1082
)
1083
.await;
1084
assert! ( ok.body.contains ( " Saved." ) ) ;
d7d18246 Andy McCall 23 days ago ↶
1085
1086
let bob = register ( app, " bob" ) .await;
1087
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
1088
db:: accounts:: create_org( & app.db, " club" , " Club" , andy_id) .await.unwrap ( ) ;
1089
assert_eq! ( get ( app, " /orgs/club/settings/backups" , Some ( & bob) ) .await.status, StatusCode:: NOT_FOUND ) ;
1090
let club = get ( app, " /orgs/club/settings/backups" , Some ( & andy) ) .await;
1091
assert! ( club.body.contains ( " club</a>'s repositories" ) , " {}" , club.body) ;
1092
assert! ( get ( app, " /orgs/club/settings" , Some ( & andy) ) .await.body.contains ( " /orgs/club/settings/backups" ) ) ;
1e8a6e8b Andy McCall 23 days ago ↶
1093
1094
1095
1096
sqlx:: query( " UPDATE accounts SET is_admin = (username = 'andy')" ) .execute ( & app.db) .await.unwrap ( ) ;
1097
app.repos.create ( & oxygenhub:: repos:: RepoName:: parse( " bob/game" ) .unwrap ( ) , None ) .unwrap ( ) ;
1098
db:: repos:: create( & app.db, " bob" , " game" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1099
assert_eq! ( get ( app, " /admin/backups" , Some ( & bob) ) .await.status, StatusCode:: NOT_FOUND ) ;
1100
let admin = get ( app, " /admin/backups" , Some ( & andy) ) .await;
1101
assert_eq! ( admin.status, StatusCode:: OK ) ;
1102
assert! ( admin.body.contains ( " gs://andy-backup" ) & & admin.body.contains ( " hourly" ) & & admin.body.contains ( " never" ) , " {}" , admin.body) ;
1103
let missing = & admin.body[ admin.body.find ( " Not backed up" ) .unwrap ( ) .. ] ;
1104
assert! ( missing.contains ( " href=\" /bob\" " ) & & missing.contains ( " 1 repository" ) , " {missing}" ) ;
1105
assert! ( get ( app, " /admin" , Some ( & andy) ) .await.body.contains ( " /admin/backups" ) ) ;
d3b048b2 Andy McCall 23 days ago ↶
1106
1107
let game = get ( app, " /bob/game/settings" , Some ( & bob) ) .await;
1108
assert! ( game.body.contains ( " bob has no backup set up yet" ) & & game.body.contains ( r # "name="backup" value="" checked"# ) , " {}" , game.body) ;
1109
let csrf = csrf_of ( & game.body) ;
1110
let saved = post ( app, " /bob/game/settings/backup" , Some ( & bob) , & format! ( " _csrf={csrf} &backup=off" ) ) .await;
1111
assert! ( saved.body.contains ( " left out of its owner" ) , " {}" , saved.body) ;
1112
assert_eq! ( db:: repos:: get( & app.db, " bob" , " game" ) .await.unwrap ( ) .unwrap ( ) .backup, " off" ) ;
1113
let bad = post ( app, " /bob/game/settings/backup" , Some ( & bob) , & format! ( " _csrf={csrf} &backup=weekly" ) ) .await;
1114
assert! ( bad.body.contains ( " Pick how this repository is backed up." ) ) ;
d7d18246 Andy McCall 23 days ago ↶
1115
}
bff9106e Andy McCall 23 days ago ↶
1116
1117
1118
1119
1120
# [ tokio ::test ( flavor = " multi_thread" ) ]
1121
async fn owners_hear_by_default_and_agents_tell_their_owner ( ) {
1122
let h = hub ( ) .await;
1123
let app = & h.app;
1124
let andy = register ( app, " andy" ) .await;
1125
let bob = register ( app, " bob" ) .await;
1126
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
1127
let kinds = | id : i64 | {
1128
let db = app.db.clone ( ) ;
1129
async move { db:: notify:: inbox( & db, id, 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | n | ( n.kind, n.title, n.actor) ) .collect:: < Vec< _ > > ( ) }
1130
} ;
1131
assert! ( kinds ( andy_id) .await.iter ( ) .any ( | ( k , t , _) | k = = " account.registered" & & t.contains ( " bob" ) ) , " the administrator hears of bob" ) ;
1132
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
1133
app.repos.create ( & repo, None ) .unwrap ( ) ;
1134
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1135
let open = | who : & 'static str , title : & 'static str | {
1136
let ( app, cookie) = ( app.clone ( ) , if who = = " bob" { bob.clone ( ) } else { andy.clone ( ) } ) ;
1137
async move {
1138
let csrf = csrf_of ( & get ( & app, " /andy/demo/issues/new" , Some ( & cookie) ) .await.body) ;
1139
post ( & app, " /andy/demo/issues/new" , Some ( & cookie) , & format! ( " _csrf={csrf} &title={title} &body=x" ) ) .await
1140
}
1141
} ;
1142
1143
assert! ( get ( app, " /andy/demo" , Some ( & andy) ) .await.body.contains ( " UNWATCH" ) , " an owner watches by default" ) ;
1144
open ( " bob" , " First" ) .await;
1145
assert! ( kinds ( andy_id) .await.iter ( ) .any ( | ( k , t , _) | k = = " issue.opened" & & t.contains ( " First" ) ) ) ;
1146
1147
let csrf = csrf_of ( & get ( app, " /andy/demo" , Some ( & andy) ) .await.body) ;
1148
post ( app, " /andy/demo/watch" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
1149
assert! ( get ( app, " /andy/demo" , Some ( & andy) ) .await.body.contains ( " >WATCH<" ) , " muted" ) ;
1150
open ( " bob" , " Second" ) .await;
1151
assert! ( ! kinds ( andy_id) .await.iter ( ) .any ( | ( _, t , _ ) | t.contains ( " Second" ) ) ) ;
1152
post ( app, " /andy/demo/watch" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
1153
1154
1155
let token = | agent : bool | {
1156
let db = app.db.clone ( ) ;
1157
async move {
1158
let ( t, hash) = oxygenhub:: auth:: new_token( ) ;
1159
db:: accounts:: create_token( & db, andy_id, if agent { " Claude" } else { " laptop" } , & hash, " repo:read,repo:write" , " " , None )
1160
.await
1161
.unwrap ( ) ;
1162
if agent {
1163
let id = db:: accounts:: token_by_hash( & db, & hash) .await.unwrap ( ) .unwrap ( ) .id;
1164
db:: accounts:: set_token_agent( & db, andy_id, id, true ) .await.unwrap ( ) ;
1165
}
1166
t
1167
}
1168
} ;
1169
let api_issue = | t : String, title : & 'static str | {
1170
let app = app.clone ( ) ;
1171
async move {
1172
use tower:: ServiceExt;
1173
let req = axum:: http:: Request:: post( " /api/v1/repos/andy/demo/issues" )
1174
.header ( " authorization" , format! ( " Bearer {t} " ) )
1175
.header ( " content-type" , " application/json" )
1176
.body ( axum:: body:: Body:: from( serde_json:: json! ( { " title" : title, " body" : " " } ) .to_string ( ) ) )
1177
.unwrap ( ) ;
1178
oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) .status ( )
1179
}
1180
} ;
1181
assert_eq! ( api_issue ( token ( false ) .await, " By hand" ) .await, StatusCode:: CREATED ) ;
1182
assert! ( ! kinds ( andy_id) .await.iter ( ) .any ( | ( _, t , _ ) | t.contains ( " By hand" ) ) , " his own doing" ) ;
1183
assert_eq! ( api_issue ( token ( true ) .await, " By the agent" ) .await, StatusCode:: CREATED ) ;
1184
let told = kinds ( andy_id) .await;
1185
let agent = told.iter ( ) .find ( | ( _, t , _ ) | t.contains ( " By the agent" ) ) .expect ( " told of his agent's issue" ) ;
1186
assert_eq! ( agent.2 , " andy (agent token Claude)" ) ;
1187
1188
let page = get ( app, " /settings/tokens" , Some ( & andy) ) .await;
00ef7b83 Andy McCall 23 days ago ↶
1189
assert! ( page.body.contains ( " Not an agent" ) & & page.body.contains ( " Mark as agent" ) , " {}" , page.body) ;
1190
assert! ( page.body.contains ( r # "<span class="pill agent-pill""# ) , " the agent's token says so" ) ;
bff9106e Andy McCall 23 days ago ↶
1191
}