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 ( ) ;
c8684d44 Andy McCall 19 days ago
42
43
config.registration.setup_code = " test-setup" .into ( ) ;
18e381ea Andy McCall 1 month ago ↶
44
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
45
config.registration.mode = " open" .into ( ) ;
46
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
47
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
48
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
49
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
50
let router = oxygenhub:: router( app.clone ( ) ) ;
51
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
52
Hub { app, base: format! ( " http://127.0.0.1:{port} " ) , _dir: dir }
53
}
54
55
struct Res {
56
status : StatusCode,
57
body : String,
58
cookie : Option< String> ,
59
location : Option< String> ,
60
}
61
62
async fn send ( app : & Shared, method : & str , path : & str , cookie : Option< & str > , form : Option< & str > ) -> Res {
63
let mut req = Request:: builder( ) .method ( method) .uri ( path) ;
64
if let Some ( c) = cookie {
65
req = req.header ( header:: COOKIE , format! ( " oxyhub={c} " ) ) ;
66
}
67
let body = match form {
68
Some ( f) => {
69
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
70
Body:: from( f.to_string ( ) )
71
}
72
None => Body:: empty( ) ,
73
} ;
74
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req.body ( body) .unwrap ( ) ) .await.unwrap ( ) ;
75
let status = res.status ( ) ;
76
let cookie = res
77
.headers ( )
78
.get ( header:: SET_COOKIE )
79
.and_then ( | v | v.to_str ( ) .ok ( ) )
80
.and_then ( | v | v.split ( ' ;' ) .next ( ) )
81
.and_then ( | kv | kv.split_once ( ' =' ) )
82
.map ( | ( _, v ) | v.to_string ( ) ) ;
83
let location = res.headers ( ) .get ( header:: LOCATION ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .map ( String :: from) ;
84
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
85
Res { status, body: String :: from_utf8_lossy( & bytes) .into_owned ( ) , cookie, location }
86
}
87
88
async fn get ( app : & Shared, path : & str , cookie : Option< & str > ) -> Res {
89
send ( app, " GET" , path, cookie, None ) .await
90
}
91
92
async fn post ( app : & Shared, path : & str , cookie : Option< & str > , form : & str ) -> Res {
93
send ( app, " POST" , path, cookie, Some ( form) ) .await
94
}
95
96
async fn register ( app : & Shared, user : & str ) -> String {
c8684d44 Andy McCall 19 days ago
97
let r = post (
98
app,
99
" /register" ,
100
None ,
101
& format! ( " username={user} &email={user} %40example.com&password=correct-horse-battery&setup=test-setup" ) ,
102
)
103
.await;
18e381ea Andy McCall 1 month ago ↶
104
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
105
r.cookie.unwrap ( )
106
}
107
c7067c7b Andy McCall 15 days ago
108
109
110
async fn settled ( app : & Shared, r : Res, cookie : Option< & str > , page : & str ) -> Res {
111
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
112
let to = r.location.expect ( " a redirect" ) ;
113
assert! ( to.starts_with ( & format! ( " {page} ?" ) ) , " {to} is not {page}" ) ;
114
get ( app, & to, cookie) .await
115
}
116
18e381ea Andy McCall 1 month ago ↶
117
fn csrf_of ( page : & str ) -> String {
118
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
119
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
120
}
121
122
# [ tokio ::test ( flavor = " multi_thread" ) ]
123
async fn issues_notifications_and_search ( ) {
124
let h = hub ( ) .await;
125
let app = & h.app;
126
let andy = register ( app, " andy" ) .await;
127
let bob = register ( app, " bob" ) .await;
128
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
129
app.repos.create ( & repo, None ) .unwrap ( ) ;
130
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
131
132
133
let labels_page = get ( app, " /andy/demo/labels" , Some ( & andy) ) .await;
134
let csrf_a = csrf_of ( & labels_page.body) ;
135
assert! ( post ( app, " /andy/demo/labels" , Some ( & andy) , & format! ( " _csrf={csrf_a} &name=bug&colour=%23ff0000&description=Something+broke" ) )
136
.await
137
.body
138
.contains ( " Saved bug" ) ) ;
139
assert! ( post ( app, " /andy/demo/milestones" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=v1&description=First&due=2027-01-01" ) )
140
.await
141
.body
142
.contains ( " v1" ) ) ;
143
assert_eq! ( get ( app, " /andy/demo/labels" , Some ( & bob) ) .await.status, StatusCode:: OK , " readers see labels" ) ;
144
assert! ( ! get ( app, " /andy/demo/labels" , Some ( & bob) ) .await.body.contains ( " Add or change" ) , " but cannot edit them" ) ;
145
146
147
let overview = get ( app, " /andy/demo" , Some ( & bob) ) .await;
148
let csrf_b = csrf_of ( & overview.body) ;
149
assert_eq! ( post ( app, " /andy/demo/watch" , Some ( & bob) , & format! ( " _csrf={csrf_b} " ) ) .await.status, StatusCode:: SEE_OTHER ) ;
150
assert! ( get ( app, " /andy/demo" , Some ( & bob) ) .await.body.contains ( " UNWATCH" ) ) ;
151
let new_page = get ( app, " /andy/demo/issues/new" , Some ( & andy) ) .await;
152
let csrf_a = csrf_of ( & new_page.body) ;
153
let r = post (
154
app,
155
" /andy/demo/issues/new" ,
156
Some ( & andy) ,
157
& format! ( " _csrf={csrf_a} &title=Crash+on+start&body=cc+%40bob+see+%23999+and+deadbeefcafe1234" ) ,
158
)
159
.await;
160
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
161
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/issue/1" ) ) ;
162
let page = get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await;
163
assert! ( page.body.contains ( " Crash on start" ) ) ;
164
assert! ( page.body.contains ( " href=\" /bob\" " ) , " mention linked: {}" , page.body) ;
165
assert! ( page.body.contains ( " /andy/demo/change/deadbeefcafe1234" ) , " hash linked: {}" , page.body) ;
166
assert! ( page.body.contains ( " #999" ) & & ! page.body.contains ( " issue/999" ) , " unknown numbers are not linked" ) ;
167
let unread = db:: notify:: unread( & app.db, db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id) .await.unwrap ( ) ;
168
assert_eq! ( unread, 1 , " one notification, not one per reason" ) ;
ea14c4ec Andy McCall 20 days ago ↶
169
170
171
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
172
let filter = db:: notify:: Filter { done: false , unread_only: false , reason: None , kind: None , repo_id: None } ;
173
let t = db:: notify:: threads( & app.db, bob_id, & filter, 10 ) .await.unwrap ( ) ;
174
assert_eq! ( ( t[ 0 ] .count, t[ 0 ] .unread) , ( 1 , 1 ) ) ;
175
let unread_only = db:: notify:: Filter { unread_only: true , .. filter.clone ( ) } ;
176
assert_eq! ( db:: notify:: threads( & app.db, bob_id, & unread_only, 10 ) .await.unwrap ( ) .len ( ) , 1 ) ;
18e381ea Andy McCall 1 month ago ↶
177
let inbox = get ( app, " /notifications" , Some ( & bob) ) .await;
178
assert! ( inbox.body.contains ( " andy/demo#1: Crash on start" ) , " {}" , inbox.body) ;
179
assert! ( get ( app, " /andy/demo" , Some ( & bob) ) .await.body.contains ( " class=\" count\" >1<" ) , " bell shows the count" ) ;
180
181
182
let page = get ( app, " /andy/demo/issue/1" , Some ( & andy) ) .await;
183
let csrf_a = csrf_of ( & page.body) ;
184
let label_id: i64 = sqlx:: query_scalar( " SELECT id FROM labels WHERE name = 'bug'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
185
let r = post ( app, " /andy/demo/issue/1/labels" , Some ( & andy) , & format! ( " _csrf={csrf_a} &label={label_id} " ) ) .await;
186
assert! ( r.body.contains ( " >bug<" ) , " {}" , r.body) ;
187
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 ↶
188
assert! ( r.body.contains ( " <li><a href=\" /bob\" ><img class=\" avatar xs\" " ) , " {}" , r.body) ;
189
190
assert! ( r.body.contains ( " Assign people to this issue" ) & & r.body.contains ( " data-menu-filter" ) , " the assignees menu" ) ;
191
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 20 days ago
192
let assigned = | db : oxygenhub:: db:: Pool| async move {
cb71e18e Andy McCall 1 month ago ↶
193
let id: i64 = sqlx:: query_scalar( " SELECT id FROM issues WHERE number = 1" ) .fetch_one ( & db) .await.unwrap ( ) ;
194
let mut a = db:: issues:: assignees( & db, id) .await.unwrap ( ) ;
195
a.sort ( ) ;
196
a
197
} ;
198
assert_eq! ( assigned ( app.db.clone ( ) ) .await, [ " andy" , " bob" ] , " {}" , r.status) ;
199
post ( app, " /andy/demo/issue/1/assign" , Some ( & andy) , & format! ( " _csrf={csrf_a} &action=set&username=bob" ) ) .await;
200
assert_eq! ( assigned ( app.db.clone ( ) ) .await, [ " bob" ] , " set takes off who is not listed" ) ;
18e381ea Andy McCall 1 month ago ↶
201
let ms_id: i64 = sqlx:: query_scalar( " SELECT id FROM milestones WHERE title = 'v1'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
202
let r = post ( app, " /andy/demo/issue/1/milestone" , Some ( & andy) , & format! ( " _csrf={csrf_a} &milestone={ms_id} " ) ) .await;
203
assert! ( r.body.contains ( " >v1<" ) , " {}" , r.body) ;
cb71e18e Andy McCall 1 month ago ↶
204
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 ↶
205
assert_eq! (
206
post (
207
app,
208
" /andy/demo/issue/1/state" ,
209
Some ( & bob) ,
210
& format! ( " _csrf={} &action=lock" , csrf_of ( & get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await.body) )
211
)
212
.await
213
.status,
214
StatusCode:: NOT_FOUND ,
215
" bob cannot lock"
216
) ;
217
218
let list = get ( app, " /andy/demo/issues?q=label:bug+assignee:bob+milestone:v1" , None ) .await;
219
assert! ( list.body.contains ( " Crash on start" ) , " {}" , list.body) ;
220
assert! ( ! get ( app, " /andy/demo/issues?q=label:feature" , None ) .await.body.contains ( " Crash on start" ) ) ;
221
assert! ( get ( app, " /andy/demo/milestones" , None ) .await.body.contains ( " 1 open, 0 closed" ) ) ;
222
223
224
let csrf_b = csrf_of ( & get ( app, " /andy/demo/issue/1" , Some ( & bob) ) .await.body) ;
225
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & bob) , & format! ( " _csrf={csrf_b} &body=Reproduced+with+the+flux+capacitor" ) ) .await;
226
assert! ( r.body.contains ( " flux capacitor" ) , " {}" , r.body) ;
227
assert! ( get ( app, " /notifications" , Some ( & andy) ) .await.body.contains ( " andy/demo#1" ) ) ;
228
let s = get ( app, " /search?q=capacitor" , None ) .await;
229
assert! ( s.body.contains ( " Crash on start" ) & & s.body.contains ( " <mark>capacitor</mark>" ) , " {}" , s.body) ;
230
let s = get ( app, " /search?q=demo" , None ) .await;
231
assert! ( s.body.contains ( " /andy/demo\" " ) , " repository hit: {}" , s.body) ;
232
233
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & bob) , & format! ( " _csrf={csrf_b} &body=closing&close=completed" ) ) .await;
234
assert! ( r.body.contains ( " >open<" ) | | r.body.contains ( " head\" >open" ) , " still open: {}" , r.body) ;
235
let r = post ( app, " /andy/demo/issue/1/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=fixed&close=completed" ) ) .await;
236
assert! ( r.body.contains ( " closed" ) , " {}" , r.body) ;
237
assert! ( get ( app, " /andy/demo/issues?state=closed" , None ) .await.body.contains ( " Crash on start" ) ) ;
238
239
240
let csrf_a = csrf_of ( & get ( app, " /andy/demo/issues/new" , Some ( & andy) ) .await.body) ;
241
assert_eq! (
242
post ( app, " /andy/demo/issues/new" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=Typo&body=" ) ) .await.location.as_deref ( ) ,
243
Some ( " /andy/demo/issue/2" )
244
) ;
245
let work = tempfile:: tempdir( ) .unwrap ( ) ;
246
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
247
client.config.set ( " remote.origin.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
248
client.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
249
client.save_config ( ) .unwrap ( ) ;
250
write ( work.path ( ) , " a.txt" , b " a\n " ) ;
251
client.snapshot ( " t" ) .unwrap ( ) ;
252
client.commit ( " Fix the typo\n \n Closes #2" , & [ ] ) .unwrap ( ) ;
253
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
92cea629 Andy McCall 25 days ago ↶
254
255
let repo_id = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) .id;
256
let mut issue2 = db:: issues:: get( & app.db, repo_id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
257
for _ in 0 .. 200 {
258
if issue2.state = = " closed" {
259
break ;
260
}
261
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
262
issue2 = db:: issues:: get( & app.db, repo_id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
263
}
18e381ea Andy McCall 1 month ago ↶
264
assert_eq! ( issue2.state, " closed" , " closed by the landed change" ) ;
265
assert! ( get ( app, " /andy/demo/issue/2" , None ) .await.body.contains ( " closed by change" ) ) ;
266
267
268
client.bookmark_create ( " feat" , None ) .unwrap ( ) ;
269
client.bookmark_switch ( " feat" , false ) .unwrap ( ) ;
270
write ( work.path ( ) , " b.txt" , b " b\n " ) ;
271
client.snapshot ( " t" ) .unwrap ( ) ;
272
client.commit ( " feat" , & [ ] ) .unwrap ( ) ;
273
assert! ( client.push_bookmark ( " origin" , " feat" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
274
let csrf_a = csrf_of ( & get ( app, " /andy/demo/proposals/new?source=feat&target=main" , Some ( & andy) ) .await.body) ;
275
let r = post ( app, " /andy/demo/proposals/new" , Some ( & andy) , & format! ( " _csrf={csrf_a} &title=Feat&target=main&source=feat" ) ) .await;
276
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) , " {}" , r.body) ;
277
assert_eq! ( get ( app, " /andy/demo/issue/3" , None ) .await.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) ) ;
278
279
let r = post ( app, " /andy/demo/issue/2/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=see+%233" ) ) .await;
280
assert! ( r.body.contains ( " href=\" /andy/demo/proposal/3\" " ) , " {}" , r.body) ;
78001759 Andy McCall 1 month ago ↶
281
282
client.bookmark_switch ( " main" , false ) .unwrap ( ) ;
283
write ( work.path ( ) , " c.txt" , b " c\n " ) ;
284
client.snapshot ( " t" ) .unwrap ( ) ;
285
let r = client.commit ( " Tidy up (refs #1, see #3)" , & [ ] ) .unwrap ( ) ;
286
assert! ( client.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
287
let short = & r.to_hex ( ) [ .. 12 ] ;
92cea629 Andy McCall 25 days ago ↶
288
289
290
let mut one = get ( app, " /andy/demo/issue/1" , None ) .await.body;
291
for _ in 0 .. 200 {
292
if one.contains ( " referenced by change" ) {
293
break ;
294
}
295
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
296
one = get ( app, " /andy/demo/issue/1" , None ) .await.body;
297
}
78001759 Andy McCall 1 month ago ↶
298
assert! (
299
one.contains ( " referenced by change" ) & & one.contains ( & format! ( " href=\" /andy/demo/change/{short} \" " ) ) & & one.contains ( " on main" ) ,
300
" {one}"
301
) ;
302
let three = get ( app, " /andy/demo/proposal/3" , Some ( & andy) ) .await.body;
303
assert! ( three.contains ( " referenced by change" ) & & three.contains ( short) , " proposals too" ) ;
304
assert! ( ! get ( app, " /andy/demo/issue/2" , None ) .await.body.contains ( " referenced by change" ) , " a closing change records only that" ) ;
8bb7baeb Andy McCall 10 days ago ↶
305
306
client.bookmark_create ( " again" , None ) .unwrap ( ) ;
307
assert! ( client.push_bookmark ( " origin" , " again" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
308
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 1500 ) ) .await;
309
let recorded: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM issue_events WHERE event = 'referenced by change' AND detail LIKE $1" )
310
.bind ( format! ( " {short} %" ) )
311
.fetch_one ( & app.db)
312
.await
313
.unwrap ( ) ;
314
assert_eq! ( recorded, 1 , " once per change, whichever bookmarks it is on" ) ;
78001759 Andy McCall 1 month ago ↶
315
4f840409 Andy McCall 11 days ago ↶
316
317
318
let issue1 = db:: issues:: get( & app.db, repo_id, 1 ) .await.unwrap ( ) .unwrap ( ) ;
319
let day = 1_792_540_800_000 i64 ;
320
for ( i, at) in [ day + 1_000 , day + 2_000 , day + 3_000 , day - 86_400_000 ] .into_iter ( ) .enumerate ( ) {
321
sqlx:: query( " INSERT INTO issue_events (issue_id, at_ms, actor, event, detail) VALUES ($1, $2, 'andy', 'referenced by change', $3)" )
322
.bind ( issue1.id)
323
.bind ( at)
324
.bind ( format! ( " {:012x} on main" , 0xabc000 + i) )
325
.execute ( & app.db)
326
.await
327
.unwrap ( ) ;
328
}
8bb7baeb Andy McCall 10 days ago ↶
329
330
sqlx:: query( " INSERT INTO issue_events (issue_id, at_ms, actor, event, detail) VALUES ($1, $2, 'andy', 'referenced by change', $3)" )
331
.bind ( issue1.id)
332
.bind ( day + 4_000 )
333
.bind ( format! ( " {:012x} on develop" , 0xabc000 ) )
334
.execute ( & app.db)
335
.await
336
.unwrap ( ) ;
4f840409 Andy McCall 11 days ago ↶
337
let one = get ( app, " /andy/demo/issue/1" , None ) .await.body;
338
assert! ( one.contains ( " referenced by 3 changes on 21 Oct 2026" ) , " {one}" ) ;
8bb7baeb Andy McCall 10 days ago ↶
339
assert_eq! ( one.matches ( & format! ( " change/{:012x}\" " , 0xabc000 ) ) .count ( ) , 1 , " each change once" ) ;
340
assert! ( ! one.contains ( " mute timeline" ) , " the events have no rule of their own (#318)" ) ;
4f840409 Andy McCall 11 days ago ↶
341
assert! ( one.contains ( " refs-day\" open><summary>referenced by 3 changes" ) , " the newest box is open: {one}" ) ;
342
assert! ( ! one.contains ( " changes on 20 Oct 2026" ) , " a lone reference is a line" ) ;
343
assert! ( one.contains ( & format! ( " href=\" /andy/demo/change/{short} \" " ) ) , " today's own stays" ) ;
344
eb0c44a9 Andy McCall 1 month ago ↶
345
346
let part = get ( app, " /notifications?reason=participating" , Some ( & bob) ) .await.body;
347
assert! ( part.contains ( " andy/demo#1" ) & & ! part.contains ( " andy/demo#3: Feat" ) , " {part}" ) ;
348
let watch = get ( app, " /notifications?reason=watching" , Some ( & bob) ) .await.body;
349
assert! ( watch.contains ( " andy/demo#3: Feat" ) & & ! watch.contains ( " Crash on start" ) , " {watch}" ) ;
350
let props = get ( app, " /notifications?kind=proposal" , Some ( & bob) ) .await.body;
351
assert! ( props.contains ( " andy/demo#3: Feat" ) & & ! props.contains ( " andy/demo#1" ) , " kind filter" ) ;
352
assert! ( get ( app, " /notifications?repo=andy/demo" , Some ( & bob) ) .await.body.contains ( " andy/demo#3: Feat" ) ) ;
353
assert! ( get ( app, " /notifications?repo=nobody/none" , Some ( & bob) ) .await.body.contains ( " Nothing matches these filters" ) ) ;
354
355
let inbox = get ( app, " /notifications" , Some ( & bob) ) .await.body;
356
assert_eq! ( inbox.matches ( " data-row-thread=\" /andy/demo/issue/1\" " ) .count ( ) , 1 , " {inbox}" ) ;
357
assert! ( inbox.contains ( " updates" ) , " the count of a busy thread" ) ;
358
359
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
360
let newest: i64 = sqlx:: query_scalar(
3b1bc27f Andy McCall 20 days ago
361
" 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 ↶
362
)
363
.bind ( bob_id)
364
.fetch_one ( & app.db)
365
.await
366
.unwrap ( ) ;
367
let before = db:: notify:: unread( & app.db, bob_id) .await.unwrap ( ) ;
368
let r = get ( app, & format! ( " /notifications/open/{newest} " ) , Some ( & bob) ) .await;
369
assert_eq! ( r.location.as_deref ( ) , Some ( " /andy/demo/proposal/3" ) ) ;
370
assert! ( db:: notify:: unread( & app.db, bob_id) .await.unwrap ( ) < before) ;
371
assert_eq! ( get ( app, & format! ( " /notifications/open/{newest} " ) , Some ( & andy) ) .await.status, StatusCode:: NOT_FOUND , " only your own" ) ;
372
373
let csrf_b = csrf_of ( & get ( app, " /notifications" , Some ( & bob) ) .await.body) ;
374
let r = post (
375
app,
376
" /notifications/thread" ,
377
Some ( & bob) ,
378
& format! ( " _csrf={csrf_b} &thread=%2Fandy%2Fdemo%2Fissue%2F1&action=done&back=%2Fnotifications" ) ,
379
)
380
.await;
381
assert_eq! ( r.location.as_deref ( ) , Some ( " /notifications" ) ) ;
382
assert! ( ! get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) ) ;
383
assert! ( get ( app, " /notifications?view=done" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) ) ;
384
post ( app, " /andy/demo/issue/1/comment" , Some ( & andy) , & format! ( " _csrf={csrf_a} &body=one+more+thing" ) ) .await;
385
assert! ( get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " Crash on start" ) , " back with the new comment" ) ;
386
let r = post (
387
app,
388
" /notifications/thread" ,
389
Some ( & bob) ,
390
& format! ( " _csrf={csrf_b} &thread=%2Fandy%2Fdemo%2Fissue%2F1&action=read&back=https%3A%2F%2Fevil.example" ) ,
391
)
392
.await;
393
assert_eq! ( r.location.as_deref ( ) , Some ( " /notifications" ) , " only back to the inbox" ) ;
394
18e381ea Andy McCall 1 month ago ↶
395
396
assert! ( get ( app, " /notifications" , Some ( & bob) ) .await.body.contains ( " andy/demo#3: Feat" ) ) ;
397
let csrf_b = csrf_of ( & get ( app, " /notifications" , Some ( & bob) ) .await.body) ;
398
let r = post ( app, " /notifications/read" , Some ( & bob) , & format! ( " _csrf={csrf_b} " ) ) .await;
399
assert! ( r.body.contains ( " 0 unread" ) , " {}" , r.body) ;
400
401
let r = post ( app, " /settings/notifications" , Some ( & bob) , & format! ( " _csrf={csrf_b} &email=1" ) ) .await;
402
assert! ( r.body.contains ( " Email notifications are on" ) ) ;
403
let key: String = sqlx:: query_scalar( " SELECT unsubscribe_key FROM accounts WHERE username = 'bob'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
404
assert_eq! ( get ( app, & format! ( " /settings/notifications?unsubscribe={key} " ) , None ) .await.status, StatusCode:: OK ) ;
405
let on: bool = sqlx:: query_scalar( " SELECT email_notifications FROM accounts WHERE username = 'bob'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
406
assert! ( ! on) ;
407
}
25ab8c73 Andy McCall 1 month ago ↶
408
409
# [ tokio ::test ( flavor = " multi_thread" ) ]
410
async fn dashboard_shows_what_needs_the_viewer ( ) {
411
let h = hub ( ) .await;
412
let app = & h.app;
413
let andy = register ( app, " andy" ) .await;
414
let bob = register ( app, " bob" ) .await;
415
for ( name, vis) in [ ( " demo" , hub_api:: Visibility:: Public) , ( " secret" , hub_api:: Visibility:: Private) ] {
416
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
417
db:: repos:: create( & app.db, " andy" , name, " " , vis) .await.unwrap ( ) ;
418
}
419
let demo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
420
let secret = db:: repos:: get( & app.db, " andy" , " secret" ) .await.unwrap ( ) .unwrap ( ) ;
421
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
422
for ( repo, title) in [ ( demo.id, " Faster startup" ) , ( secret.id, " Hidden work" ) ] {
423
let p = db:: proposals:: create(
424
& app.db,
425
db:: proposals:: NewProposal {
426
repo_id: repo,
427
title,
428
body: " " ,
429
author_id: Some ( bob_id) ,
430
author: " bob" ,
431
draft: false ,
432
target: " main" ,
433
source_kind: " bookmark" ,
434
source_repo_id: Some ( repo) ,
435
source_bookmark: " fast" ,
436
source_changes: " " ,
437
} ,
438
)
439
.await
440
.unwrap ( ) ;
441
db:: proposals:: add_event( & app.db, p.id, " bob" , " opened" , " " ) .await.unwrap ( ) ;
442
}
443
444
445
let anon = get ( app, " /" , None ) .await;
446
assert! ( anon.body.contains ( " andy /" ) & & ! anon.body.contains ( " Dashboard <small>" ) , " {}" , anon.body) ;
447
448
449
let page = get ( app, " /" , Some ( & andy) ) .await;
450
assert_eq! ( page.status, StatusCode:: OK ) ;
f8cf54e9 Andy McCall 22 days ago ↶
451
assert! ( page.body.contains ( " Waiting for your review</h2><span class=\" count-chip\" >2</span>" ) , " {}" , page.body) ;
25ab8c73 Andy McCall 1 month ago ↶
452
assert! ( page.body.contains ( " /andy/demo/proposal/1" ) & & page.body.contains ( " /andy/secret/proposal/1" ) ) ;
453
assert! ( page.body.contains ( " Your repositories" ) ) ;
454
assert! ( page.body.contains ( " opened proposal" ) , " activity feed: {}" , page.body) ;
455
17ca6cdd Andy McCall 22 days ago ↶
456
457
458
assert! ( page.body.contains ( r # "<canvas id="backdrop" aria-hidden="true">"# ) ) ;
459
let start = page.body.find ( r # "id="backdrop-data">"# ) .unwrap ( ) + r # "id="backdrop-data">"# .len ( ) ;
460
let data: serde_json:: Value = serde_json:: from_str( & page.body[ start.. start + page.body[ start.. ] .find ( " </script>" ) .unwrap ( ) ] ) .unwrap ( ) ;
461
let repos: Vec< & str > = data[ " repos" ] .as_array ( ) .unwrap ( ) .iter ( ) .filter_map ( | r | r.as_str ( ) ) .collect ( ) ;
462
assert! ( repos.contains ( & " andy/demo" ) & & repos.contains ( & " andy/secret" ) , " {data}" ) ;
463
let events = data[ " events" ] .as_array ( ) .unwrap ( ) ;
464
assert! ( events.iter ( ) .any ( | e | e[ 1 ] = = 1 ) , " the proposals opened are marks: {data}" ) ;
465
assert! ( events.iter ( ) .all ( | e | e[ 0 ] .as_u64 ( ) .unwrap ( ) < repos.len ( ) as u64 ) ) ;
33bd0bd4 Andy McCall 22 days ago ↶
466
467
let res = oxygenhub:: router( app.clone ( ) )
468
.oneshot ( Request:: builder( ) .uri ( " /" ) .header ( header:: COOKIE , format! ( " oxyhub={andy} " ) ) .body ( Body:: empty( ) ) .unwrap ( ) )
469
.await
470
.unwrap ( ) ;
471
assert_eq! ( res.headers ( ) [ header:: CACHE_CONTROL ] , " private, no-store" ) ;
6246d1e5 Andy McCall 22 days ago ↶
472
473
assert! ( get ( app, " /login" , None )
474
.await
475
.body
476
.contains ( r # "<script type="application/json" id="backdrop-data">{"scene":"mark"}</script>"# ) ) ;
477
let lost = get ( app, " /no/such/page/here" , None ) .await;
478
assert_eq! ( lost.status, StatusCode:: NOT_FOUND ) ;
479
assert! ( lost.body.contains ( r # "{"scene":"adrift"}"# ) ) ;
0a3b244b Andy McCall 22 days ago ↶
480
481
let sky_of = | body : & str | {
482
let start = body.find ( r # "id="backdrop-data">"# ) .unwrap ( ) + r # "id="backdrop-data">"# .len ( ) ;
483
body[ start.. start + body[ start.. ] .find ( " </script>" ) .unwrap ( ) ] .to_string ( )
484
} ;
485
let hub_sky = sky_of ( & anon.body) ;
486
assert! ( hub_sky.contains ( " andy/demo" ) & & ! hub_sky.contains ( " andy/secret" ) , " {hub_sky}" ) ;
487
488
489
let explore = get ( app, " /explore" , Some ( & andy) ) .await;
490
assert! ( explore.body.contains ( " andy/secret" ) , " the list shows him his private repository" ) ;
491
let hub_sky = sky_of ( & explore.body) ;
492
assert! ( hub_sky.contains ( " andy/demo" ) & & ! hub_sky.contains ( " andy/secret" ) , " {hub_sky}" ) ;
493
assert! ( ! get ( app, " /andy" , None ) .await.body.contains ( " backdrop-data" ) ) ;
17ca6cdd Andy McCall 22 days ago ↶
494
495
let res = oxygenhub:: router( app.clone ( ) )
496
.oneshot ( Request:: builder( ) .uri ( " /static/backdrop/backdrop_bg.wasm" ) .body ( Body:: empty( ) ) .unwrap ( ) )
497
.await
498
.unwrap ( ) ;
499
assert_eq! ( res.headers ( ) [ header:: CONTENT_TYPE ] , " application/wasm" ) ;
500
assert! ( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .starts_with ( b " \0 asm" ) ) ;
da8f7826 Andy McCall 22 days ago ↶
501
502
assert! ( sky_of ( & page.body) .contains ( r # ""live":"mine""# ) & & hub_sky.contains ( r # ""live":"hub""# ) ) ;
503
let since = db:: now_ms( ) - 3_600_000 ;
504
let arrivals = | res : Res| -> Vec< String> {
505
assert_eq! ( res.status, StatusCode:: OK , " {}" , res.body) ;
506
let v: serde_json:: Value = serde_json:: from_str( & res.body) .unwrap ( ) ;
507
v[ " events" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | e | e[ 0 ] .as_str ( ) .unwrap ( ) .to_string ( ) ) .collect ( )
508
} ;
509
let mine = arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={since} " ) , Some ( & andy) ) .await) ;
510
assert! ( mine.iter ( ) .any ( | r | r = = " andy/demo" ) & & mine.iter ( ) .any ( | r | r = = " andy/secret" ) , " {mine:?}" ) ;
511
let hub = arrivals ( get ( app, & format! ( " /sky/arrivals?scene=hub&since={since} " ) , Some ( & andy) ) .await) ;
512
assert! ( hub.iter ( ) .any ( | r | r = = " andy/demo" ) & & ! hub.iter ( ) .any ( | r | r = = " andy/secret" ) , " the hub's sky is public only: {hub:?}" ) ;
513
let anon_hub = arrivals ( get ( app, & format! ( " /sky/arrivals?scene=hub&since={since} " ) , None ) .await) ;
514
assert! ( ! anon_hub.iter ( ) .any ( | r | r = = " andy/secret" ) , " {anon_hub:?}" ) ;
515
assert! ( arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={since} " ) , None ) .await) .is_empty ( ) , " signed out has no `mine`" ) ;
516
assert! (
517
arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={since} " ) , Some ( & bob) ) .await) .is_empty ( ) ,
518
" Bob's sky has none of Andy's"
519
) ;
520
assert! (
521
arrivals ( get ( app, & format! ( " /sky/arrivals?scene=mine&since={} " , db:: now_ms( ) ) , Some ( & andy) ) .await) .is_empty ( ) ,
522
" nothing since now"
523
) ;
17ca6cdd Andy McCall 22 days ago ↶
524
25ab8c73 Andy McCall 1 month ago ↶
525
526
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
527
let p = db:: proposals:: get( & app.db, demo.id, 1 ) .await.unwrap ( ) .unwrap ( ) ;
528
db:: proposals:: add_review( & app.db, p.id, andy_id, " andy" , " approve" , " " ) .await.unwrap ( ) ;
f8cf54e9 Andy McCall 22 days ago ↶
529
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 ↶
530
531
532
let page = get ( app, " /" , Some ( & bob) ) .await;
33bd0bd4 Andy McCall 22 days ago ↶
533
534
535
let start = page.body.find ( r # "id="backdrop-data">"# ) .unwrap ( ) + r # "id="backdrop-data">"# .len ( ) ;
536
let sky = & page.body[ start.. start + page.body[ start.. ] .find ( " </script>" ) .unwrap ( ) ] ;
537
assert! ( ! sky.contains ( " andy/secret" ) & & ! sky.contains ( " andy/demo" ) , " {sky}" ) ;
f8cf54e9 Andy McCall 22 days ago ↶
538
assert! ( page.body.contains ( " Your proposals</h2><span class=\" count-chip\" >1</span>" ) , " {}" , page.body) ;
25ab8c73 Andy McCall 1 month ago ↶
539
assert! ( ! page.body.contains ( " Hidden work" ) ) ;
f8cf54e9 Andy McCall 22 days ago ↶
540
assert! ( page.body.contains ( " Waiting for your review</h2><span class=\" count-chip\" >0</span>" ) ) ;
25ab8c73 Andy McCall 1 month ago ↶
541
assert! ( page.body.contains ( " Faster startup" ) ) ;
542
let feed = & page.body[ page.body.find ( " Recent activity" ) .unwrap ( ) .. ] ;
543
assert! ( ! feed.contains ( " andy/secret" ) , " private activity leaked: {feed}" ) ;
544
545
546
assert! ( get ( app, " /explore" , Some ( & bob) ) .await.body.contains ( " andy /" ) ) ;
547
}
4edcf240 Andy McCall 1 month ago ↶
548
549
# [ tokio ::test ( flavor = " multi_thread" ) ]
550
async fn empty_repository_shows_a_quickstart ( ) {
551
let h = hub ( ) .await;
552
let app = & h.app;
553
let andy = register ( app, " andy" ) .await;
554
let bob = register ( app, " bob" ) .await;
555
app.repos.create ( & RepoName:: parse( " andy/fresh" ) .unwrap ( ) , None ) .unwrap ( ) ;
556
db:: repos:: create( & app.db, " andy" , " fresh" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
557
558
let page = get ( app, " /andy/fresh" , Some ( & andy) ) .await;
559
assert! ( page.body.contains ( " This repository is empty" ) , " {}" , page.body) ;
560
let clone_url = format! ( " {} /andy/fresh" , h.base) ;
561
assert! ( page.body.contains ( & format! ( " oxy remote add hub {clone_url} " ) ) , " {}" , page.body) ;
562
assert! ( page.body.contains ( " oxy config remote.hub.token YOUR-TOKEN" ) ) ;
563
assert! ( page.body.contains ( " oxy push hub" ) ) ;
564
assert! (
565
page.body.contains ( " mkdir fresh && cd fresh" ) | | page.body.contains ( " mkdir fresh && cd fresh" ) ,
566
" {}" ,
567
page.body
568
) ;
569
assert! ( page.body.contains ( " oxy git import --branch main" ) ) ;
570
assert! ( ! page.body.contains ( " oxyhub-ca.pem" ) , " no certificate step without a self-signed certificate" ) ;
571
572
573
let page = get ( app, " /andy/fresh" , Some ( & bob) ) .await;
574
assert! ( page.body.contains ( " Nothing has been pushed yet" ) & & ! page.body.contains ( " oxy push hub" ) , " {}" , page.body) ;
575
576
577
let work = tempfile:: tempdir( ) .unwrap ( ) ;
578
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
579
client.config.set ( " remote.hub.url" , & clone_url) .unwrap ( ) ;
580
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
581
client.save_config ( ) .unwrap ( ) ;
582
write ( work.path ( ) , " README.md" , b " # fresh\n " ) ;
583
client.commit ( " First change" , & [ ] ) .unwrap ( ) ;
584
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
585
let page = get ( app, " /andy/fresh" , Some ( & andy) ) .await;
586
assert! ( ! page.body.contains ( " This repository is empty" ) & & page.body.contains ( " README.md" ) , " {}" , page.body) ;
587
}
c40c2d3e Andy McCall 1 month ago ↶
588
589
# [ tokio ::test ( flavor = " multi_thread" ) ]
590
async fn theme_is_chosen_in_settings_and_remembered_in_a_cookie ( ) {
591
let h = hub ( ) .await;
592
let app = & h.app;
593
let andy = register ( app, " andy" ) .await;
594
let page = get ( app, " /settings" , Some ( & andy) ) .await;
595
assert! ( page.body.contains ( " Appearance" ) ) ;
d98a93c1 Andy McCall 1 month ago ↶
596
assert! ( page.body.contains ( r # "value="light" checked"# ) , " light by default: {}" , page.body) ;
c40c2d3e Andy McCall 1 month ago ↶
597
assert! ( page.body.contains ( " data-theme-toggle" ) , " the header has the switch" ) ;
598
let csrf = csrf_of ( & page.body) ;
d98a93c1 Andy McCall 1 month ago ↶
599
let r = post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=dark" ) ) .await;
c40c2d3e Andy McCall 1 month ago ↶
600
assert_eq! ( r.status, StatusCode:: SEE_OTHER ) ;
d98a93c1 Andy McCall 1 month ago ↶
601
assert_eq! ( r.cookie.as_deref ( ) , Some ( " dark" ) ) ;
c40c2d3e Andy McCall 1 month ago ↶
602
assert_eq! ( r.location.as_deref ( ) , Some ( " /settings" ) ) ;
d98a93c1 Andy McCall 1 month ago ↶
603
let page = send_with_theme ( app, & andy, " dark" ) .await;
604
assert! ( page.contains ( r # "value="dark" checked"# ) , " the form shows the cookie's theme" ) ;
c40c2d3e Andy McCall 1 month ago ↶
605
assert_eq! ( post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=purple" ) ) .await.status, StatusCode:: BAD_REQUEST ) ;
17ca6cdd Andy McCall 22 days ago ↶
606
607
608
609
assert! ( get ( app, " /settings" , Some ( & andy) ) .await.body.contains ( r # "name="backdrop" value="moving" checked"# ) ) ;
610
let res = oxygenhub:: router( app.clone ( ) )
611
.oneshot (
612
Request:: builder( )
613
.method ( " POST" )
614
.uri ( " /settings/appearance" )
615
.header ( header:: COOKIE , format! ( " oxyhub={andy} " ) )
616
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
617
.body ( Body:: from( format! ( " _csrf={csrf} &theme=light&backdrop=still" ) ) )
618
.unwrap ( ) ,
619
)
620
.await
621
.unwrap ( ) ;
622
let cookies: Vec< & str > = res.headers ( ) .get_all ( header:: SET_COOKIE ) .iter ( ) .filter_map ( | v | v.to_str ( ) .ok ( ) ) .collect ( ) ;
623
assert! (
624
cookies.iter ( ) .any ( | c | c.starts_with ( " theme=light;" ) ) & & cookies.iter ( ) .any ( | c | c.starts_with ( " backdrop=still;" ) ) ,
625
" {cookies:?}"
626
) ;
627
assert_eq! (
628
post ( app, " /settings/appearance" , Some ( & andy) , & format! ( " _csrf={csrf} &theme=light&backdrop=wild" ) ) .await.status,
629
StatusCode:: BAD_REQUEST
630
) ;
c40c2d3e Andy McCall 1 month ago ↶
631
assert_eq! ( post ( app, " /settings/appearance" , Some ( & andy) , " _csrf=nope&theme=light" ) .await.status, StatusCode:: BAD_REQUEST ) ;
632
}
633
634
async fn send_with_theme ( app : & Shared, session : & str , theme : & str ) -> String {
635
let req =
636
Request:: builder( ) .uri ( " /settings" ) .header ( header:: COOKIE , format! ( " oxyhub={session} ; theme={theme} " ) ) .body ( Body:: empty( ) ) .unwrap ( ) ;
637
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
638
String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( )
639
}
10174045 Andy McCall 1 month ago ↶
640
641
# [ tokio ::test ( flavor = " multi_thread" ) ]
642
async fn profile_and_organisation_pages ( ) {
643
let h = hub ( ) .await;
644
let app = & h.app;
645
let andy = register ( app, " andy" ) .await;
646
let bob = register ( app, " bob" ) .await;
647
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
648
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
649
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
650
db:: repos:: create( & app.db, " andy" , " demo" , " A demo" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
651
app.repos.create ( & RepoName:: parse( " andy/hidden" ) .unwrap ( ) , None ) .unwrap ( ) ;
652
db:: repos:: create( & app.db, " andy" , " hidden" , " " , hub_api:: Visibility:: Private) .await.unwrap ( ) ;
653
654
655
let csrf = csrf_of ( & get ( app, " /settings" , Some ( & andy) ) .await.body) ;
656
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;
657
assert! ( r.body.contains ( " must be an http" ) , " {}" , r.body) ;
658
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;
659
assert! ( r.body.contains ( " Profile saved" ) , " {}" , r.body) ;
660
661
let org = db:: accounts:: create_org( & app.db, " retro" , " Retro Club" , andy_id) .await.unwrap ( ) ;
662
db:: accounts:: set_org_member( & app.db, org.id, bob_id, " member" ) .await.unwrap ( ) ;
663
let team = db:: accounts:: create_team( & app.db, org.id, " core" ) .await.unwrap ( ) ;
664
db:: accounts:: add_team_member( & app.db, team.id, bob_id) .await.unwrap ( ) ;
665
666
let page = get ( app, " /andy" , Some ( & bob) ) .await;
667
assert_eq! ( page.status, StatusCode:: OK ) ;
668
for want in [ " Andy McCall" , " Earth" , r # "href="https://example.org""# , " /avatar/andy" , " Signing keys" , " /retro" , " demo" ] {
669
assert! ( page.body.contains ( want) , " profile lacks {want}: {}" , page.body) ;
670
}
671
assert! ( ! page.body.contains ( " <b>oxy</b>" ) , " the bio is escaped" ) ;
672
assert! ( ! page.body.contains ( " [email protected] " ) , " the email stays private" ) ;
673
assert! ( ! page.body.contains ( " /andy/hidden" ) , " bob cannot see the private repository" ) ;
b951c587 Andy McCall 10 days ago ↶
674
assert! ( ! page.body.contains ( " Edit profile" ) , " only the owner gets the edit link" ) ;
675
let own = get ( app, " /andy" , Some ( & andy) ) .await.body;
676
assert! ( own.contains ( " /andy/hidden" ) , " andy sees his own private repository" ) ;
677
678
assert! (
679
own.contains ( r # "<nav class="profile-actions""# ) & & own.contains ( r # "class="go wide" href="/settings">Edit profile</a>"# ) ,
680
" {own}"
681
) ;
10174045 Andy McCall 1 month ago ↶
682
683
let page = get ( app, " /retro" , Some ( & bob) ) .await;
684
assert_eq! ( page.status, StatusCode:: OK ) ;
61ecbd29 Andy McCall 22 days ago ↶
685
for want in [ " Retro Club" , " organisation" , r # "Members <span class="count-chip">2</span>"# , " core" , " 1 member" ] {
10174045 Andy McCall 1 month ago ↶
686
assert! ( page.body.contains ( want) , " org page lacks {want}: {}" , page.body) ;
687
}
688
assert! ( ! page.body.contains ( " /orgs/retro/settings" ) , " bob is not an owner" ) ;
689
assert! ( get ( app, " /retro" , Some ( & andy) ) .await.body.contains ( " /orgs/retro/settings" ) ) ;
690
691
let svg = get ( app, " /avatar/andy" , None ) .await;
692
assert_eq! ( svg.status, StatusCode:: OK ) ;
693
assert! ( svg.body.starts_with ( " <svg" ) ) ;
694
assert_eq! ( svg.body, get ( app, " /avatar/ANDY" , None ) .await.body) ;
695
assert_eq! ( get ( app, " /nobody-here" , None ) .await.status, StatusCode:: NOT_FOUND ) ;
696
}
9df855f2 Andy McCall 1 month ago ↶
697
698
# [ tokio ::test ( flavor = " multi_thread" ) ]
699
async fn diffs_mark_words_hide_whitespace_and_collapse ( ) {
700
let h = hub ( ) .await;
701
let app = & h.app;
702
let andy = register ( app, " andy" ) .await;
703
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
704
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
705
let work = tempfile:: tempdir( ) .unwrap ( ) ;
706
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
707
client.config.set ( " remote.hub.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
708
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
709
client.save_config ( ) .unwrap ( ) ;
710
write ( work.path ( ) , " main.rs" , b " fn main() {\n setup();\n let answer = 42;\n }\n " ) ;
711
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
712
write ( work.path ( ) , " main.rs" , b " fn main() {\n setup();\n let answer = 43;\n }\n " ) ;
713
let second = client.commit ( " reindent and change the answer" , & [ ] ) .unwrap ( ) ;
714
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
715
716
let url = format! ( " /andy/demo/change/{} " , second.to_hex ( ) ) ;
717
let page = get ( app, & url, Some ( & andy) ) .await;
718
assert_eq! ( page.status, StatusCode:: OK ) ;
719
assert! ( page.body.contains ( r # "<details class="file-diff""# ) , " files collapse: {}" , page.body) ;
720
assert! ( page.body.contains ( r # "<span class="wx">43</span>"# ) , " the changed word is marked: {}" , page.body) ;
721
assert! ( page.body.contains ( " hide whitespace changes" ) ) ;
722
assert! ( page.body.contains ( " data-diff-mode=\" split\" " ) ) ;
723
let adds = | b : & str | b.matches ( r # "<tr class="add">"# ) .count ( ) ;
724
assert_eq! ( adds ( & page.body) , 2 , " re-indent and answer both changed" ) ;
725
726
let quiet = get ( app, & format! ( " {url} ?w=1" ) , Some ( & andy) ) .await;
727
assert_eq! ( adds ( & quiet.body) , 1 , " only the answer changed ignoring whitespace: {}" , quiet.body) ;
728
assert! ( quiet.body.contains ( " show whitespace changes" ) ) ;
729
730
assert! ( quiet.body.contains ( " ?w=0" ) , " the toggle leads back" ) ;
731
}
07b1a5bb Andy McCall 1 month ago ↶
732
733
# [ tokio ::test ( flavor = " multi_thread" ) ]
734
async fn fork_and_propose_upstream ( ) {
735
let h = hub ( ) .await;
736
let app = & h.app;
737
let andy = register ( app, " andy" ) .await;
738
let bob = register ( app, " bob" ) .await;
739
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
740
db:: repos:: create( & app.db, " andy" , " demo" , " The demo" , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
741
let work = tempfile:: tempdir( ) .unwrap ( ) ;
742
let mut client = Repo:: init( work.path ( ) ) .unwrap ( ) ;
743
client.config.set ( " remote.hub.url" , & format! ( " {} /andy/demo" , h.base) ) .unwrap ( ) ;
744
client.config.set ( " remote.hub.token" , " site" ) .unwrap ( ) ;
745
client.save_config ( ) .unwrap ( ) ;
746
write ( work.path ( ) , " README.md" , b " # demo\n " ) ;
747
client.commit ( " first" , & [ ] ) .unwrap ( ) ;
748
assert! ( client.push ( " hub" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
749
750
751
let page = get ( app, " /andy/demo" , Some ( & bob) ) .await;
752
assert! ( page.body.contains ( " /andy/demo/fork" ) , " a fork button: {}" , page.body) ;
753
let csrf = csrf_of ( & get ( app, " /andy/demo/fork" , Some ( & bob) ) .await.body) ;
754
let r = post ( app, " /andy/demo/fork" , Some ( & bob) , & format! ( " _csrf={csrf} &owner=andy&name=other" ) ) .await;
755
assert! ( r.body.contains ( " cannot create repositories under that owner" ) , " {}" , r.body) ;
756
let r = post ( app, " /andy/demo/fork" , Some ( & bob) , & format! ( " _csrf={csrf} &owner=bob&name=demo" ) ) .await;
757
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
758
assert_eq! ( r.location.as_deref ( ) , Some ( " /bob/demo" ) ) ;
759
let forked = get ( app, " /bob/demo" , Some ( & bob) ) .await;
760
assert! ( forked.body.contains ( " README.md" ) & & forked.body.contains ( " forked from" ) , " {}" , forked.body) ;
761
assert! ( forked.body.contains ( " propose to andy/demo" ) ) ;
762
assert! ( get ( app, " /andy/demo" , None ) .await.body.contains ( " 1 fork" ) ) ;
763
assert! ( get ( app, " /andy/demo/forks" , None ) .await.body.contains ( " /bob/demo" ) ) ;
764
let fork_row = db:: repos:: get( & app.db, " bob" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
765
let up_row = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
766
assert_eq! ( fork_row.forked_from, Some ( up_row.id) ) ;
767
assert_eq! ( fork_row.description, " The demo" ) ;
768
let fork_repo = app.repos.open ( & RepoName:: parse( " bob/demo" ) .unwrap ( ) ) .unwrap ( ) ;
769
let up_repo = app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
770
assert_ne! ( fork_repo.id ( ) .unwrap ( ) , up_repo.id ( ) .unwrap ( ) , " a fork is a repository of its own" ) ;
771
assert_eq! ( fork_repo.bookmark_heads ( ) .unwrap ( ) , up_repo.bookmark_heads ( ) .unwrap ( ) ) ;
772
assert_eq! ( fork_repo.fsck ( ) .unwrap ( ) .errors ( ) , 0 ) ;
773
774
775
let bw = tempfile:: tempdir( ) .unwrap ( ) ;
776
let mut mine = Repo:: clone( & format! ( " {} /bob/demo" , h.base) , & bw.path ( ) .join ( " c" ) ) .unwrap ( ) ;
777
mine.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
778
mine.save_config ( ) .unwrap ( ) ;
779
mine.bookmark_create ( " feat" , None ) .unwrap ( ) ;
780
mine.bookmark_switch ( " feat" , false ) .unwrap ( ) ;
781
write ( & bw.path ( ) .join ( " c" ) , " feature.txt" , b " new\n " ) ;
782
mine.commit ( " Add a feature" , & [ ] ) .unwrap ( ) ;
783
assert! ( mine.push_bookmark ( " origin" , " feat" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
784
let csrf = csrf_of ( & get ( app, " /andy/demo/proposals/new?source=bob/demo:feat" , Some ( & bob) ) .await.body) ;
785
let r =
786
post ( app, " /andy/demo/proposals/new" , Some ( & bob) , & format! ( " _csrf={csrf} &title=From+my+fork&target=main&source=bob%2Fdemo%3Afeat" ) )
787
.await;
788
assert_eq! ( r.status, StatusCode:: SEE_OTHER , " {}" , r.body) ;
789
let p = get ( app, & r.location.unwrap ( ) , Some ( & andy) ) .await;
790
assert! ( p.body.contains ( " From my fork" ) & & p.body.contains ( " fork bob/demo" ) , " {}" , p.body) ;
791
let files = get ( app, " /andy/demo/proposal/1?tab=files" , Some ( & andy) ) .await;
792
assert! ( files.body.contains ( " feature.txt" ) , " {}" , files.body) ;
793
}
1553bfc9 Andy McCall 1 month ago ↶
794
795
# [ tokio ::test ( flavor = " multi_thread" ) ]
796
async fn stars_topics_and_explore ( ) {
797
let h = hub ( ) .await;
798
let app = & h.app;
799
let andy = register ( app, " andy" ) .await;
800
let bob = register ( app, " bob" ) .await;
801
for ( name, desc) in [ ( " alpha" , " An emulator" ) , ( " beta" , " A compiler" ) , ( " gamma" , " Retro games" ) ] {
802
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
803
db:: repos:: create( & app.db, " andy" , name, desc, hub_api:: Visibility:: Public) .await.unwrap ( ) ;
804
}
805
806
807
let csrf = csrf_of ( & get ( app, " /andy/gamma/settings" , Some ( & andy) ) .await.body) ;
808
let r = post (
809
app,
810
" /andy/gamma/settings" ,
811
Some ( & andy) ,
812
& format! ( " _csrf={csrf} &description=Retro+games&default_bookmark=main&visibility=public&topics=c%2B%2B" ) ,
813
)
814
.await;
c7067c7b Andy McCall 15 days ago
815
let r = settled ( app, r, Some ( & andy) , " /andy/gamma/settings/general" ) .await;
1553bfc9 Andy McCall 1 month ago ↶
816
assert! ( r.body.contains ( " is not a topic" ) , " {}" , r.body) ;
817
let r = post (
818
app,
819
" /andy/gamma/settings" ,
820
Some ( & andy) ,
821
& format! ( " _csrf={csrf} &description=Retro+games&default_bookmark=main&visibility=public&topics=Retro%2C+6502" ) ,
822
)
823
.await;
c7067c7b Andy McCall 15 days ago
824
let r = settled ( app, r, Some ( & andy) , " /andy/gamma/settings/general" ) .await;
1553bfc9 Andy McCall 1 month ago ↶
825
assert! ( r.body.contains ( " Saved" ) , " {}" , r.body) ;
826
assert_eq! ( db:: repos:: get( & app.db, " andy" , " gamma" ) .await.unwrap ( ) .unwrap ( ) .topics, " retro 6502" ) ;
827
let page = get ( app, " /andy/gamma" , None ) .await;
828
assert! ( page.body.contains ( r # "href="/explore?topic=retro""# ) , " {}" , page.body) ;
829
830
831
for ( who, repo) in [ ( & bob, " beta" ) , ( & bob, " gamma" ) , ( & andy, " gamma" ) , ( & andy, " alpha" ) ] {
832
let csrf = csrf_of ( & get ( app, & format! ( " /andy/{repo} " ) , Some ( who) ) .await.body) ;
833
assert_eq! ( post ( app, & format! ( " /andy/{repo} /star" ) , Some ( who) , & format! ( " _csrf={csrf} " ) ) .await.status, StatusCode:: SEE_OTHER ) ;
834
}
835
let csrf = csrf_of ( & get ( app, " /andy/alpha" , Some ( & andy) ) .await.body) ;
836
post ( app, " /andy/alpha/star" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
837
assert! ( get ( app, " /andy/gamma" , Some ( & bob) ) .await.body.contains ( " ★ UNSTAR" ) ) ;
7f283f8e Andy McCall 22 days ago ↶
838
assert! ( get ( app, " /andy/gamma" , None ) .await.body.contains ( r # "title="2 stars">"# ) ) ;
839
assert! ( get ( app, " /andy/alpha" , None ) .await.body.contains ( r # "title="0 stars">"# ) , " unstarred again" ) ;
1553bfc9 Andy McCall 1 month ago ↶
840
841
842
let order = | b : & str | {
843
[ " alpha" , " beta" , " gamma" ]
844
.into_iter ( )
845
.map ( | n | ( b.find ( & format! ( " /andy/{n} \" " ) ) .unwrap_or ( usize :: MAX ) , n) )
846
.collect:: < std:: collections:: BTreeMap< _ , _ > > ( )
847
.into_values ( )
848
.collect:: < Vec< _ > > ( )
849
} ;
850
assert_eq! ( order ( & get ( app, " /explore?sort=stars" , None ) .await.body) [ .. 2 ] , [ " gamma" , " beta" ] ) ;
851
assert_eq! ( order ( & get ( app, " /explore?sort=name" , None ) .await.body) , [ " alpha" , " beta" , " gamma" ] ) ;
852
let filtered = get ( app, " /explore?q=compiler" , None ) .await.body;
853
assert! ( filtered.contains ( " /andy/beta\" " ) & & ! filtered.contains ( " /andy/alpha\" " ) , " {filtered}" ) ;
854
let by_topic = get ( app, " /explore?topic=6502" , None ) .await.body;
855
assert! ( by_topic.contains ( " /andy/gamma\" " ) & & ! by_topic.contains ( " /andy/beta\" " ) , " {by_topic}" ) ;
856
}
1ac84c62 Andy McCall 1 month ago ↶
857
858
# [ tokio ::test ( flavor = " multi_thread" ) ]
859
async fn lists_filter_sort_page_and_count ( ) {
860
let h = hub ( ) .await;
861
let app = & h.app;
862
let andy = register ( app, " andy" ) .await;
863
let _bob = register ( app, " bob" ) .await;
864
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
865
let repo = db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
866
for n in 0 .. 30 {
867
let author = if n % 3 = = 0 { " bob" } else { " andy" } ;
868
db:: issues:: create(
869
& app.db,
870
db:: issues:: NewIssue { repo_id: repo.id, title: & format! ( " Issue number {n} " ) , body: " " , author_id: None , author } ,
871
)
872
.await
873
.unwrap ( ) ;
874
}
875
let ui = db:: issues:: upsert_label( & app.db, repo.id, " ui" , " #00f0ff" , " " ) .await.unwrap ( ) ;
876
for ( title, author, target) in [ ( " Faster menus" , " bob" , " main" ) , ( " New theme" , " andy" , " main" ) , ( " Release prep" , " andy" , " stable" ) ] {
877
let p = db:: proposals:: create(
878
& app.db,
879
db:: proposals:: NewProposal {
880
repo_id: repo.id,
881
title,
882
body: " " ,
883
author_id: None ,
884
author,
885
draft: false ,
886
target,
887
source_kind: " changes" ,
888
source_repo_id: None ,
889
source_bookmark: " " ,
890
source_changes: " " ,
891
} ,
892
)
893
.await
894
.unwrap ( ) ;
895
if title = = " New theme" {
896
db:: issues:: set_proposal_label( & app.db, p.id, ui.id, true ) .await.unwrap ( ) ;
897
}
898
}
899
900
901
let page = get ( app, " /andy/demo/issues" , Some ( & andy) ) .await;
902
assert! ( page.body.contains ( r # "Issues <span class="count-chip">30</span>"# ) , " {}" , page.body) ;
903
assert! ( page.body.contains ( r # "Proposals <span class="count-chip">3</span>"# ) ) ;
904
905
906
let rows = | b : & str | b.matches ( " /andy/demo/issue/" ) .count ( ) ;
907
assert_eq! ( rows ( & page.body) , 25 ) ;
908
assert! ( page.body.contains ( " page 1 of 2" ) & & page.body.contains ( " page=2" ) , " a pager" ) ;
909
let second = get ( app, " /andy/demo/issues?page=2" , Some ( & andy) ) .await;
910
assert_eq! ( rows ( & second.body) , 5 ) ;
911
assert! ( second.body.contains ( " Issue number 0<" ) , " the oldest are last" ) ;
912
let oldest = get ( app, " /andy/demo/issues?sort=oldest" , Some ( & andy) ) .await;
913
assert! ( oldest.body.find ( " Issue number 0<" ) .unwrap ( ) < oldest.body.find ( " Issue number 1<" ) .unwrap ( ) ) ;
914
let bobs = get ( app, " /andy/demo/issues?q=author:bob" , Some ( & andy) ) .await;
915
assert_eq! ( rows ( & bobs.body) , 10 , " {}" , bobs.body) ;
916
917
918
let by_bob = get ( app, " /andy/demo/proposals?q=author:bob" , Some ( & andy) ) .await.body;
919
assert! ( by_bob.contains ( " Faster menus" ) & & ! by_bob.contains ( " New theme" ) ) ;
920
let ui_only = get ( app, " /andy/demo/proposals?q=label:ui" , Some ( & andy) ) .await.body;
921
assert! ( ui_only.contains ( " New theme" ) & & ! ui_only.contains ( " Faster menus" ) & & ui_only.contains ( r # "class="label""# ) ) ;
922
let stable = get ( app, " /andy/demo/proposals?q=target:stable" , Some ( & andy) ) .await.body;
923
assert! ( stable.contains ( " Release prep" ) & & ! stable.contains ( " New theme" ) ) ;
924
}
e1ec9fbf Andy McCall 1 month ago ↶
925
926
# [ tokio ::test ( flavor = " multi_thread" ) ]
927
async fn project_boards ( ) {
928
let h = hub ( ) .await;
929
let app = & h.app;
930
let andy = register ( app, " andy" ) .await;
931
let bob = register ( app, " bob" ) .await;
932
for ( name, vis) in [ ( " demo" , hub_api:: Visibility:: Public) , ( " secret" , hub_api:: Visibility:: Private) ] {
933
app.repos.create ( & RepoName:: parse( & format! ( " andy/{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
934
let row = db:: repos:: create( & app.db, " andy" , name, " " , vis) .await.unwrap ( ) ;
935
db:: issues:: create(
936
& app.db,
937
db:: issues:: NewIssue { repo_id: row.id, title: & format! ( " {name} issue" ) , body: " " , author_id: None , author: " andy" } ,
938
)
939
.await
940
.unwrap ( ) ;
941
}
16ee5d19 Andy McCall 18 days ago ↶
942
943
944
a024e800 Andy McCall 26 days ago ↶
945
let home = get ( app, " /andy/demo" , Some ( & andy) ) .await.body;
16ee5d19 Andy McCall 18 days ago ↶
946
assert! ( home.contains ( " href=\" /projects/andy\" ><svg class=\" ic\" " ) , " site bar" ) ;
947
assert! ( ! home.contains ( " project boards\" >Projects</a>" ) , " not a repository tab" ) ;
948
assert! ( get ( app, " /andy" , Some ( & bob) ) .await.body.contains ( " href=\" /projects/andy\" >Projects</a>" ) , " the owner's profile" ) ;
a024e800 Andy McCall 26 days ago ↶
949
let list = get ( app, " /projects/andy" , Some ( & andy) ) .await.body;
950
assert! ( list.contains ( " href=\" #new\" >+ NEW PROJECT</a>" ) & & list.contains ( " id=\" new\" " ) , " the button and the form" ) ;
951
assert! ( get ( app, " /projects/andy" , Some ( & bob) ) .await.body.contains ( " Only andy" ) , " who can make one" ) ;
952
let csrf = csrf_of ( & list) ;
e1ec9fbf Andy McCall 1 month ago ↶
953
let r = post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap&description=Next+steps" ) ) .await;
954
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1" ) , " {}" , r.body) ;
7b26e611 Andy McCall 25 days ago ↶
955
956
let list = get ( app, " /projects/andy" , Some ( & andy) ) .await.body;
957
assert! (
958
list.contains ( " >Roadmap</a>" ) & & list.contains ( " Next steps" ) & & list.contains ( " Open <span class=\" count-chip\" >1</span>" ) ,
959
" {list}"
960
) ;
961
let filtered = get ( app, " /projects/andy?q=roadm" , Some ( & andy) ) .await.body;
962
assert! ( filtered.contains ( " >Roadmap</a>" ) , " a filter matches part of the title, any case" ) ;
963
let none = get ( app, " /projects/andy?q=nothing+like+it" , Some ( & andy) ) .await.body;
964
assert! ( none.contains ( " No open project matches" ) & & ! none.contains ( " >Roadmap</a>" ) , " {none}" ) ;
965
let closed = get ( app, " /projects/andy?state=closed" , Some ( & andy) ) .await.body;
966
assert! ( closed.contains ( " No closed projects." ) & & closed.contains ( " Closed <span class=\" count-chip\" >0</span>" ) , " {closed}" ) ;
e1ec9fbf Andy McCall 1 month ago ↶
967
let board = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
968
for col in [ " Backlog" , " Doing" , " Done" ] {
969
assert! ( board.contains ( & format! ( " >{col} <span" ) ) , " {col}" ) ;
970
}
971
let cols: Vec< i64 > = sqlx:: query_scalar( " SELECT id FROM project_columns ORDER BY position" ) .fetch_all ( & app.db) .await.unwrap ( ) ;
972
let act = | form : String| {
973
let andy = andy.clone ( ) ;
974
async move { post ( app, " /projects/andy/1" , Some ( & andy) , & form) .await }
975
} ;
976
977
assert_eq! (
978
act ( format! ( " _csrf={csrf} &action=add&column={} &reference=demo%231" , cols[ 0 ] ) ) .await.location.as_deref ( ) ,
979
Some ( " /projects/andy/1" )
980
) ;
981
act ( format! ( " _csrf={csrf} &action=add&column={} &repo=secret&reference=%231" , cols[ 0 ] ) ) .await;
982
act ( format! ( " _csrf={csrf} &action=add&column={} ¬e=Write+the+docs" , cols[ 1 ] ) ) .await;
983
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} &reference=demo%231" , cols[ 1 ] ) ) .await.body.contains ( " already on this project" ) ) ;
984
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} &reference=demo%2399" , cols[ 0 ] ) ) .await.body.contains ( " no issue or proposal #99" ) ) ;
985
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} &reference=nothere%231" , cols[ 0 ] ) ) .await.body.contains ( " no repository nothere" ) ) ;
986
987
let demo_card: i64 =
988
sqlx:: query_scalar( " SELECT c.id FROM project_cards c JOIN repositories r ON r.id = c.repo_id WHERE r.name = 'demo'" )
989
.fetch_one ( & app.db)
990
.await
991
.unwrap ( ) ;
992
act ( format! ( " _csrf={csrf} &action=move&card={demo_card} &column={} " , cols[ 2 ] ) ) .await;
993
let issue = get ( app, " /andy/demo/issue/1" , None ) .await.body;
994
assert! ( issue.contains ( " href=\" /projects/andy/1\" >Roadmap</a> <span class=\" mute\" >· Done" ) , " the issue shows where it is" ) ;
995
0bc95135 Andy McCall 1 day ago ↶
996
997
998
let moves: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM project_card_moves" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
999
assert_eq! ( moves, 4 , " three cards added and one moved" ) ;
1000
act ( format! ( " _csrf={csrf} &action=wip&column={} &limit=0" , cols[ 2 ] ) ) .await;
1001
let b = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
1002
assert! ( b.contains ( " class=\" over-wip\" " ) & & b.contains ( " 1 / 0" ) , " Done is over its limit of none in progress" ) ;
1003
let r = act ( format! ( " _csrf={csrf} &action=save_view&name=By+repo&layout=table&lane=repo&q=is%3Aopen" ) ) .await;
1004
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1?view=By%20repo" ) , " {}" , r.body) ;
1005
let t = get ( app, " /projects/andy/1?view=By%20repo" , Some ( & andy) ) .await.body;
1006
assert! ( t.contains ( " class=\" board-table\" " ) & & t.contains ( " aria-current=\" page\" >By repo" ) , " the view's tab and its table" ) ;
1007
assert! ( t.contains ( " <th colspan=\" 8\" scope=\" colgroup\" >secret</th>" ) & & t.contains ( " secret issue" ) , " {t}" ) ;
1008
assert! ( ! t.contains ( " Write the docs" ) , " a note is not open work" ) ;
1009
let lanes = get ( app, " /projects/andy/1?lane=repo" , Some ( & andy) ) .await.body;
1010
assert! ( lanes.contains ( " aria-label=\" demo\" " ) & & lanes.contains ( " aria-label=\" (notes)\" " ) , " {lanes}" ) ;
1011
let only = get ( app, " /projects/andy/1?q=repo%3Asecret" , Some ( & andy) ) .await.body;
1012
assert! ( only.contains ( " secret issue" ) & & ! only.contains ( " demo issue" ) , " repo: filters" ) ;
f2d2776b Andy McCall 1 day ago ↶
1013
1014
1015
1016
let ch = get ( app, " /projects/andy/1/charts" , Some ( & andy) ) .await.body;
1017
assert! ( ch.contains ( " Cumulative flow" ) & & ch.contains ( " class=\" band cf-5\" " ) , " {ch}" ) ;
1018
let two_weeks = {
1019
let t = time:: OffsetDateTime:: now_utc( ) + time:: Duration:: days( 14 ) ;
1020
format! ( " {:04} -{:02} -{:02} " , t.year ( ) , u8 :: from( t.month ( ) ) , t.day ( ) )
1021
} ;
1022
assert! ( ch.contains ( " Open cards on the board</td><td>2</td>" ) & & ch.contains ( & format! ( " <strong>{two_weeks} </strong>" ) ) , " {ch}" ) ;
1023
assert! ( ch.contains ( " From 1 week of throughput" ) , " says what it rests on" ) ;
1024
let anon_ch = get ( app, " /projects/andy/1/charts" , None ) .await.body;
1025
assert! ( anon_ch.contains ( " Open cards on the board</td><td>1</td>" ) , " a reader's forecast counts only what it may see" ) ;
0bc95135 Andy McCall 1 day ago ↶
1026
let view_id: i64 = sqlx:: query_scalar( " SELECT id FROM project_views" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
1027
act ( format! ( " _csrf={csrf} &action=delete_view&view={view_id} " ) ) .await;
1028
assert! ( ! get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body.contains ( " >By repo <" ) , " deleted" ) ;
1029
act ( format! ( " _csrf={csrf} &action=wip&column={} &limit=" , cols[ 2 ] ) ) .await;
1030
e1ec9fbf Andy McCall 1 month ago ↶
1031
1032
let anon = get ( app, " /projects/andy/1" , None ) .await.body;
1033
assert! ( anon.contains ( " demo issue" ) & & anon.contains ( " Write the docs" ) & & ! anon.contains ( " secret issue" ) , " {anon}" ) ;
1034
assert! ( ! anon.contains ( " name=\" action\" " ) , " and cannot change anything" ) ;
1035
assert! ( get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body.contains ( " secret issue" ) ) ;
1036
1037
let csrf_b = csrf_of ( & get ( app, " /settings" , Some ( & bob) ) .await.body) ;
1038
assert_eq! (
1039
post ( app, " /projects/andy/1" , Some ( & bob) , & format! ( " _csrf={csrf_b} &action=remove&card={demo_card} " ) ) .await.status,
1040
StatusCode:: NOT_FOUND
1041
) ;
1042
act ( format! ( " _csrf={csrf} &action=settings&title=Roadmap&description=&private=1" ) ) .await;
1043
assert_eq! ( get ( app, " /projects/andy/1" , Some ( & bob) ) .await.status, StatusCode:: NOT_FOUND ) ;
1044
assert! ( ! get ( app, " /projects/andy" , None ) .await.body.contains ( " Roadmap" ) , " not even listed" ) ;
1045
assert! ( ! get ( app, " /andy/demo/issue/1" , None ) .await.body.contains ( " Roadmap" ) , " nor on the issue" ) ;
1046
1047
1048
act ( format! ( " _csrf={csrf} &action=remove_column&column={} " , cols[ 1 ] ) ) .await;
1049
let note_col: i64 = sqlx:: query_scalar( " SELECT column_id FROM project_cards WHERE kind = 'note'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
1050
assert_eq! ( note_col, cols[ 0 ] ) ;
1051
act ( format! ( " _csrf={csrf} &action=remove_column&column={} " , cols[ 2 ] ) ) .await;
1052
assert! ( act ( format! ( " _csrf={csrf} &action=remove_column&column={} " , cols[ 0 ] ) ) .await.body.contains ( " at least one column" ) ) ;
1053
1054
act ( format! ( " _csrf={csrf} &action=settings&title=Roadmap&closed=1" ) ) .await;
1055
assert! ( act ( format! ( " _csrf={csrf} &action=add&column={} ¬e=late" , cols[ 0 ] ) ) .await.body.contains ( " This project is closed" ) ) ;
1056
let r = act ( format! ( " _csrf={csrf} &action=delete" ) ) .await;
1057
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy" ) ) ;
1058
assert_eq! ( get ( app, " /projects/andy/1" , Some ( & andy) ) .await.status, StatusCode:: NOT_FOUND ) ;
1059
assert_eq! ( get ( app, " /andy/demo/issue/1" , None ) .await.status, StatusCode:: OK ) ;
1060
assert_eq! ( get ( app, " /projects/nobody" , None ) .await.status, StatusCode:: NOT_FOUND ) ;
1061
}
c79a1111 Andy McCall 1 month ago ↶
1062
657e7d5c Andy McCall 11 days ago ↶
1063
1064
c79a1111 Andy McCall 1 month ago ↶
1065
# [ tokio ::test ( flavor = " multi_thread" ) ]
657e7d5c Andy McCall 11 days ago ↶
1066
async fn a_long_column_shows_its_newest_cards ( ) {
1067
let h = hub ( ) .await;
1068
let app = & h.app;
1069
let andy = register ( app, " andy" ) .await;
1070
let csrf = csrf_of ( & get ( app, " /projects/andy" , Some ( & andy) ) .await.body) ;
1071
post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
1072
let ( project, done) : ( i64 , i64 ) =
1073
sqlx:: query_as( " SELECT p.id, c.id FROM projects p JOIN project_columns c ON c.project_id = p.id WHERE c.name = 'Done'" )
1074
.fetch_one ( & app.db)
1075
.await
1076
.unwrap ( ) ;
1077
for i in 1 .. = 35 {
1078
sqlx:: query(
1079
" INSERT INTO project_cards (project_id, column_id, position, kind, note, created_ms) VALUES ($1, $2, $3, 'note', $4, 0)" ,
1080
)
1081
.bind ( project)
1082
.bind ( done)
1083
.bind ( i)
1084
.bind ( format! ( " card-{i:03} " ) )
1085
.execute ( & app.db)
1086
.await
1087
.unwrap ( ) ;
1088
}
1089
let board = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
1090
assert! ( board.contains ( " 5 older cards not shown" ) & & board.contains ( & format! ( " ?all={done} #col-{done} \" >show all 35</a>" ) ) , " {board}" ) ;
1091
assert! ( board.contains ( " card-006" ) & & board.contains ( " card-035" ) & & ! board.contains ( " card-005" ) , " the newest 30" ) ;
1092
assert! ( board.contains ( " >35</span></h2>" ) , " the count is the column's" ) ;
1093
let all = get ( app, & format! ( " /projects/andy/1?all={done} " ) , Some ( & andy) ) .await.body;
1094
assert! ( all.contains ( " card-001" ) & & ! all.contains ( " older cards not shown" ) , " show all" ) ;
1095
let found = get ( app, " /projects/andy/1?q=CARD-002" , Some ( & andy) ) .await.body;
1096
assert! ( found.contains ( " card-002" ) & & ! found.contains ( " card-030" ) & & found.contains ( " 1 of 35" ) , " {found}" ) ;
1097
assert! ( found.contains ( " >clear</a>" ) ) ;
1098
let open = get ( app, " /projects/andy/1?state=open" , Some ( & andy) ) .await.body;
1099
assert! ( ! open.contains ( " card-0" ) , " notes have no state" ) ;
1100
}
1101
6a306cd0 Andy McCall 11 days ago ↶
1102
1103
1104
657e7d5c Andy McCall 11 days ago ↶
1105
# [ tokio ::test ( flavor = " multi_thread" ) ]
6a306cd0 Andy McCall 11 days ago ↶
1106
async fn a_board_filters_by_label_and_assignee ( ) {
1107
let h = hub ( ) .await;
1108
let app = & h.app;
1109
let andy = register ( app, " andy" ) .await;
1110
let csrf = csrf_of ( & get ( app, " /projects/andy" , Some ( & andy) ) .await.body) ;
1111
post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
1112
app.repos.create ( & RepoName:: parse( " andy/game" ) .unwrap ( ) , None ) .unwrap ( ) ;
1113
let row = db:: repos:: create( & app.db, " andy" , " game" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1114
let mut ids = Vec :: new( ) ;
1115
for title in [ " Sound crackles" , " Jump is floaty" , " Title screen" ] {
1116
let i = db:: issues:: create( & app.db, db:: issues:: NewIssue { repo_id: row.id, title, body: " " , author_id: None , author: " andy" } )
1117
.await
1118
.unwrap ( ) ;
1119
ids.push ( i.id) ;
1120
}
1121
let bug = db:: issues:: upsert_label( & app.db, row.id, " bug" , " #d73a4a" , " Something is wrong" ) .await.unwrap ( ) ;
1122
let feel = db:: issues:: upsert_label( & app.db, row.id, " feel" , " #0e8a16" , " " ) .await.unwrap ( ) ;
1123
db:: issues:: set_issue_label( & app.db, ids[ 0 ] , bug.id, true ) .await.unwrap ( ) ;
1124
db:: issues:: set_issue_label( & app.db, ids[ 1 ] , bug.id, true ) .await.unwrap ( ) ;
1125
db:: issues:: set_issue_label( & app.db, ids[ 1 ] , feel.id, true ) .await.unwrap ( ) ;
1126
let ( andy_id, ) : ( i64 , ) = sqlx:: query_as( " SELECT id FROM accounts WHERE username = 'andy'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
1127
db:: issues:: set_assignee( & app.db, ids[ 1 ] , andy_id, true ) .await.unwrap ( ) ;
1128
let ( project, todo) : ( i64 , i64 ) =
1129
sqlx:: query_as( " SELECT p.id, c.id FROM projects p JOIN project_columns c ON c.project_id = p.id ORDER BY c.position LIMIT 1" )
1130
.fetch_one ( & app.db)
1131
.await
1132
.unwrap ( ) ;
1133
for n in 1 .. = 3 i64 {
1134
sqlx:: query(
1135
" INSERT INTO project_cards (project_id, column_id, position, kind, repo_id, number, note, created_ms) VALUES ($1, $2, $3, 'issue', $4, $5, '', 0)" ,
1136
)
1137
.bind ( project)
1138
.bind ( todo)
1139
.bind ( n)
1140
.bind ( row.id)
1141
.bind ( n)
1142
.execute ( & app.db)
1143
.await
1144
.unwrap ( ) ;
1145
}
1146
let board = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
1147
assert! ( board.contains ( " href=\" /projects/andy/1?q=label:bug\" " ) , " {board}" ) ;
1148
assert! ( board.contains ( " title=\" Something is wrong\" >bug</a>" ) , " a label's description is its title" ) ;
1149
assert! ( board.contains ( " >@andy</a>" ) , " {board}" ) ;
1150
let bugs = get ( app, " /projects/andy/1?q=label:bug" , Some ( & andy) ) .await.body;
1151
assert! ( bugs.contains ( " Sound crackles" ) & & bugs.contains ( " Jump is floaty" ) & & ! bugs.contains ( " Title screen" ) , " {bugs}" ) ;
1152
assert! ( bugs.contains ( " 2 of 3" ) ) ;
1153
let both = get ( app, " /projects/andy/1?q=label:BUG+label:feel" , Some ( & andy) ) .await.body;
1154
assert! ( ! both.contains ( " Sound crackles" ) & & both.contains ( " Jump is floaty" ) , " every label must hold" ) ;
1155
let mine = get ( app, " /projects/andy/1?q=assignee:%40andy" , Some ( & andy) ) .await.body;
1156
assert! ( mine.contains ( " Jump is floaty" ) & & ! mine.contains ( " Sound crackles" ) , " {mine}" ) ;
1157
let words = get ( app, " /projects/andy/1?q=label:bug+sound" , Some ( & andy) ) .await.body;
1158
assert! ( words.contains ( " Sound crackles" ) & & ! words.contains ( " Jump is floaty" ) ) ;
1159
let closed = get ( app, " /projects/andy/1?q=state:closed" , Some ( & andy) ) .await.body;
1160
assert! ( closed.contains ( " 0 of 3" ) , " {closed}" ) ;
1161
}
1162
439ecd87 Andy McCall 11 days ago ↶
1163
1164
1165
1166
6a306cd0 Andy McCall 11 days ago ↶
1167
# [ tokio ::test ( flavor = " multi_thread" ) ]
439ecd87 Andy McCall 11 days ago ↶
1168
async fn board_cards_are_archived_and_restored ( ) {
1169
let h = hub ( ) .await;
1170
let app = & h.app;
1171
let andy = register ( app, " andy" ) .await;
1172
let csrf = csrf_of ( & get ( app, " /projects/andy" , Some ( & andy) ) .await.body) ;
1173
post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
1174
let ( project, done) : ( i64 , i64 ) =
1175
sqlx:: query_as( " SELECT p.id, c.id FROM projects p JOIN project_columns c ON c.project_id = p.id WHERE c.name = 'Done'" )
1176
.fetch_one ( & app.db)
1177
.await
1178
.unwrap ( ) ;
1179
let day = 86_400_000 i64 ;
1180
let now = db:: now_ms( ) ;
1181
for ( i, age) in [ 40 , 20 , 10 , 1 ] .iter ( ) .enumerate ( ) {
1182
sqlx:: query(
1183
" INSERT INTO project_cards (project_id, column_id, position, kind, note, created_ms, moved_ms) VALUES ($1, $2, $3, 'note', $4, 0, $5)" ,
1184
)
1185
.bind ( project)
1186
.bind ( done)
1187
.bind ( i as i64 )
1188
.bind ( format! ( " aged-{age} " ) )
1189
.bind ( now - age * day)
1190
.execute ( & app.db)
1191
.await
1192
.unwrap ( ) ;
1193
}
1194
let id_of = | note : & 'static str | async move {
1195
sqlx:: query_scalar:: < _ , i64 > ( " SELECT id FROM project_cards WHERE note = $1" ) .bind ( note) .fetch_one ( & app.db) .await.unwrap ( )
1196
} ;
1197
let board = " /projects/andy/1" ;
1198
let ( who, token) = ( & andy, & csrf) ;
1199
let act = | body : String| async move { post ( app, board, Some ( who) , & format! ( " _csrf={token} &{body} " ) ) .await } ;
1200
1201
1202
let forty = id_of ( " aged-40" ) .await;
1203
act ( format! ( " action=archive&card={forty} " ) ) .await;
1204
let page = get ( app, board, Some ( & andy) ) .await.body;
1205
assert! ( ! page.contains ( " aged-40" ) & & page.contains ( " aged-20" ) , " {page}" ) ;
1206
assert! ( page.contains ( " ?archived=1\" >1 archived card</a>" ) , " {page}" ) ;
1207
let archived = get ( app, & format! ( " {board} ?archived=1" ) , Some ( & andy) ) .await.body;
1208
assert! ( archived.contains ( " aged-40" ) & & ! archived.contains ( " aged-20" ) , " {archived}" ) ;
1209
assert! ( archived.contains ( " value=\" restore\" " ) & & archived.contains ( " Back to the board" ) ) ;
1210
assert! ( ! archived.contains ( " add a card" ) , " the archive view adds nothing" ) ;
1211
let found = get ( app, & format! ( " {board} ?archived=1&q=aged-40" ) , Some ( & andy) ) .await.body;
1212
assert! ( found.contains ( " 1 of 1" ) & & found.contains ( " name=\" archived\" value=\" 1\" " ) , " {found}" ) ;
1213
act ( format! ( " action=restore&card={forty} " ) ) .await;
1214
assert! ( get ( app, board, Some ( & andy) ) .await.body.contains ( " aged-40" ) ) ;
1215
1216
1217
1218
let r = act ( format! ( " action=archive_older&column={done} &days=15" ) ) .await;
1219
assert! ( r.body.contains ( " Archived 1 card from Done." ) , " {}" , r.body) ;
1220
let page = get ( app, board, Some ( & andy) ) .await.body;
1221
assert! ( page.contains ( " aged-40" ) & & ! page.contains ( " aged-20" ) & & page.contains ( " aged-10" ) ) ;
1222
let r = act ( format! ( " action=archive_older&column={done} &days=soon" ) ) .await;
1223
assert! ( r.body.contains ( " Give a number of days" ) , " {}" , r.body) ;
1224
1225
1226
let r = act ( format! ( " action=archive_after&column={done} &days=5" ) ) .await;
1227
assert! ( r.body.contains ( " Done archives its cards 5 days after they reach it." ) , " {}" , r.body) ;
1228
assert! ( get ( app, board, Some ( & andy) ) .await.body.contains ( " archives cards 5 days after they reach it" ) ) ;
1229
assert_eq! ( db:: projects:: archive_due( & app.db, db:: now_ms( ) ) .await.unwrap ( ) , 1 ) ;
1230
let page = get ( app, board, Some ( & andy) ) .await.body;
1231
assert! ( page.contains ( " aged-40" ) & & ! page.contains ( " aged-10" ) & & page.contains ( " aged-1<" ) , " {page}" ) ;
1232
assert! ( page.contains ( " 2 archived cards" ) ) ;
946fe7a0 Andy McCall 10 days ago ↶
1233
let r = act ( format! ( " action=archive_after&column={done} &mode=never" ) ) .await;
439ecd87 Andy McCall 11 days ago ↶
1234
assert! ( r.body.contains ( " Done keeps its cards until you archive them." ) ) ;
1235
assert_eq! ( db:: projects:: archive_due( & app.db, db:: now_ms( ) + 100 * day) .await.unwrap ( ) , 0 , " never, now" ) ;
946fe7a0 Andy McCall 10 days ago ↶
1236
1237
1238
1239
let r = act ( format! ( " action=archive_after&column={done} &mode=finished" ) ) .await;
1240
assert! ( r.body.contains ( " Done archives finished cards 14 days after they reach it." ) , " {}" , r.body) ;
1241
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1242
let repo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1243
let mut cards = vec! [ ] ;
1244
for title in [ " shipped" , " still open" ] {
1245
let i = db:: issues:: create( & app.db, db:: issues:: NewIssue { repo_id: repo.id, title, body: " " , author_id: None , author: " andy" } )
1246
.await
1247
.unwrap ( ) ;
1248
if title = = " shipped" {
1249
db:: issues:: set_state( & app.db, i.id, " closed" , " completed" ) .await.unwrap ( ) ;
1250
}
1251
let id: i64 = sqlx:: query_scalar(
1252
" INSERT INTO project_cards (project_id, column_id, position, kind, repo_id, number, note, created_ms, moved_ms) \
1253
VALUES ($1, $2, 10, 'issue', $3, $4, '', 0, $5) RETURNING id" ,
1254
)
1255
.bind ( project)
1256
.bind ( done)
1257
.bind ( repo.id)
1258
.bind ( i.number)
1259
.bind ( now - 20 * day)
1260
.fetch_one ( & app.db)
1261
.await
1262
.unwrap ( ) ;
1263
cards.push ( id) ;
1264
}
1265
assert_eq! ( db:: projects:: archive_due( & app.db, db:: now_ms( ) ) .await.unwrap ( ) , 1 , " the closed issue only" ) ;
1266
let archived_ms = | id : i64 | async move {
1267
sqlx:: query_scalar:: < _ , Option< i64 > > ( " SELECT archived_ms FROM project_cards WHERE id = $1" )
1268
.bind ( id)
1269
.fetch_one ( & app.db)
1270
.await
1271
.unwrap ( )
1272
} ;
1273
assert! ( archived_ms ( cards[ 0 ] ) .await.is_some ( ) & & archived_ms ( cards[ 1 ] ) .await.is_none ( ) ) ;
1274
let page = get ( app, board, Some ( & andy) ) .await.body;
1275
assert! ( page.contains ( " value=\" finished\" selected" ) , " the column says so" ) ;
439ecd87 Andy McCall 11 days ago ↶
1276
}
1277
046e3352 Andy McCall 8 days ago ↶
1278
1279
1280
439ecd87 Andy McCall 11 days ago ↶
1281
# [ tokio ::test ( flavor = " multi_thread" ) ]
046e3352 Andy McCall 8 days ago ↶
1282
async fn board_iterations_close_into_the_archive ( ) {
1283
let h = hub ( ) .await;
1284
let app = & h.app;
1285
let andy = register ( app, " andy" ) .await;
1286
let csrf = csrf_of ( & get ( app, " /projects/andy" , Some ( & andy) ) .await.body) ;
1287
post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
1288
let column = | name : & 'static str | async move {
1289
sqlx:: query_scalar:: < _ , i64 > ( " SELECT id FROM project_columns WHERE name = $1" ) .bind ( name) .fetch_one ( & app.db) .await.unwrap ( )
1290
} ;
1291
let ( backlog, done) = ( column ( " Backlog" ) .await, column ( " Done" ) .await) ;
1292
let project: i64 = sqlx:: query_scalar( " SELECT id FROM projects" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
1293
let day = 86_400_000 i64 ;
1294
let now = db:: now_ms( ) ;
1295
for ( i, ( note, col, age) ) in [ ( " done-a" , done, 2 ) , ( " done-b" , done, 0 ) , ( " todo-c" , backlog, 5 ) ] .into_iter ( ) .enumerate ( ) {
1296
sqlx:: query(
1297
" INSERT INTO project_cards (project_id, column_id, position, kind, note, created_ms, moved_ms) VALUES ($1, $2, $3, 'note', $4, 0, $5)" ,
1298
)
1299
.bind ( project)
1300
.bind ( col)
1301
.bind ( i as i64 )
1302
.bind ( note)
1303
.bind ( now - age * day)
1304
.execute ( & app.db)
1305
.await
1306
.unwrap ( ) ;
1307
}
1308
let date = | ms : i64 | {
1309
let t = time:: OffsetDateTime:: from_unix_timestamp( ms / 1000 ) .unwrap ( ) ;
1310
format! ( " {:04} -{:02} -{:02} " , t.year ( ) , u8 :: from( t.month ( ) ) , t.day ( ) )
1311
} ;
1312
let board = " /projects/andy/1" ;
1313
let ( who, token) = ( & andy, & csrf) ;
1314
let act = | body : String| async move { post ( app, board, Some ( who) , & format! ( " _csrf={token} &{body} " ) ) .await } ;
1315
1316
1317
assert! ( get ( app, board, Some ( & andy) ) .await.body.contains ( " start an iteration" ) ) ;
1318
let r = act ( format! ( " action=start_iteration&name=Sprint+1&start={} &end=nonsense" , date ( now) ) ) .await;
1319
assert! ( r.body.contains ( " Give the first and last days as dates." ) , " {}" , r.body) ;
1320
let r = act ( format! ( " action=start_iteration&name=Sprint+1&start={} &end={} " , date ( now) , date ( now - 2 * day) ) ) .await;
1321
assert! ( r.body.contains ( " ends on or after its first day" ) , " {}" , r.body) ;
1322
let r = act ( format! ( " action=start_iteration&name=Sprint+1&start={} &end={} " , date ( now - 3 * day) , date ( now + 10 * day) ) ) .await;
1323
assert! ( r.body.contains ( " Sprint 1 started." ) , " {}" , r.body) ;
1324
let page = get ( app, board, Some ( & andy) ) .await.body;
1325
assert! ( page.contains ( " day 4 of 14 · 2 cards done" ) , " {page}" ) ;
1326
1327
1328
assert! ( page.contains ( " <polyline points=\" 0,64 20,64 40,32 60,32 80,0\" " ) , " {page}" ) ;
1329
assert! ( page.contains ( " value=\" close_iteration\" " ) ) ;
1330
let r = act ( format! ( " action=start_iteration&name=Sprint+2&start={} &end={} " , date ( now) , date ( now + day) ) ) .await;
1331
assert! ( r.body.contains ( " Close the open iteration before starting another." ) , " {}" , r.body) ;
1332
1333
1334
let it: i64 = sqlx:: query_scalar( " SELECT id FROM project_iterations" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
1335
let r = act ( format! ( " action=close_iteration&iteration={it} " ) ) .await;
1336
assert! ( r.body.contains ( " Closed: 2 done cards archived under it." ) , " {}" , r.body) ;
1337
let page = get ( app, board, Some ( & andy) ) .await.body;
1338
assert! ( ! page.contains ( " done-a" ) & & ! page.contains ( " done-b" ) & & page.contains ( " todo-c" ) , " {page}" ) ;
1339
assert! ( page.contains ( & format! ( " ?iteration={it} \" >Sprint 1</a>" ) ) & & page.contains ( " start an iteration" ) , " {page}" ) ;
1340
let r = act ( format! ( " action=close_iteration&iteration={it} " ) ) .await;
1341
assert! ( r.body.contains ( " That iteration is not open." ) , " {}" , r.body) ;
1342
1343
1344
let past = get ( app, & format! ( " {board} ?iteration={it} " ) , Some ( & andy) ) .await.body;
1345
assert! ( past.contains ( " done-a" ) & & past.contains ( " done-b" ) & & ! past.contains ( " todo-c" ) , " {past}" ) ;
1346
assert! ( past.contains ( " 2 cards done, archived when it closed" ) & & past.contains ( " class=\" burnup\" " ) , " {past}" ) ;
1347
let a: i64 = sqlx:: query_scalar( " SELECT id FROM project_cards WHERE note = 'done-a'" ) .fetch_one ( & app.db) .await.unwrap ( ) ;
1348
act ( format! ( " action=restore&card={a} " ) ) .await;
1349
let past = get ( app, & format! ( " {board} ?iteration={it} " ) , Some ( & andy) ) .await.body;
1350
assert! ( ! past.contains ( " done-a" ) & & past.contains ( " 1 card done" ) , " {past}" ) ;
1351
assert! ( get ( app, board, Some ( & andy) ) .await.body.contains ( " done-a" ) , " back on the board" ) ;
1352
}
1353
1354
1355
# [ test ]
1356
fn a_burn_up_counts_each_days_done_cards ( ) {
1357
use oxygenhub:: db:: projects:: burn_up;
1358
let day = 86_400_000 i64 ;
1359
let start = 10 * day;
1360
1361
1362
let times = [ start - day, start + 1000 , start + 2 * day + 5 , start + 2 * day + 9 ] ;
1363
assert_eq! ( burn_up ( & times, start, start + 5 * day, start + 3 * day + day / 2 ) , [ 2 , 2 , 4 , 4 ] ) ;
1364
1365
assert_eq! ( burn_up ( & times, start, start + 5 * day, start + 40 * day) .len ( ) , 5 ) ;
1366
1367
assert! ( burn_up ( & times, start, start + 5 * day, start - day) .is_empty ( ) ) ;
1368
}
1369
1370
# [ tokio ::test ( flavor = " multi_thread" ) ]
c79a1111 Andy McCall 1 month ago ↶
1371
async fn issue_markdown_follows_the_default_bookmark ( ) {
1372
let h = hub ( ) .await;
1373
let app = & h.app;
1374
register ( app, " andy" ) .await;
1375
app.repos.create ( & RepoName:: parse( " andy/trunky" ) .unwrap ( ) , None ) .unwrap ( ) ;
1376
let row = db:: repos:: create( & app.db, " andy" , " trunky" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
3b1bc27f Andy McCall 20 days ago
1377
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 ↶
1378
db:: issues:: create(
1379
& app.db,
1380
db:: issues:: NewIssue {
1381
repo_id: row.id,
1382
title: " Docs" ,
1383
body: " See [the guide](docs/guide.md) and ." ,
1384
author_id: None ,
1385
author: " andy" ,
1386
} ,
1387
)
1388
.await
1389
.unwrap ( ) ;
1390
let page = get ( app, " /andy/trunky/issue/1" , None ) .await.body;
1391
assert! ( page.contains ( " href=\" /andy/trunky/file/trunk/docs/guide.md\" " ) , " {page}" ) ;
1392
assert! ( page.contains ( " src=\" /andy/trunky/raw/trunk/img/a.png\" " ) ) ;
1393
assert! ( ! page.contains ( " /file/main/" ) ) ;
1394
}
b1e92a62 Andy McCall 25 days ago ↶
1395
1396
1397
1398
1399
# [ tokio ::test ( flavor = " multi_thread" ) ]
1400
async fn a_project_transfers_to_an_organisation ( ) {
1401
let h = hub ( ) .await;
1402
let app = & h.app;
1403
1404
1405
let _first = register ( app, " ada" ) .await;
1406
let andy = register ( app, " andy" ) .await;
1407
let _bob = register ( app, " bob" ) .await;
1408
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
1409
let bob_id = db:: accounts:: account_by_name( & app.db, " bob" ) .await.unwrap ( ) .unwrap ( ) .id;
1410
db:: accounts:: create_org( & app.db, " retro" , " Retro Club" , andy_id) .await.unwrap ( ) ;
1411
db:: accounts:: create_org( & app.db, " other" , " Someone else's" , bob_id) .await.unwrap ( ) ;
1412
let list = get ( app, " /projects/andy" , Some ( & andy) ) .await.body;
1413
let csrf = csrf_of ( & list) ;
1414
let r = post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
1415
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1" ) ) ;
1416
post ( app, " /projects/andy/1" , Some ( & andy) , & format! ( " _csrf={csrf} &action=add¬e=Keep+me" ) ) .await;
1417
1418
1419
let board = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
1420
assert! ( board.contains ( " <option value=\" retro\" >retro</option>" ) & & ! board.contains ( " value=\" other\" " ) , " {board}" ) ;
1421
let refused = post ( app, " /projects/andy/1" , Some ( & andy) , & format! ( " _csrf={csrf} &action=transfer&to=other" ) ) .await;
1422
assert! ( refused.body.contains ( " You cannot make projects under other" ) , " {}" , refused.body) ;
1423
1424
let r = post ( app, " /projects/andy/1" , Some ( & andy) , & format! ( " _csrf={csrf} &action=transfer&to=retro" ) ) .await;
1425
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/retro/1" ) , " {}" , r.body) ;
1426
let moved = get ( app, " /projects/retro/1" , Some ( & andy) ) .await.body;
1427
assert! ( moved.contains ( " Roadmap" ) & & moved.contains ( " Keep me" ) , " the cards come too" ) ;
1428
let old = get ( app, " /projects/andy/1" , Some ( & andy) ) .await;
1429
assert_eq! ( old.status, StatusCode:: PERMANENT_REDIRECT ) ;
1430
assert_eq! ( old.location.as_deref ( ) , Some ( " /projects/retro/1" ) ) ;
1431
1432
1433
let r = post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Fresh" ) ) .await;
1434
assert_eq! ( r.location.as_deref ( ) , Some ( " /projects/andy/1" ) ) ;
1435
let fresh = get ( app, " /projects/andy/1" , Some ( & andy) ) .await;
1436
assert_eq! ( fresh.status, StatusCode:: OK ) ;
1437
assert! ( fresh.body.contains ( " Fresh" ) ) ;
1438
}
d7d18246 Andy McCall 22 days ago ↶
1439
1440
1441
1442
# [ tokio ::test ( flavor = " multi_thread" ) ]
1443
async fn backups_are_set_up_in_settings ( ) {
1444
let h = hub ( ) .await;
1445
let app = & h.app;
1446
let andy = register ( app, " andy" ) .await;
1447
let page = get ( app, " /settings/backups" , Some ( & andy) ) .await;
1448
assert_eq! ( page.status, StatusCode:: OK ) ;
1449
assert! ( page.body.contains ( " href=\" /settings/backups\" class=\" on\" " ) & & page.body.contains ( " No runs yet." ) , " {}" , page.body) ;
1450
let csrf = csrf_of ( & page.body) ;
1451
1452
let bad = post (
1453
app,
1454
" /settings/backups" ,
1455
Some ( & andy) ,
1456
& format! ( " _csrf={csrf} &every=hourly&destination=gcs&bucket=andy-backup&provider=nope&keep_days=30" ) ,
1457
)
1458
.await;
1459
assert! ( bad.body.contains ( " workload identity provider" ) , " {}" , bad.body) ;
1460
let dir =
1461
post ( app, " /settings/backups" , Some ( & andy) , & format! ( " _csrf={csrf} &every=hourly&destination=dir&dir=%2Ftmp&keep_days=30" ) ) .await;
1462
assert! ( dir.body.contains ( " Pick one of the directories this hub allows." ) ) ;
1463
let provider =
1464
" %2F%2Fiam.googleapis.com%2Fprojects%2F1%2Flocations%2Fglobal%2FworkloadIdentityPools%2Foxygenhub%2Fproviders%2Foxygenhub-io" ;
1465
let ok = post (
1466
app,
1467
" /settings/backups" ,
1468
Some ( & andy) ,
1469
& format! ( " _csrf={csrf} &every=hourly&destination=gcs&bucket=gs%3A%2F%2Fandy-backup&provider={provider} &keep_days=7" ) ,
1470
)
1471
.await;
1472
assert! ( ok.body.contains ( " Saved." ) , " {}" , ok.body) ;
1473
let s = oxygenhub:: backups:: setting( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
1474
assert_eq! ( ( s.target.as_str ( ) , s.keep_days, s.every.as_str ( ) ) , ( " andy-backup" , 7 , " hourly" ) ) ;
7b9f4570 Andy McCall 22 days ago ↶
1475
1476
let bad = post (
1477
app,
1478
" /settings/backups" ,
1479
Some ( & andy) ,
1480
& format! ( " _csrf={csrf} &every=hourly&destination=s3&s3_bucket=andy-backup&s3_region=eu-west-2&s3_role=admin&keep_days=30" ) ,
1481
)
1482
.await;
1483
assert! ( bad.body.contains ( " the IAM role" ) , " {}" , bad.body) ;
1484
let role = " arn%3Aaws%3Aiam%3A%3A123456789012%3Arole%2Foxygenhub-backup" ;
1485
let ok = post (
1486
app,
1487
" /settings/backups" ,
1488
Some ( & andy) ,
1489
& format! (
1490
" _csrf={csrf} &every=daily&destination=s3&s3_bucket=s3%3A%2F%2Fandy-backup&s3_region=eu-west-2&s3_role={role} &keep_days=30"
1491
) ,
1492
)
1493
.await;
1494
assert! ( ok.body.contains ( " Saved." ) & & ok.body.contains ( r # "value="s3" checked"# ) , " {}" , ok.body) ;
1495
let s = oxygenhub:: backups:: setting( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
1496
assert_eq! ( ( s.destination.as_str ( ) , s.target.as_str ( ) , s.region.as_str ( ) ) , ( " s3" , " andy-backup" , " eu-west-2" ) ) ;
1497
assert_eq! ( s.provider, " arn:aws:iam::123456789012:role/oxygenhub-backup" ) ;
1498
1499
let ok = post (
1500
app,
1501
" /settings/backups" ,
1502
Some ( & andy) ,
1503
& format! ( " _csrf={csrf} &every=hourly&destination=gcs&bucket=gs%3A%2F%2Fandy-backup&provider={provider} &keep_days=7" ) ,
1504
)
1505
.await;
1506
assert! ( ok.body.contains ( " Saved." ) ) ;
d7d18246 Andy McCall 22 days ago ↶
1507
1508
let bob = register ( app, " bob" ) .await;
1509
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
1510
db:: accounts:: create_org( & app.db, " club" , " Club" , andy_id) .await.unwrap ( ) ;
1511
assert_eq! ( get ( app, " /orgs/club/settings/backups" , Some ( & bob) ) .await.status, StatusCode:: NOT_FOUND ) ;
1512
let club = get ( app, " /orgs/club/settings/backups" , Some ( & andy) ) .await;
1513
assert! ( club.body.contains ( " club</a>'s repositories" ) , " {}" , club.body) ;
1514
assert! ( get ( app, " /orgs/club/settings" , Some ( & andy) ) .await.body.contains ( " /orgs/club/settings/backups" ) ) ;
1e8a6e8b Andy McCall 22 days ago ↶
1515
1516
1517
1518
sqlx:: query( " UPDATE accounts SET is_admin = (username = 'andy')" ) .execute ( & app.db) .await.unwrap ( ) ;
1519
app.repos.create ( & oxygenhub:: repos:: RepoName:: parse( " bob/game" ) .unwrap ( ) , None ) .unwrap ( ) ;
1520
db:: repos:: create( & app.db, " bob" , " game" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1521
assert_eq! ( get ( app, " /admin/backups" , Some ( & bob) ) .await.status, StatusCode:: NOT_FOUND ) ;
1522
let admin = get ( app, " /admin/backups" , Some ( & andy) ) .await;
1523
assert_eq! ( admin.status, StatusCode:: OK ) ;
1524
assert! ( admin.body.contains ( " gs://andy-backup" ) & & admin.body.contains ( " hourly" ) & & admin.body.contains ( " never" ) , " {}" , admin.body) ;
1525
let missing = & admin.body[ admin.body.find ( " Not backed up" ) .unwrap ( ) .. ] ;
1526
assert! ( missing.contains ( " href=\" /bob\" " ) & & missing.contains ( " 1 repository" ) , " {missing}" ) ;
1527
assert! ( get ( app, " /admin" , Some ( & andy) ) .await.body.contains ( " /admin/backups" ) ) ;
d3b048b2 Andy McCall 22 days ago ↶
1528
c7067c7b Andy McCall 15 days ago
1529
let game = get ( app, " /bob/game/settings/backup" , Some ( & bob) ) .await;
d3b048b2 Andy McCall 22 days ago ↶
1530
assert! ( game.body.contains ( " bob has no backup set up yet" ) & & game.body.contains ( r # "name="backup" value="" checked"# ) , " {}" , game.body) ;
1531
let csrf = csrf_of ( & game.body) ;
1532
let saved = post ( app, " /bob/game/settings/backup" , Some ( & bob) , & format! ( " _csrf={csrf} &backup=off" ) ) .await;
c7067c7b Andy McCall 15 days ago
1533
let saved = settled ( app, saved, Some ( & bob) , " /bob/game/settings/backup" ) .await;
d3b048b2 Andy McCall 22 days ago ↶
1534
assert! ( saved.body.contains ( " left out of its owner" ) , " {}" , saved.body) ;
1535
assert_eq! ( db:: repos:: get( & app.db, " bob" , " game" ) .await.unwrap ( ) .unwrap ( ) .backup, " off" ) ;
1536
let bad = post ( app, " /bob/game/settings/backup" , Some ( & bob) , & format! ( " _csrf={csrf} &backup=weekly" ) ) .await;
c7067c7b Andy McCall 15 days ago
1537
let bad = settled ( app, bad, Some ( & bob) , " /bob/game/settings/backup" ) .await;
d3b048b2 Andy McCall 22 days ago ↶
1538
assert! ( bad.body.contains ( " Pick how this repository is backed up." ) ) ;
d7d18246 Andy McCall 22 days ago ↶
1539
}
bff9106e Andy McCall 22 days ago ↶
1540
1541
1542
1543
1544
# [ tokio ::test ( flavor = " multi_thread" ) ]
1545
async fn owners_hear_by_default_and_agents_tell_their_owner ( ) {
1546
let h = hub ( ) .await;
1547
let app = & h.app;
1548
let andy = register ( app, " andy" ) .await;
1549
let bob = register ( app, " bob" ) .await;
1550
let andy_id = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
1551
let kinds = | id : i64 | {
1552
let db = app.db.clone ( ) ;
1553
async move { db:: notify:: inbox( & db, id, 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | n | ( n.kind, n.title, n.actor) ) .collect:: < Vec< _ > > ( ) }
1554
} ;
1555
assert! ( kinds ( andy_id) .await.iter ( ) .any ( | ( k , t , _) | k = = " account.registered" & & t.contains ( " bob" ) ) , " the administrator hears of bob" ) ;
1556
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
1557
app.repos.create ( & repo, None ) .unwrap ( ) ;
1558
db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1559
let open = | who : & 'static str , title : & 'static str | {
1560
let ( app, cookie) = ( app.clone ( ) , if who = = " bob" { bob.clone ( ) } else { andy.clone ( ) } ) ;
1561
async move {
1562
let csrf = csrf_of ( & get ( & app, " /andy/demo/issues/new" , Some ( & cookie) ) .await.body) ;
1563
post ( & app, " /andy/demo/issues/new" , Some ( & cookie) , & format! ( " _csrf={csrf} &title={title} &body=x" ) ) .await
1564
}
1565
} ;
1566
1567
assert! ( get ( app, " /andy/demo" , Some ( & andy) ) .await.body.contains ( " UNWATCH" ) , " an owner watches by default" ) ;
1568
open ( " bob" , " First" ) .await;
1569
assert! ( kinds ( andy_id) .await.iter ( ) .any ( | ( k , t , _) | k = = " issue.opened" & & t.contains ( " First" ) ) ) ;
1570
1571
let csrf = csrf_of ( & get ( app, " /andy/demo" , Some ( & andy) ) .await.body) ;
1572
post ( app, " /andy/demo/watch" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
1573
assert! ( get ( app, " /andy/demo" , Some ( & andy) ) .await.body.contains ( " >WATCH<" ) , " muted" ) ;
1574
open ( " bob" , " Second" ) .await;
1575
assert! ( ! kinds ( andy_id) .await.iter ( ) .any ( | ( _, t , _ ) | t.contains ( " Second" ) ) ) ;
1576
post ( app, " /andy/demo/watch" , Some ( & andy) , & format! ( " _csrf={csrf} " ) ) .await;
1577
1578
1579
let token = | agent : bool | {
1580
let db = app.db.clone ( ) ;
1581
async move {
1582
let ( t, hash) = oxygenhub:: auth:: new_token( ) ;
1583
db:: accounts:: create_token( & db, andy_id, if agent { " Claude" } else { " laptop" } , & hash, " repo:read,repo:write" , " " , None )
1584
.await
1585
.unwrap ( ) ;
1586
if agent {
1587
let id = db:: accounts:: token_by_hash( & db, & hash) .await.unwrap ( ) .unwrap ( ) .id;
1588
db:: accounts:: set_token_agent( & db, andy_id, id, true ) .await.unwrap ( ) ;
1589
}
1590
t
1591
}
1592
} ;
1593
let api_issue = | t : String, title : & 'static str | {
1594
let app = app.clone ( ) ;
1595
async move {
1596
use tower:: ServiceExt;
1597
let req = axum:: http:: Request:: post( " /api/v1/repos/andy/demo/issues" )
1598
.header ( " authorization" , format! ( " Bearer {t} " ) )
1599
.header ( " content-type" , " application/json" )
1600
.body ( axum:: body:: Body:: from( serde_json:: json! ( { " title" : title, " body" : " " } ) .to_string ( ) ) )
1601
.unwrap ( ) ;
1602
oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) .status ( )
1603
}
1604
} ;
1605
assert_eq! ( api_issue ( token ( false ) .await, " By hand" ) .await, StatusCode:: CREATED ) ;
1606
assert! ( ! kinds ( andy_id) .await.iter ( ) .any ( | ( _, t , _ ) | t.contains ( " By hand" ) ) , " his own doing" ) ;
1607
assert_eq! ( api_issue ( token ( true ) .await, " By the agent" ) .await, StatusCode:: CREATED ) ;
1608
let told = kinds ( andy_id) .await;
1609
let agent = told.iter ( ) .find ( | ( _, t , _ ) | t.contains ( " By the agent" ) ) .expect ( " told of his agent's issue" ) ;
1610
assert_eq! ( agent.2 , " andy (agent token Claude)" ) ;
1611
1612
let page = get ( app, " /settings/tokens" , Some ( & andy) ) .await;
00ef7b83 Andy McCall 22 days ago ↶
1613
assert! ( page.body.contains ( " Not an agent" ) & & page.body.contains ( " Mark as agent" ) , " {}" , page.body) ;
1614
assert! ( page.body.contains ( r # "<span class="pill agent-pill""# ) , " the agent's token says so" ) ;
bff9106e Andy McCall 22 days ago ↶
1615
}
1000b40a Andy McCall 10 days ago ↶
1616
1617
1618
1619
# [ tokio ::test ( flavor = " multi_thread" ) ]
1620
async fn finished_work_moves_to_its_column ( ) {
1621
let h = hub ( ) .await;
1622
let app = & h.app;
1623
let andy = register ( app, " andy" ) .await;
1624
app.repos.create ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) , None ) .unwrap ( ) ;
1625
let row = db:: repos:: create( & app.db, " andy" , " demo" , " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
1626
let mut issues = vec! [ ] ;
1627
for t in [ " closed here" , " closed by a change" , " stays open" , " the column is off" ] {
1628
let i = db:: issues:: create( & app.db, db:: issues:: NewIssue { repo_id: row.id, title: t, body: " " , author_id: None , author: " andy" } )
1629
.await
1630
.unwrap ( ) ;
1631
issues.push ( i) ;
1632
}
1633
let csrf = csrf_of ( & get ( app, " /projects/andy" , Some ( & andy) ) .await.body) ;
1634
post ( app, " /projects/andy" , Some ( & andy) , & format! ( " _csrf={csrf} &title=Roadmap" ) ) .await;
1635
let cols: Vec< ( i64 , String, bool ) > =
1636
sqlx:: query_as( " SELECT id, name, takes_finished FROM project_columns ORDER BY position" ) .fetch_all ( & app.db) .await.unwrap ( ) ;
1637
assert_eq! ( cols.iter ( ) .map ( | c | c.2 ) .collect:: < Vec< _ > > ( ) , [ false , false , true ] , " a new board's Done takes finished work" ) ;
1638
let ( backlog, doing, done) = ( cols[ 0 ] .0 , cols[ 1 ] .0 , cols[ 2 ] .0 ) ;
1639
let act = | form : String| {
1640
let andy = andy.clone ( ) ;
1641
async move { post ( app, " /projects/andy/1" , Some ( & andy) , & form) .await }
1642
} ;
1643
for ( i, col) in issues.iter ( ) .zip ( [ backlog, doing, backlog, backlog] ) {
1644
act ( format! ( " _csrf={csrf} &action=add&column={col} &reference=demo%23{} " , i.number) ) .await;
1645
}
1646
let column_of = | n : i64 | async move {
1647
sqlx:: query_scalar:: < _ , i64 > ( " SELECT column_id FROM project_cards WHERE number = $1" ) .bind ( n) .fetch_one ( & app.db) .await.unwrap ( )
1648
} ;
1649
let page = get ( app, " /projects/andy/1" , Some ( & andy) ) .await.body;
1650
assert! ( page.contains ( " Finished work comes here" ) & & page.contains ( " MOVE IT HERE" ) , " {page}" ) ;
1651
1652
1653
let issue = format! ( " /andy/demo/issue/{} " , issues[ 0 ] .number) ;
1654
let icsrf = csrf_of ( & get ( app, & issue, Some ( & andy) ) .await.body) ;
1655
post ( app, & format! ( " {issue} /state" ) , Some ( & andy) , & format! ( " _csrf={icsrf} &action=close" ) ) .await;
1656
assert_eq! ( column_of ( issues[ 0 ] .number) .await, done, " closed on its page" ) ;
1657
db:: issues:: set_state( & app.db, issues[ 1 ] .id, " closed" , " completed" ) .await.unwrap ( ) ;
1658
assert_eq! ( column_of ( issues[ 1 ] .number) .await, done, " from Doing too" ) ;
1659
assert_eq! ( column_of ( issues[ 2 ] .number) .await, backlog, " an open issue stays" ) ;
1660
1661
1662
let r = act ( format! ( " _csrf={csrf} &action=takes_finished&column={done} &mode=off" ) ) .await;
1663
assert! ( r.body.contains ( " Cards stay where they are" ) , " {}" , r.body) ;
1664
db:: issues:: set_state( & app.db, issues[ 3 ] .id, " closed" , " not_planned" ) .await.unwrap ( ) ;
1665
assert_eq! ( column_of ( issues[ 3 ] .number) .await, backlog) ;
1666
let r = act ( format! ( " _csrf={csrf} &action=takes_finished&column={doing} &mode=on" ) ) .await;
1667
assert! ( r.body.contains ( " Cards move to Doing when their issue closes" ) , " {}" , r.body) ;
1668
let flags: Vec< bool > =
1669
sqlx:: query_scalar( " SELECT takes_finished FROM project_columns ORDER BY position" ) .fetch_all ( & app.db) .await.unwrap ( ) ;
1670
assert_eq! ( flags, [ false , true , false ] , " one column of a board" ) ;
1671
}