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" ) ;
152
let inbox = get ( app, " /notifications" , Some ( & bob) ) .await;
153
assert! ( inbox.body.contains ( " andy/demo#1: Crash on start" ) , " {}" , inbox.body) ;
154
assert! ( get ( app, " /andy/demo" , Some ( & bob) ) .await.body.contains ( " class=\" count\" >1<" ) , " bell shows the count" ) ;
155
156
157
let page = get ( app, " /andy/demo/issue/1" , Some ( & andy) ) .await;
158
let csrf_a = csrf_of ( & page.body) ;
159
let label_id: i64 = sqlx:: query_scalar( " SELECT id FROM labels WHERE name = 'bug'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
160
let r = post ( app, " /andy/demo/issue/1/labels" , Some ( & andy) , & format! ( " _csrf={csrf_a} &label={label_id} " ) ) .await;
161
assert! ( r.body.contains ( " >bug<" ) , " {}" , r.body) ;
162
let r = post ( app, " /andy/demo/issue/1/assign" , Some ( & andy) , & format! ( " _csrf={csrf_a} &username=bob&action=add" ) ) .await;
163
assert! ( r.body.contains ( " href=\" /bob\" >bob</a>" ) , " {}" , r.body) ;
164
let ms_id: i64 = sqlx:: query_scalar( " SELECT id FROM milestones WHERE title = 'v1'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
165
let r = post ( app, " /andy/demo/issue/1/milestone" , Some ( & andy) , & format! ( " _csrf={csrf_a} &milestone={ms_id} " ) ) .await;
166
assert! ( r.body.contains ( " >v1<" ) , " {}" , r.body) ;
167
assert! ( post ( app, " /andy/demo/issue/1/state" , Some ( & andy) , & format! ( " _csrf={csrf_a} &action=pin" ) ) .await.body.contains ( " UNPIN" ) ) ;
168
assert_eq! (
169
post (
170
app,
171
" /andy/demo/issue/1/state" ,
172
Some ( & bob) ,
173
& format! ( " _csrf={} &action=lock" , csrf_of ( & get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await.body) )
174
)
175
.await
176
.status,
177
StatusCode:: NOT_FOUND ,
178
" bob cannot lock"
179
) ;
180
181
let list = get ( app, " /andy/demo/issues?q=label:bug+assignee:bob+milestone:v1" , None ) .await;
182
assert! ( list.body.contains ( " Crash on start" ) , " {}" , list.body) ;
183
assert! ( ! get ( app, " /andy/demo/issues?q=label:feature" , None ) .await.body.contains ( " Crash on start" ) ) ;
184
assert! ( get ( app, " /andy/demo/milestones" , None ) .await.body.contains ( " 1 open, 0 closed" ) ) ;
185
186
187
let csrf_b = csrf_of ( & get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await.body) ;
188
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & bob) , & format! ( " _csrf={csrf_b} &body=Reproduced+with+the+flux+capacitor" ) ) .await;
189
assert! ( r.body.contains ( " flux capacitor" ) , " {}" , r.body) ;
190
assert! ( get ( app, " /notifications" , Some ( & andy) ) .await.body.contains ( " andy/demo#1" ) ) ;
191
let s = get ( app, " /search?q=capacitor" , None ) .await;
192
assert! ( s.body.contains ( " Crash on start" ) & & s.body.contains ( " <mark>capacitor</mark>" ) , " {}" , s.body) ;
193
let s = get ( app, " /search?q=demo" , None ) .await;
194
assert! ( s.body.contains ( " /andy/demo\" " ) , " repository hit: {}" , s.body) ;
195
196
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & bob) , & format! ( " _csrf={csrf_b} &body=closing&close=completed" ) ) .await;
197
assert! ( r.body.contains ( " >open<" ) | | r.body.contains ( " head\" >open" ) , " still open: {}" , r.body) ;
198
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=fixed&close=completed" ) ) .await;
199
assert! ( r.body.contains ( " closed" ) , " {}" , r.body) ;
200
assert! ( get ( app, " /andy/demo/issues?state=closed" , None ) .await.body.contains ( " Crash on start" ) ) ;
201
202
203
let csrf_a = csrf_of ( & get ( app, " /andy/demo/issues/new" , Some ( & andy) ) .await.body) ;
204
assert_eq! (
205
post ( app, " /andy/demo/issues/new" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=Typo&body=" ) ) .await.location.as_deref ( ) ,
206
Some ( " /andy/demo/issue/2" )
207
) ;
208
let work = tempfile:: tempdir( ) .unwrap ( ) ;
209
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
210
client.config.set ( " remote.origin.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
211
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
212
client.save_config ( ) .unwrap ( ) ;
213
write ( work.path ( ) , " a.txt" , b " a\n " ) ;
214
client.snapshot ( " t" ) .unwrap ( ) ;
215
client.commit ( " Fix the typo\n \n Closes #2" , & [ ] ) .unwrap ( ) ;
216
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
217
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
218
let issue2 = db:: issues:: get( & app.db, db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) .id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
219
assert_eq! ( issue2.state, " closed" , " closed by the landed change" ) ;
220
assert! ( get ( app, " /andy/demo/issue/2" , None ) .await.body.contains ( " closed by change" ) ) ;
221
222
223
client.bookmark_create ( " feat" , None ) .unwrap ( ) ;
224
client.bookmark_switch ( " feat" , false ) .unwrap ( ) ;
225
write ( work.path ( ) , " b.txt" , b " b\n " ) ;
226
client.snapshot ( " t" ) .unwrap ( ) ;
227
client.commit ( " feat" , & [ ] ) .unwrap ( ) ;
228
assert! ( client.push_bookmark ( " origin" , " feat" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
229
let csrf_a = csrf_of ( & get ( app, " /andy/demo/proposals/new?source=feat&target=main" , Some ( & andy) ) .await.body) ;
230
let r = post ( app, " /andy/demo/proposals/new" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=Feat&target=main&source=feat" ) ) .await;
231
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) , " {}" , r.body) ;
232
assert_eq! ( get ( app, " /andy/demo/issue/3" , None ) .await.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) ) ;
233
234
let r = post ( app, " /andy/demo/issue/2/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=see+%233" ) ) .await;
235
assert! ( r.body.contains ( " href=\" /andy/demo/proposal/3\" " ) , " {}" , r.body) ;
eb0c44a9 Andy McCall 1 month ago ↶
236
237
let part = get ( app, " /notifications?reason=participating" , Some ( & bob) ) .await.body;
238
assert! ( part.contains ( " andy/demo#1" ) & & ! part.contains ( " andy/demo#3: Feat" ) , " {part}" ) ;
239
let watch = get ( app, " /notifications?reason=watching" , Some ( & bob) ) .await.body;
240
assert! ( watch.contains ( " andy/demo#3: Feat" ) & & ! watch.contains ( " Crash on start" ) , " {watch}" ) ;
241
let props = get ( app, " /notifications?kind=proposal" , Some ( & bob) ) .await.body;
242
assert! ( props.contains ( " andy/demo#3: Feat" ) & & ! props.contains ( " andy/demo#1" ) , " kind filter" ) ;
243
assert! ( get ( app, " /notifications?repo=andy/demo" , Some ( & bob) ) .await.body.contains ( " andy/demo#3: Feat" ) ) ;
244
assert! ( get ( app, " /notifications?repo=nobody/none" , Some ( & bob) ) .await.body.contains ( " Nothing matches these filters" ) ) ;
245
246
let inbox = get ( app, " /notifications" , Some ( & bob) ) .await.body;
247
assert_eq! ( inbox.matches ( " data-row-thread=\" /andy/demo/issue/1\" " ) .count ( ) , 1 , " {inbox}" ) ;
248
assert! ( inbox.contains ( " updates" ) , " the count of a busy thread" ) ;
249
250
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
251
let newest: i64 = sqlx:: query_scalar(
252
" SELECT id FROM notifications WHERE account_id = ? AND thread = '/andy/demo/proposal/3' ORDER BY id DESC LIMIT 1" ,
253
)
254
.bind ( bob_id)
255
.fetch_one ( & app.db)
256
.await
257
.unwrap ( ) ;
258
let before = db:: notify:: unread( & app.db, bob_id) .await.unwrap ( ) ;
259
let r = get ( app, & format! ( " /notifications/open/{newest} " ) , Some ( & bob) ) .await;
260
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) ) ;
261
assert! ( db:: notify:: unread( & app.db, bob_id) .await.unwrap ( ) < before) ;
262
assert_eq! ( get ( app, & format! ( " /notifications/open/{newest} " ) , Some ( & andy) ) .await.status, StatusCode:: NOT_FOUND , " only your own" ) ;
263
264
let csrf_b = csrf_of ( & get ( app, " /notifications" , Some ( & bob) ) .await.body) ;
265
let r = post (
266
app,
267
" /notifications/thread" ,
268
Some ( & bob) ,
269
& format! ( " _csrf={csrf_b} &thread=%2Fandy%2Fdemo%2Fissue%2F1&action=done&back=%2Fnotifications" ) ,
270
)
271
.await;
272
assert_eq! ( r.location.as_deref ( ) , Some ( " /notifications" ) ) ;
273
assert! ( ! get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) ) ;
274
assert! ( get ( app, " /notifications?view=done" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) ) ;
275
post ( app, " /andy/demo/issue/1/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=one+more+thing" ) ) .await;
276
assert! ( get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) , " back with the new comment" ) ;
277
let r = post (
278
app,
279
" /notifications/thread" ,
280
Some ( & bob) ,
281
& format! ( " _csrf={csrf_b} &thread=%2Fandy%2Fdemo%2Fissue%2F1&action=read&back=https%3A%2F%2Fevil.example" ) ,
282
)
283
.await;
284
assert_eq! ( r.location.as_deref ( ) , Some ( " /notifications" ) , " only back to the inbox" ) ;
285
18e381ea Andy McCall 1 month ago ↶
286
287
assert! ( get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " andy/demo#3: Feat" ) ) ;
288
let csrf_b = csrf_of ( & get ( app, " /notifications" , Some ( & bob) ) .await.body) ;
289
let r = post ( app, " /notifications/read" , Some ( & bob) , & format! ( " _csrf={csrf_b} " ) ) .await;
290
assert! ( r.body.contains ( " 0 unread" ) , " {}" , r.body) ;
291
292
let r = post ( app, " /settings/notifications" , Some ( & bob) , & format! ( " _csrf={csrf_b} &email=1" ) ) .await;
293
assert! ( r.body.contains ( " Email notifications are on" ) ) ;
294
let key: String = sqlx:: query_scalar( " SELECT unsubscribe_key FROM accounts WHERE username = 'bob'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
295
assert_eq! ( get ( app, & format! ( " /settings/notifications?unsubscribe={key} " ) , None ) .await.status, StatusCode:: OK ) ;
296
let on: bool = sqlx:: query_scalar( " SELECT email_notifications FROM accounts WHERE username = 'bob'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
297
assert! ( ! on) ;
298
}
25ab8c73 Andy McCall 1 month ago ↶
299
300
# [ tokio ::test ( flavor = " multi_thread" ) ]
301
async fn dashboard_shows_what_needs_the_viewer ( ) {
302
let h = hub ( ) .await;
303
let app = & h.app;
304
let andy = register ( app, " andy" ) .await;
305
let bob = register ( app, " bob" ) .await;
306
for ( name, vis) in [ ( " demo" , hub_api:: Visibility:: Public) , ( " secret" , hub_api:: Visibility:: Private) ] {
307
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
308
db:: repos:: create( & app.db, " andy" , name, " " , vis) .await.unwrap ( ) ;
309
}
310
let demo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
311
let secret = db:: repos:: get( & app.db, " andy" , " secret" ) .await.unwrap ( ) .unwrap ( ) ;
312
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
313
for ( repo, title) in [ ( demo.id, " Faster startup" ) , ( secret.id, " Hidden work" ) ] {
314
let p = db:: proposals:: create(
315
& app.db,
316
db:: proposals:: NewProposal {
317
repo_id: repo,
318
title,
319
body: " " ,
320
author_id: Some ( bob_id) ,
321
author: " bob" ,
322
draft: false ,
323
target: " main" ,
324
source_kind: " bookmark" ,
325
source_repo_id: Some ( repo) ,
326
source_bookmark: " fast" ,
327
source_changes: " " ,
328
} ,
329
)
330
.await
331
.unwrap ( ) ;
332
db:: proposals:: add_event( & app.db, p.id, " bob" , " opened" , " " ) .await.unwrap ( ) ;
333
}
334
335
336
let anon = get ( app, " /" , None ) .await;
337
assert! ( anon.body.contains ( " andy /" ) & & ! anon.body.contains ( " Dashboard <small>" ) , " {}" , anon.body) ;
338
339
340
let page = get ( app, " /" , Some ( & andy) ) .await;
341
assert_eq! ( page.status, StatusCode:: OK ) ;
342
assert! ( page.body.contains ( " Waiting for your review <small>2</small>" ) , " {}" , page.body) ;
343
assert! ( page.body.contains ( " /andy/demo/proposal/1" ) & & page.body.contains ( " /andy/secret/proposal/1" ) ) ;
344
assert! ( page.body.contains ( " Your repositories" ) ) ;
345
assert! ( page.body.contains ( " opened proposal" ) , " activity feed: {}" , page.body) ;
346
347
348
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
349
let p = db:: proposals:: get( & app.db, demo.id, 1 ) .await.unwrap ( ) .unwrap ( ) ;
350
db:: proposals:: add_review( & app.db, p.id, andy_id, " andy" , " approve" , " " ) .await.unwrap ( ) ;
351
assert! ( get ( app, " /" , Some ( & andy) ) .await.body.contains ( " Waiting for your review <small>1</small>" ) ) ;
352
353
354
let page = get ( app, " /" , Some ( & bob) ) .await;
355
assert! ( page.body.contains ( " Your proposals <small>1</small>" ) , " {}" , page.body) ;
356
assert! ( ! page.body.contains ( " Hidden work" ) ) ;
357
assert! ( page.body.contains ( " Waiting for your review <small>0</small>" ) ) ;
358
assert! ( page.body.contains ( " Faster startup" ) ) ;
359
let feed = & page.body[ page.body.find ( " Recent activity" ) .unwrap ( ) .. ] ;
360
assert! ( ! feed.contains ( " andy/secret" ) , " private activity leaked: {feed}" ) ;
361
362
363
assert! ( get ( app, " /explore" , Some ( & bob) ) .await.body.contains ( " andy /" ) ) ;
364
}
4edcf240 Andy McCall 1 month ago ↶
365
366
# [ tokio ::test ( flavor = " multi_thread" ) ]
367
async fn empty_repository_shows_a_quickstart ( ) {
368
let h = hub ( ) .await;
369
let app = & h.app;
370
let andy = register ( app, " andy" ) .await;
371
let bob = register ( app, " bob" ) .await;
372
app.repos.create ( & RepoName:: parse( " andy/fresh" ) .unwrap ( ) , None ) .unwrap ( ) ;
373
db:: repos:: create( & app.db, " andy" , " fresh" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
374
375
let page = get ( app, " /andy/fresh" , Some ( & andy) ) .await;
376
assert! ( page.body.contains ( " This repository is empty" ) , " {}" , page.body) ;
377
let clone_url = format! ( " {} /andy/fresh" , h.base) ;
378
assert! ( page.body.contains ( & format! ( " oxy remote add hub {clone_url} " ) ) , " {}" , page.body) ;
379
assert! ( page.body.contains ( " oxy config remote.hub.token YOUR-TOKEN" ) ) ;
380
assert! ( page.body.contains ( " oxy push hub" ) ) ;
381
assert! (
382
page.body.contains ( " mkdir fresh && cd fresh" ) | | page.body.contains ( " mkdir fresh && cd fresh" ) ,
383
" {}" ,
384
page.body
385
) ;
386
assert! ( page.body.contains ( " oxy git import --branch main" ) ) ;
387
assert! ( ! page.body.contains ( " oxyhub-ca.pem" ) , " no certificate step without a self-signed certificate" ) ;
388
389
390
let page = get ( app, " /andy/fresh" , Some ( & bob) ) .await;
391
assert! ( page.body.contains ( " Nothing has been pushed yet" ) & & ! page.body.contains ( " oxy push hub" ) , " {}" , page.body) ;
392
393
394
let work = tempfile:: tempdir( ) .unwrap ( ) ;
395
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
396
client.config.set ( " remote.hub.url" , & clone_url) .unwrap ( ) ;
397
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
398
client.save_config ( ) .unwrap ( ) ;
399
write ( work.path ( ) , " README.md" , b " # fresh\n " ) ;
400
client.commit ( " First change" , & [ ] ) .unwrap ( ) ;
401
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
402
let page = get ( app, " /andy/fresh" , Some ( & andy) ) .await;
403
assert! ( ! page.body.contains ( " This repository is empty" ) & & page.body.contains ( " README.md" ) , " {}" , page.body) ;
404
}
c40c2d3e Andy McCall 1 month ago ↶
405
406
# [ tokio ::test ( flavor = " multi_thread" ) ]
407
async fn theme_is_chosen_in_settings_and_remembered_in_a_cookie ( ) {
408
let h = hub ( ) .await;
409
let app = & h.app;
410
let andy = register ( app, " andy" ) .await;
411
let page = get ( app, " /settings" , Some ( & andy) ) .await;
412
assert! ( page.body.contains ( " Appearance" ) ) ;
413
assert! ( page.body.contains ( r # "value="dark" checked"# ) , " dark by default: {}" , page.body) ;
414
assert! ( page.body.contains ( " data-theme-toggle" ) , " the header has the switch" ) ;
415
let csrf = csrf_of ( & page.body) ;
416
let r = post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=light" ) ) .await;
417
assert_eq! ( r.status, StatusCode:: SEE_OTHER ) ;
418
assert_eq! ( r.cookie.as_deref ( ) , Some ( " light" ) ) ;
419
assert_eq! ( r.location.as_deref ( ) , Some ( " /settings" ) ) ;
420
let page = send_with_theme ( app, & andy, " light" ) .await;
421
assert! ( page.contains ( r # "value="light" checked"# ) , " the form shows the cookie's theme" ) ;
422
assert_eq! ( post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=purple" ) ) .await.status, StatusCode:: BAD_REQUEST ) ;
423
assert_eq! ( post ( app, " /settings/appearance" , Some ( & andy) , " _csrf=nope&theme=light" ) .await.status, StatusCode:: BAD_REQUEST ) ;
424
}
425
426
async fn send_with_theme ( app : & Shared, session : & str , theme : & str ) -> String {
427
let req =
428
Request:: builder( ) .uri ( " /settings" ) .header ( header:: COOKIE , format! ( " oxyhub={session} ; theme={theme} " ) ) .body ( Body:: empty( ) ) .unwrap ( ) ;
429
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
430
String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( )
431
}
10174045 Andy McCall 1 month ago ↶
432
433
# [ tokio ::test ( flavor = " multi_thread" ) ]
434
async fn profile_and_organisation_pages ( ) {
435
let h = hub ( ) .await;
436
let app = & h.app;
437
let andy = register ( app, " andy" ) .await;
438
let bob = register ( app, " bob" ) .await;
439
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
440
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
441
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
442
db:: repos:: create( & app.db, " andy" , " demo" , " A demo" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
443
app.repos.create ( & RepoName:: parse( " andy/hidden" ) .unwrap ( ) , None ) .unwrap ( ) ;
444
db:: repos:: create( & app.db, " andy" , " hidden" , " " , hub_api:: Visibility:: Private) .await.unwrap ( ) ;
445
446
447
let csrf = csrf_of ( & get ( app, " /settings" , Some ( & andy) ) .await.body) ;
448
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;
449
assert! ( r.body.contains ( " must be an http" ) , " {}" , r.body) ;
450
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;
451
assert! ( r.body.contains ( " Profile saved" ) , " {}" , r.body) ;
452
453
let org = db:: accounts:: create_org( & app.db, " retro" , " Retro Club" , andy_id) .await.unwrap ( ) ;
454
db:: accounts:: set_org_member( & app.db, org.id, bob_id, " member" ) .await.unwrap ( ) ;
455
let team = db:: accounts:: create_team( & app.db, org.id, " core" ) .await.unwrap ( ) ;
456
db:: accounts:: add_team_member( & app.db, team.id, bob_id) .await.unwrap ( ) ;
457
458
let page = get ( app, " /andy" , Some ( & bob) ) .await;
459
assert_eq! ( page.status, StatusCode:: OK ) ;
460
for want in [ " Andy McCall" , " Earth" , r # "href="https://example.org""# , " /avatar/andy" , " Signing keys" , " /retro" , " demo" ] {
461
assert! ( page.body.contains ( want) , " profile lacks {want}: {}" , page.body) ;
462
}
463
assert! ( ! page.body.contains ( " <b>oxy</b>" ) , " the bio is escaped" ) ;
464
assert! ( ! page.body.contains ( " [email protected] " ) , " the email stays private" ) ;
465
assert! ( ! page.body.contains ( " /andy/hidden" ) , " bob cannot see the private repository" ) ;
466
assert! ( ! page.body.contains ( " edit profile" ) , " only the owner gets the edit link" ) ;
467
assert! ( get ( app, " /andy" , Some ( & andy) ) .await.body.contains ( " /andy/hidden" ) , " andy sees his own private repository" ) ;
468
469
let page = get ( app, " /retro" , Some ( & bob) ) .await;
470
assert_eq! ( page.status, StatusCode:: OK ) ;
471
for want in [ " Retro Club" , " organisation" , " Members <small>2</small>" , " core" , " 1 member" ] {
472
assert! ( page.body.contains ( want) , " org page lacks {want}: {}" , page.body) ;
473
}
474
assert! ( ! page.body.contains ( " /orgs/retro/settings" ) , " bob is not an owner" ) ;
475
assert! ( get ( app, " /retro" , Some ( & andy) ) .await.body.contains ( " /orgs/retro/settings" ) ) ;
476
477
let svg = get ( app, " /avatar/andy" , None ) .await;
478
assert_eq! ( svg.status, StatusCode:: OK ) ;
479
assert! ( svg.body.starts_with ( " <svg" ) ) ;
480
assert_eq! ( svg.body, get ( app, " /avatar/ANDY" , None ) .await.body) ;
481
assert_eq! ( get ( app, " /nobody-here" , None ) .await.status, StatusCode:: NOT_FOUND ) ;
482
}
9df855f2 Andy McCall 1 month ago ↶
483
484
# [ tokio ::test ( flavor = " multi_thread" ) ]
485
async fn diffs_mark_words_hide_whitespace_and_collapse ( ) {
486
let h = hub ( ) .await;
487
let app = & h.app;
488
let andy = register ( app, " andy" ) .await;
489
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
490
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
491
let work = tempfile:: tempdir( ) .unwrap ( ) ;
492
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
493
client.config.set ( " remote.hub.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
494
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
495
client.save_config ( ) .unwrap ( ) ;
496
write ( work.path ( ) , " main.rs" , b " fn main() {\n setup();\n let answer = 42;\n }\n " ) ;
497
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
498
write ( work.path ( ) , " main.rs" , b " fn main() {\n setup();\n let answer = 43;\n }\n " ) ;
499
let second = client.commit ( " reindent and change the answer" , & [ ] ) .unwrap ( ) ;
500
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
501
502
let url = format! ( " /andy/demo/change/{} " , second.to_hex ( ) ) ;
503
let page = get ( app, & url, Some ( & andy) ) .await;
504
assert_eq! ( page.status, StatusCode:: OK ) ;
505
assert! ( page.body.contains ( r # "<details class="file-diff""# ) , " files collapse: {}" , page.body) ;
506
assert! ( page.body.contains ( r # "<span class="wx">43</span>"# ) , " the changed word is marked: {}" , page.body) ;
507
assert! ( page.body.contains ( " hide whitespace changes" ) ) ;
508
assert! ( page.body.contains ( " data-diff-mode=\" split\" " ) ) ;
509
let adds = | b : & str | b.matches ( r # "<tr class="add">"# ) .count ( ) ;
510
assert_eq! ( adds ( & page.body) , 2 , " re-indent and answer both changed" ) ;
511
512
let quiet = get ( app, & format! ( " {url} ?w=1" ) , Some ( & andy) ) .await;
513
assert_eq! ( adds ( & quiet.body) , 1 , " only the answer changed ignoring whitespace: {}" , quiet.body) ;
514
assert! ( quiet.body.contains ( " show whitespace changes" ) ) ;
515
516
assert! ( quiet.body.contains ( " ?w=0" ) , " the toggle leads back" ) ;
517
}
07b1a5bb Andy McCall 1 month ago ↶
518
519
# [ tokio ::test ( flavor = " multi_thread" ) ]
520
async fn fork_and_propose_upstream ( ) {
521
let h = hub ( ) .await;
522
let app = & h.app;
523
let andy = register ( app, " andy" ) .await;
524
let bob = register ( app, " bob" ) .await;
525
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
526
db:: repos:: create( & app.db, " andy" , " demo" , " The demo" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
527
let work = tempfile:: tempdir( ) .unwrap ( ) ;
528
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
529
client.config.set ( " remote.hub.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
530
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
531
client.save_config ( ) .unwrap ( ) ;
532
write ( work.path ( ) , " README.md" , b " # demo\n " ) ;
533
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
534
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
535
536
537
let page = get ( app, " /andy/demo" , Some ( & bob) ) .await;
538
assert! ( page.body.contains ( " /andy/demo/fork" ) , " a fork button: {}" , page.body) ;
539
let csrf = csrf_of ( & get ( app, " /andy/demo/fork" , Some ( & bob) ) .await.body) ;
540
let r = post ( app, " /andy/demo/fork" , Some ( & bob) , & format! ( " _csrf={csrf} &owner=andy&name=other" ) ) .await;
541
assert! ( r.body.contains ( " cannot create repositories under that owner" ) , " {}" , r.body) ;
542
let r = post ( app, " /andy/demo/fork" , Some ( & bob) , & format! ( " _csrf={csrf} &owner=bob&name=demo" ) ) .await;
543
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
544
assert_eq! ( r.location.as_deref ( ) , Some ( " /bob/demo" ) ) ;
545
let forked = get ( app, " /bob/demo" , Some ( & bob) ) .await;
546
assert! ( forked.body.contains ( " README.md" ) & & forked.body.contains ( " forked from" ) , " {}" , forked.body) ;
547
assert! ( forked.body.contains ( " propose to andy/demo" ) ) ;
548
assert! ( get ( app, " /andy/demo" , None ) .await.body.contains ( " 1 fork" ) ) ;
549
assert! ( get ( app, " /andy/demo/forks" , None ) .await.body.contains ( " /bob/demo" ) ) ;
550
let fork_row = db:: repos:: get( & app.db, " bob" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
551
let up_row = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
552
assert_eq! ( fork_row.forked_from, Some ( up_row.id) ) ;
553
assert_eq! ( fork_row.description, " The demo" ) ;
554
let fork_repo = app.repos.open ( & RepoName:: parse( " bob/demo" ) .unwrap ( ) ) .unwrap ( ) ;
555
let up_repo = app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
556
assert_ne! ( fork_repo.id ( ) .unwrap ( ) , up_repo.id ( ) .unwrap ( ) , " a fork is a repository of its own" ) ;
557
assert_eq! ( fork_repo.bookmark_heads ( ) .unwrap ( ) , up_repo.bookmark_heads ( ) .unwrap ( ) ) ;
558
assert_eq! ( fork_repo.fsck ( ) .unwrap ( ) .errors ( ) , 0 ) ;
559
560
561
let bw = tempfile:: tempdir( ) .unwrap ( ) ;
562
let mut mine = Repo:: clone( & format! ( " {} /bob/demo" , h.base) , & bw.path ( ) .join ( " c" ) ) .unwrap ( ) ;
563
mine.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
564
mine.save_config ( ) .unwrap ( ) ;
565
mine.bookmark_create ( " feat" , None ) .unwrap ( ) ;
566
mine.bookmark_switch ( " feat" , false ) .unwrap ( ) ;
567
write ( & bw.path ( ) .join ( " c" ) , " feature.txt" , b " new\n " ) ;
568
mine.commit ( " Add a feature" , & [ ] ) .unwrap ( ) ;
569
assert! ( mine.push_bookmark ( " origin" , " feat" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
570
let csrf = csrf_of ( & get ( app, " /andy/demo/proposals/new?source=bob/demo:feat" , Some ( & bob) ) .await.body) ;
571
let r =
572
post ( app, " /andy/demo/proposals/new" , Some ( & bob) , & format! ( " _csrf={csrf} &title=From+my+fork&target=main&source=bob%2Fdemo%3Afeat" ) )
573
.await;
574
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
575
let p = get ( app, & r.location.unwrap ( ) , Some ( & andy) ) .await;
576
assert! ( p.body.contains ( " From my fork" ) & & p.body.contains ( " fork bob/demo" ) , " {}" , p.body) ;
577
let files = get ( app, " /andy/demo/proposal/1?tab=files" , Some ( & andy) ) .await;
578
assert! ( files.body.contains ( " feature.txt" ) , " {}" , files.body) ;
579
}
1553bfc9 Andy McCall 1 month ago ↶
580
581
# [ tokio ::test ( flavor = " multi_thread" ) ]
582
async fn stars_topics_and_explore ( ) {
583
let h = hub ( ) .await;
584
let app = & h.app;
585
let andy = register ( app, " andy" ) .await;
586
let bob = register ( app, " bob" ) .await;
587
for ( name, desc) in [ ( " alpha" , " An emulator" ) , ( " beta" , " A compiler" ) , ( " gamma" , " Retro games" ) ] {
588
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
589
db:: repos:: create( & app.db, " andy" , name, desc, hub_api:: Visibility:: Public) .await.unwrap ( ) ;
590
}
591
592
593
let csrf = csrf_of ( & get ( app, " /andy/gamma/settings" , Some ( & andy) ) .await.body) ;
594
let r = post (
595
app,
596
" /andy/gamma/settings" ,
597
Some ( & andy) ,
598
& format! ( " _csrf={csrf} &description=Retro+games&default_bookmark=main&visibility=public&topics=c%2B%2B" ) ,
599
)
600
.await;
601
assert! ( r.body.contains ( " is not a topic" ) , " {}" , r.body) ;
602
let r = post (
603
app,
604
" /andy/gamma/settings" ,
605
Some ( & andy) ,
606
& format! ( " _csrf={csrf} &description=Retro+games&default_bookmark=main&visibility=public&topics=Retro%2C+6502" ) ,
607
)
608
.await;
609
assert! ( r.body.contains ( " Saved" ) , " {}" , r.body) ;
610
assert_eq! ( db:: repos:: get( & app.db, " andy" , " gamma" ) .await.unwrap ( ) .unwrap ( ) .topics, " retro 6502" ) ;
611
let page = get ( app, " /andy/gamma" , None ) .await;
612
assert! ( page.body.contains ( r # "href="/explore?topic=retro""# ) , " {}" , page.body) ;
613
614
615
for ( who, repo) in [ ( & bob, " beta" ) , ( & bob, " gamma" ) , ( & andy, " gamma" ) , ( & andy, " alpha" ) ] {
616
let csrf = csrf_of ( & get ( app, & format! ( " /andy/{repo} " ) , Some ( who) ) .await.body) ;
617
assert_eq! ( post ( app, & format! ( " /andy/{repo} /star" ) , Some ( who) , & format! ( " _csrf={csrf} " ) ) .await.status, StatusCode:: SEE_OTHER ) ;
618
}
619
let csrf = csrf_of ( & get ( app, " /andy/alpha" , Some ( & andy) ) .await.body) ;
620
post ( app, " /andy/alpha/star" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
621
assert! ( get ( app, " /andy/gamma" , Some ( & bob) ) .await.body.contains ( " ★ UNSTAR" ) ) ;
622
assert! ( get ( app, " /andy/gamma" , None ) .await.body.contains ( " ★ 2" ) ) ;
623
assert! ( get ( app, " /andy/alpha" , None ) .await.body.contains ( " ★ 0" ) , " unstarred again" ) ;
624
625
626
let order = | b : & str | {
627
[ " alpha" , " beta" , " gamma" ]
628
.into_iter ( )
629
.map ( | n | ( b.find ( & format! ( " /andy/{n} \" " ) ) .unwrap_or ( usize :: MAX ) , n) )
630
.collect:: < std:: collections:: BTreeMap< _ , _ > > ( )
631
.into_values ( )
632
.collect:: < Vec< _ > > ( )
633
} ;
634
assert_eq! ( order ( & get ( app, " /explore?sort=stars" , None ) .await.body) [ .. 2 ] , [ " gamma" , " beta" ] ) ;
635
assert_eq! ( order ( & get ( app, " /explore?sort=name" , None ) .await.body) , [ " alpha" , " beta" , " gamma" ] ) ;
636
let filtered = get ( app, " /explore?q=compiler" , None ) .await.body;
637
assert! ( filtered.contains ( " /andy/beta\" " ) & & ! filtered.contains ( " /andy/alpha\" " ) , " {filtered}" ) ;
638
let by_topic = get ( app, " /explore?topic=6502" , None ) .await.body;
639
assert! ( by_topic.contains ( " /andy/gamma\" " ) & & ! by_topic.contains ( " /andy/beta\" " ) , " {by_topic}" ) ;
640
}
1ac84c62 Andy McCall 1 month ago ↶
641
642
# [ tokio ::test ( flavor = " multi_thread" ) ]
643
async fn lists_filter_sort_page_and_count ( ) {
644
let h = hub ( ) .await;
645
let app = & h.app;
646
let andy = register ( app, " andy" ) .await;
647
let _bob = register ( app, " bob" ) .await;
648
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
649
let repo = db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
650
for n in 0 .. 30 {
651
let author = if n % 3 = = 0 { " bob" } else { " andy" } ;
652
db:: issues:: create(
653
& app.db,
654
db:: issues:: NewIssue { repo_id: repo.id, title: & format! ( " Issue number {n} " ) , body: " " , author_id: None , author } ,
655
)
656
.await
657
.unwrap ( ) ;
658
}
659
let ui = db:: issues:: upsert_label( & app.db, repo.id, " ui" , " #00f0ff" , " " ) .await.unwrap ( ) ;
660
for ( title, author, target) in [ ( " Faster menus" , " bob" , " main" ) , ( " New theme" , " andy" , " main" ) , ( " Release prep" , " andy" , " stable" ) ] {
661
let p = db:: proposals:: create(
662
& app.db,
663
db:: proposals:: NewProposal {
664
repo_id: repo.id,
665
title,
666
body: " " ,
667
author_id: None ,
668
author,
669
draft: false ,
670
target,
671
source_kind: " changes" ,
672
source_repo_id: None ,
673
source_bookmark: " " ,
674
source_changes: " " ,
675
} ,
676
)
677
.await
678
.unwrap ( ) ;
679
if title = = " New theme" {
680
db:: issues:: set_proposal_label( & app.db, p.id, ui.id, true ) .await.unwrap ( ) ;
681
}
682
}
683
684
685
let page = get ( app, " /andy/demo/issues" , Some ( & andy) ) .await;
686
assert! ( page.body.contains ( r # "Issues <span class="count-chip">30</span>"# ) , " {}" , page.body) ;
687
assert! ( page.body.contains ( r # "Proposals <span class="count-chip">3</span>"# ) ) ;
688
689
690
let rows = | b : & str | b.matches ( " /andy/demo/issue/" ) .count ( ) ;
691
assert_eq! ( rows ( & page.body) , 25 ) ;
692
assert! ( page.body.contains ( " page 1 of 2" ) & & page.body.contains ( " page=2" ) , " a pager" ) ;
693
let second = get ( app, " /andy/demo/issues?page=2" , Some ( & andy) ) .await;
694
assert_eq! ( rows ( & second.body) , 5 ) ;
695
assert! ( second.body.contains ( " Issue number 0<" ) , " the oldest are last" ) ;
696
let oldest = get ( app, " /andy/demo/issues?sort=oldest" , Some ( & andy) ) .await;
697
assert! ( oldest.body.find ( " Issue number 0<" ) .unwrap ( ) < oldest.body.find ( " Issue number 1<" ) .unwrap ( ) ) ;
698
let bobs = get ( app, " /andy/demo/issues?q=author:bob" , Some ( & andy) ) .await;
699
assert_eq! ( rows ( & bobs.body) , 10 , " {}" , bobs.body) ;
700
701
702
let by_bob = get ( app, " /andy/demo/proposals?q=author:bob" , Some ( & andy) ) .await.body;
703
assert! ( by_bob.contains ( " Faster menus" ) & & ! by_bob.contains ( " New theme" ) ) ;
704
let ui_only = get ( app, " /andy/demo/proposals?q=label:ui" , Some ( & andy) ) .await.body;
705
assert! ( ui_only.contains ( " New theme" ) & & ! ui_only.contains ( " Faster menus" ) & & ui_only.contains ( r # "class="label""# ) ) ;
706
let stable = get ( app, " /andy/demo/proposals?q=target:stable" , Some ( & andy) ) .await.body;
707
assert! ( stable.contains ( " Release prep" ) & & ! stable.contains ( " New theme" ) ) ;
708
}