42fa9554 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:: protocol:: { open_remote, RemoteOptions} ;
9
use oxy_core:: repo:: Repo;
10
use oxygenhub:: config:: Config;
11
use oxygenhub:: repos:: RepoName;
12
use oxygenhub:: { db, Shared} ;
13
use std:: path:: Path;
14
use tower:: ServiceExt;
15
16
const ROOTFS : & str = " 36fc4e3238044e424219a2b9002bf61bbe1805fec14d52cf37e17fae77c9ff16" ;
17
18
fn hermetic ( ) {
19
static ONCE : std:: sync:: Once = std:: sync:: Once:: new( ) ;
20
static DIR : std:: sync:: OnceLock< tempfile:: TempDir> = std:: sync:: OnceLock:: new( ) ;
21
ONCE .call_once ( | | {
22
let d = tempfile:: tempdir( ) .unwrap ( ) ;
23
std:: env:: set_var( " OXY_HOME" , d.path ( ) ) ;
24
DIR .set ( d) .ok ( ) ;
25
} ) ;
26
}
27
28
fn write ( root : & Path, rel : & str , content : & str ) {
29
let p = root.join ( rel) ;
30
std:: fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) .unwrap ( ) ;
31
std:: fs:: write( p, content) .unwrap ( ) ;
32
}
33
34
struct Hub {
35
app : Shared,
36
base : String,
37
_dir : tempfile:: TempDir,
38
}
39
40
async fn hub ( ) -> Hub {
41
hermetic ( ) ;
42
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
43
let mut config = Config:: load( dir.path ( ) ) .unwrap ( ) ;
c8684d44 Andy McCall 19 days ago
44
45
config.registration.setup_code = " test-setup" .into ( ) ;
42fa9554 Andy McCall 1 month ago ↶
46
config.auth.push_tokens = vec! [ " site" .into ( ) ] ;
4d2e44d7 Andy McCall 1 month ago ↶
47
config.flow.runner_tokens = vec! [ " runner-secret" .into ( ) , " runner-two" .into ( ) , " runner-three" .into ( ) ] ;
42fa9554 Andy McCall 1 month ago ↶
48
config.flow.repos = vec! [ " andy/*" .into ( ) ] ;
10708ab8 Andy McCall 1 month ago ↶
49
config.registration.mode = " open" .into ( ) ;
42fa9554 Andy McCall 1 month ago ↶
50
let listener = tokio:: net:: TcpListener:: bind( " 127.0.0.1:0" ) .await.unwrap ( ) ;
51
let port = listener.local_addr ( ) .unwrap ( ) .port ( ) ;
52
config.server.base_url = format! ( " http://127.0.0.1:{port} " ) ;
53
let app = oxygenhub:: build( config) .await.unwrap ( ) ;
54
let router = oxygenhub:: router( app.clone ( ) ) ;
55
tokio:: spawn( async move { axum:: serve( listener, router) .await.unwrap ( ) } ) ;
56
Hub { app, base: format! ( " http://127.0.0.1:{port} " ) , _dir: dir }
57
}
58
59
async fn call ( app : & Shared, method : & str , path : & str , bearer : Option< & str > , json : Option< String> ) -> ( StatusCode, String) {
60
let mut req = Request:: builder( ) .method ( method) .uri ( path) ;
61
if let Some ( b) = bearer {
62
req = req.header ( header:: AUTHORIZATION , format! ( " Bearer {b} " ) ) ;
63
}
64
let body = match json {
65
Some ( j) => {
66
req = req.header ( header:: CONTENT_TYPE , " application/json" ) ;
67
Body:: from( j)
68
}
69
None => Body:: empty( ) ,
70
} ;
71
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req.body ( body) .unwrap ( ) ) .await.unwrap ( ) ;
72
let status = res.status ( ) ;
73
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
74
( status, String :: from_utf8_lossy( & bytes) .into_owned ( ) )
75
}
76
736f6813 Andy McCall 1 month ago ↶
77
78
async fn head ( app : & Shared, path : & str ) -> ( StatusCode, String, String) {
79
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( Request:: get( path) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
80
let h = | k : header:: HeaderName| res.headers ( ) .get ( k) .and_then ( | v | v.to_str ( ) .ok ( ) ) .unwrap_or ( " " ) .to_string ( ) ;
81
( res.status ( ) , h ( header:: LOCATION ) , h ( header:: CONTENT_TYPE ) )
82
}
83
42fa9554 Andy McCall 1 month ago ↶
84
async fn claim ( app : & Shared) -> Option< serde_json:: Value> {
85
let ( st, body) =
86
call ( app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( r # "{"runner":"test-1","runtimes":["microvm"]}"# .into ( ) ) ) .await;
87
match st {
88
StatusCode:: NO_CONTENT => None ,
89
StatusCode:: OK => Some ( serde_json:: from_str( & body) .unwrap ( ) ) ,
90
other => panic! ( " claim: {other} {body}" ) ,
91
}
92
}
93
94
fn receipt ( job : & str , pipeline_hash : & str , exit : u32 ) -> serde_json:: Value {
95
serde_json:: json! ( {
96
" job_name" : job, " pipeline_hash" : pipeline_hash, " scope_tree_hash" : " 11" .repeat ( 32 ) ,
97
" inputs" : [ ] , " environment" : " 22" .repeat ( 32 ) , " outputs" : [ ] , " exit_code" : exit, " duration_ms" : 640
98
} )
99
}
100
101
async fn finish ( app : & Shared, id : i64 , body : serde_json:: Value) -> StatusCode {
102
call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /finish" ) , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await.0
103
}
104
105
async fn create ( app : & Shared, owner : & str , name : & str ) {
106
app.repos.create ( & RepoName:: parse( & format! ( " {owner} /{name} " ) ) .unwrap ( ) , None ) .unwrap ( ) ;
107
db:: repos:: create( & app.db, owner, name, " " , hub_api:: Visibility:: Public) .await.unwrap ( ) ;
108
}
109
110
fn client ( url : & str , work : & Path) -> Repo {
111
let mut c = Repo:: init( work) .unwrap ( ) ;
112
c.config.set ( " remote.origin.url" , url) .unwrap ( ) ;
113
c.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
114
c.save_config ( ) .unwrap ( ) ;
115
c
116
}
117
118
fn pipeline ( extra : & str ) -> String {
119
format! (
120
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n job \" test\" {{ \n sandbox \" alpine\" \n depends-on \" build\" \n exec \" sh\" \" -eu\" \" test.sh\" \n }} \n job \" lint\" {{ \n sandbox \" alpine\" \n exec \" true\" \n }} \n {extra} "
121
)
122
}
123
124
async fn runs_of ( app : & Shared, owner : & str , name : & str ) -> Vec< db:: flow:: Run> {
125
let row = db:: repos:: get( & app.db, owner, name) .await.unwrap ( ) .unwrap ( ) ;
126
for _ in 0 .. 100 {
127
let r = db:: flow:: runs( & app.db, row.id, 10 , None ) .await.unwrap ( ) ;
128
if ! r.is_empty ( ) {
129
return r;
130
}
131
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
132
}
133
vec! [ ]
134
}
135
90326c96 Andy McCall 1 month ago ↶
136
137
async fn checks_when ( app : & Shared, repo_id : i64 , state : & str , n : usize ) -> Vec< db:: hooks:: Check> {
138
for _ in 0 .. 100 {
139
let c = db:: hooks:: checks_for_state( & app.db, repo_id, state) .await.unwrap ( ) ;
140
if c.len ( ) > = n {
141
return c;
142
}
143
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
144
}
145
db:: hooks:: checks_for_state( & app.db, repo_id, state) .await.unwrap ( )
146
}
147
42fa9554 Andy McCall 1 month ago ↶
148
# [ tokio ::test ( flavor = " multi_thread" ) ]
149
async fn a_push_runs_the_pipeline_through_a_runner ( ) {
150
let h = hub ( ) .await;
151
create ( & h.app, " andy" , " demo" ) .await;
152
create ( & h.app, " andy" , " secret" ) .await;
153
create ( & h.app, " other" , " plain" ) .await;
154
let url = format! ( " {} /andy/demo" , h.base) ;
155
let work = tempfile:: tempdir( ) .unwrap ( ) ;
156
let mut c = client ( & url, work.path ( ) ) ;
157
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
158
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
159
c.snapshot ( " test" ) .unwrap ( ) ;
160
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
161
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
162
163
164
let runs = runs_of ( & h.app, " andy" , " demo" ) .await;
165
assert_eq! ( runs.len ( ) , 1 ) ;
166
let run = & runs[ 0 ] ;
167
assert_eq! ( ( run.bookmark.as_str ( ) , run.status.as_str ( ) , run.actor.as_str ( ) ) , ( " main" , " queued" , " site token" ) ) ;
168
assert_eq! ( run.pipeline_hash.len ( ) , 64 ) ;
169
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
170
assert_eq! ( jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.depth) ) .collect:: < Vec< _ > > ( ) , [ ( " build" , 0 ) , ( " lint" , 0 ) , ( " test" , 1 ) ] ) ;
90326c96 Andy McCall 1 month ago ↶
171
172
let checks = checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
42fa9554 Andy McCall 1 month ago ↶
173
assert_eq! (
174
checks.iter ( ) .map ( | c | ( c.name.as_str ( ) , c.status.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
175
[ ( " flow/build" , " queued" ) , ( " flow/lint" , " queued" ) , ( " flow/test" , " queued" ) ]
176
) ;
177
178
179
assert_eq! (
180
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " site" ) , Some ( r # "{"runner":"x","runtimes":["microvm"]}"# .into ( ) ) ) .await.0 ,
181
StatusCode:: UNAUTHORIZED
182
) ;
183
assert_eq! (
184
call ( & h.app, " POST" , " /api/v1/flow/claim" , None , Some ( r # "{"runner":"x","runtimes":["microvm"]}"# .into ( ) ) ) .await.0 ,
185
StatusCode:: UNAUTHORIZED
186
) ;
187
188
let ( st, _ ) =
189
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( r # "{"runner":"x","runtimes":["wasm"]}"# .into ( ) ) ) .await;
190
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
191
192
193
let a = claim ( & h.app) .await.unwrap ( ) ;
194
let b = claim ( & h.app) .await.unwrap ( ) ;
195
assert_eq! ( ( a[ " job" ] [ " name" ] .as_str ( ) , b[ " job" ] [ " name" ] .as_str ( ) ) , ( Some ( " build" ) , Some ( " lint" ) ) ) ;
196
assert! ( claim ( & h.app) .await.is_none ( ) , " test waits for build" ) ;
197
assert_eq! ( a[ " repo" ] , " andy/demo" ) ;
198
assert_eq! ( a[ " clone_url" ] , url.as_str ( ) ) ;
199
assert_eq! ( a[ " sandbox" ] [ " rootfs" ] , ROOTFS ) ;
200
assert_eq! ( a[ " job" ] [ " exec" ] , serde_json:: json! ( [ " make" ] ) ) ;
201
assert_eq! ( a[ " timeout_s" ] , 3600 ) ;
9a065bc0 Andy McCall 1 month ago ↶
202
203
assert_eq! (
204
( a[ " state" ] .as_str ( ) , a[ " bookmark" ] .as_str ( ) , a[ " run_number" ] .as_i64 ( ) ) ,
205
( Some ( run.state.as_str ( ) ) , Some ( " main" ) , Some ( run.number) )
206
) ;
42fa9554 Andy McCall 1 month ago ↶
207
let heads: Vec< String> = a[ " heads" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | v | v.as_str ( ) .unwrap ( ) .to_string ( ) ) .collect ( ) ;
208
assert_eq! ( heads.join ( " +" ) , run.heads) ;
209
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
210
assert_eq! ( checks.iter ( ) .find ( | c | c.name = = " flow/build" ) .unwrap ( ) .status, " running" ) ;
211
212
213
let token = a[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
214
assert! ( token.starts_with ( " flowjob_" ) ) ;
e8fa5369 Andy McCall 22 days ago ↶
215
let opts = RemoteOptions { token: Some ( token.clone ( ) ) , .. Default :: default( ) } ;
42fa9554 Andy McCall 1 month ago ↶
216
open_remote ( & url, & opts) .unwrap ( ) .refs ( ) .unwrap ( ) ;
217
assert! ( open_remote ( & format! ( " {} /andy/secret" , h.base) , & opts) .unwrap ( ) .refs ( ) .is_err ( ) , " another repository" ) ;
e8fa5369 Andy McCall 22 days ago ↶
218
let bad = RemoteOptions { token: Some ( " flowjob_00" .into ( ) ) , .. Default :: default( ) } ;
42fa9554 Andy McCall 1 month ago ↶
219
assert! ( open_remote ( & url, & bad) .unwrap ( ) .refs ( ) .is_err ( ) , " an unknown job token" ) ;
220
221
c.config.set ( " remote.origin.token" , & token) .unwrap ( ) ;
222
c.save_config ( ) .unwrap ( ) ;
223
write ( work.path ( ) , " more.txt" , " x\n " ) ;
224
c.snapshot ( " test" ) .unwrap ( ) ;
225
c.commit ( " More" , & [ ] ) .unwrap ( ) ;
226
let pushed = c.push ( " origin" , false ) ;
227
assert! ( pushed.is_err ( ) | | pushed.unwrap ( ) .iter ( ) .all ( | r | ! r.ok) , " a job token cannot push" ) ;
228
c.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
229
c.save_config ( ) .unwrap ( ) ;
230
231
232
let a_id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
233
let b_id = b[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
234
let log = serde_json:: json! ( { " records" : [
235
{ " t" : 5 , " stream" : " flow" , " text" : " booting" } ,
236
{ " t" : 120 , " stream" : " out" , " text" : " cc -o demo demo.c" } ,
237
{ " t" : 130 , " stream" : " weird" , " text" : " x" .repeat ( 20_000 ) } ,
238
] } ) ;
239
assert_eq! (
240
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{a_id} /log" ) , Some ( " runner-secret" ) , Some ( log.to_string ( ) ) ) .await.0 ,
241
StatusCode:: NO_CONTENT
242
) ;
243
let lines = db:: flow:: log( & h.app.db, a_id, 0 , 100 ) .await.unwrap ( ) ;
244
assert_eq! ( lines.len ( ) , 3 ) ;
245
assert_eq! ( ( lines[ 2 ] .stream.as_str ( ) , lines[ 2 ] .text.len ( ) ) , ( " console" , 16 * 1024 ) , " streams are known ones, text is capped" ) ;
246
let ph = a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) ;
247
assert_eq! (
248
finish ( & h.app, a_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( " lint" , ph, 0 ) } ) ) .await,
249
StatusCode:: BAD_REQUEST ,
250
" a receipt for another job"
251
) ;
252
assert_eq! (
253
finish ( & h.app, a_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( " build" , ph, 0 ) } ) ) .await,
254
StatusCode:: NO_CONTENT
255
) ;
256
assert_eq! ( finish ( & h.app, a_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 } ) ) .await, StatusCode:: CONFLICT , " only once" ) ;
257
assert! ( open_remote ( & url, & opts) .unwrap ( ) .refs ( ) .is_err ( ) , " the token ends with its job" ) ;
258
assert_eq! (
259
finish ( & h.app, b_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 300 , " receipt" : receipt ( " lint" , ph, 0 ) } ) ) .await,
260
StatusCode:: NO_CONTENT
261
) ;
262
let t = claim ( & h.app) .await.unwrap ( ) ;
263
assert_eq! ( t[ " job" ] [ " name" ] , " test" ) ;
264
let t_id = t[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
265
assert_eq! (
266
finish ( & h.app, t_id, serde_json:: json! ( { " exit_code" : 3 , " duration_ms" : 500 , " receipt" : receipt ( " test" , ph, 3 ) } ) ) .await,
267
StatusCode:: NO_CONTENT
268
) ;
269
270
let run = db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) ;
271
assert_eq! ( run.status, " failed" ) ;
272
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
273
let build = jobs.iter ( ) .find ( | j | j.name = = " build" ) .unwrap ( ) ;
274
assert_eq! ( ( build.status.as_str ( ) , build.exit_code, build.receipt_hash.len ( ) ) , ( " passed" , Some ( 0 ) , 64 ) ) ;
275
assert_eq! ( jobs.iter ( ) .find ( | j | j.name = = " test" ) .unwrap ( ) .status, " failed" ) ;
276
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
277
assert_eq! (
278
checks.iter ( ) .map ( | c | ( c.name.as_str ( ) , c.status.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
279
[ ( " flow/build" , " passed" ) , ( " flow/lint" , " passed" ) , ( " flow/test" , " failed" ) ]
280
) ;
a0d4bbfb Andy McCall 1 month ago ↶
281
assert! ( checks[ 2 ] .summary.contains ( " exit 3" ) & & checks[ 2 ] .url.ends_with ( & format! ( " /andy/demo/flow/{} " , run.number) ) ) ;
42fa9554 Andy McCall 1 month ago ↶
282
283
284
let ( st, list) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
285
assert_eq! ( st, StatusCode:: OK ) ;
a0d4bbfb Andy McCall 1 month ago ↶
286
assert! ( list.contains ( & format! ( " /andy/demo/flow/{} " , run.number) ) & & list.contains ( " st-failed" ) , " {list}" ) ;
287
let ( st, page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
42fa9554 Andy McCall 1 month ago ↶
288
assert_eq! ( st, StatusCode:: OK ) ;
289
assert! ( page.contains ( " <svg class=\" flow-graph\" " ) , " graph" ) ;
290
assert_eq! ( page.matches ( " data-job-node=" ) .count ( ) , 3 ) ;
291
assert_eq! ( page.matches ( " <path class=\" edge" ) .count ( ) , 1 , " one dependency edge" ) ;
292
assert! ( page.contains ( " cc -o demo demo.c" ) , " log shown" ) ;
293
assert! ( ! page.contains ( " data-flow-live" ) , " a finished run does not poll" ) ;
a0d4bbfb Andy McCall 1 month ago ↶
294
let ( st, live) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} /live?after={a_id} :1" , run.number) , None , None ) .await;
42fa9554 Andy McCall 1 month ago ↶
295
assert_eq! ( st, StatusCode:: OK ) ;
296
let live: serde_json:: Value = serde_json:: from_str( & live) .unwrap ( ) ;
297
assert_eq! ( live[ " status" ] , " failed" ) ;
298
let build_live = live[ " jobs" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | j | j[ " name" ] = = " build" ) .unwrap ( ) .clone ( ) ;
299
assert_eq! ( build_live[ " lines" ] .as_array ( ) .unwrap ( ) .len ( ) , 2 , " lines after seq 1" ) ;
300
assert_eq! ( call ( & h.app, " GET" , " /andy/demo/flow/999" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
301
302
303
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
304
write ( work.path ( ) , " test.sh" , " echo changed\n " ) ;
305
c.snapshot ( " test" ) .unwrap ( ) ;
306
c.commit ( " Change the test" , & [ ] ) .unwrap ( ) ;
307
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
308
let mut second = None ;
309
for _ in 0 .. 100 {
310
let r = runs_of ( & h.app, " andy" , " demo" ) .await;
311
if r.len ( ) = = 2 {
312
second = Some ( r[ 0 ] .clone ( ) ) ;
313
break ;
314
}
315
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
316
}
317
let second = second.expect ( " a second run" ) ;
318
let x = claim ( & h.app) .await.unwrap ( ) ;
319
let y = claim ( & h.app) .await.unwrap ( ) ;
320
let ( build_id, lint_id) = if x[ " job" ] [ " name" ] = = " build" {
321
( x[ " job_id" ] .as_i64 ( ) .unwrap ( ) , y[ " job_id" ] .as_i64 ( ) .unwrap ( ) )
322
} else {
323
( y[ " job_id" ] .as_i64 ( ) .unwrap ( ) , x[ " job_id" ] .as_i64 ( ) .unwrap ( ) )
324
} ;
325
assert_eq! (
326
finish ( & h.app, build_id, serde_json:: json! ( { " exit_code" : null, " duration_ms" : 10 , " error" : " rootfs is not on this runner" } ) )
327
.await,
328
StatusCode:: NO_CONTENT
329
) ;
330
assert_eq! ( finish ( & h.app, lint_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 } ) ) .await, StatusCode:: NO_CONTENT ) ;
331
assert! ( claim ( & h.app) .await.is_none ( ) , " test was skipped" ) ;
332
let jobs = db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) ;
333
assert_eq! (
334
jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.status.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
335
[ ( " build" , " error" ) , ( " lint" , " passed" ) , ( " test" , " skipped" ) ]
336
) ;
337
assert_eq! ( db:: flow:: run_by_id( & h.app.db, second.id) .await.unwrap ( ) .unwrap ( ) .status, " failed" ) ;
338
339
340
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " job \" broken\" {\n sandbox \" missing\" \n exec \" x\" \n }\n " ) ) ;
341
c.snapshot ( " test" ) .unwrap ( ) ;
342
c.commit ( " Break the pipeline" , & [ ] ) .unwrap ( ) ;
343
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
344
let mut third = None ;
345
for _ in 0 .. 100 {
346
let r = runs_of ( & h.app, " andy" , " demo" ) .await;
347
if r.len ( ) = = 3 {
348
third = Some ( r[ 0 ] .clone ( ) ) ;
349
break ;
350
}
351
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
352
}
353
let third = third.expect ( " a third run" ) ;
354
assert_eq! ( third.status, " error" ) ;
355
assert! ( third.error.contains ( " line 18" ) & & third.error.contains ( " \" missing\" " ) , " {}" , third.error) ;
a0d4bbfb Andy McCall 1 month ago ↶
356
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , third.number) , None , None ) .await;
42fa9554 Andy McCall 1 month ago ↶
357
assert! ( page.contains ( " The pipeline is not valid" ) ) ;
358
736f6813 Andy McCall 1 month ago ↶
359
360
write ( work.path ( ) , " README.md" , " # Demo\n \n [](.flow/run)\n \n \n " ) ;
361
std:: fs:: create_dir_all( work.path ( ) .join ( " img" ) ) .unwrap ( ) ;
362
std:: fs:: write( work.path ( ) .join ( " img/logo.png" ) , b " \x89 PNG\r \n \x1a \n \0 \0 \0 \r IHDR\0 \0 \0 \x01 \0 \0 \0 \x01 \x08 \x06 \0 \0 \0 \x1f \x15 \xc4 \x89 " )
363
.unwrap ( ) ;
364
c.snapshot ( " test" ) .unwrap ( ) ;
365
c.commit ( " A README with a stamp" , & [ ] ) .unwrap ( ) ;
366
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
367
let mut fourth = None ;
368
for _ in 0 .. 100 {
369
let r = runs_of ( & h.app, " andy" , " demo" ) .await;
370
if r.len ( ) = = 4 {
371
fourth = Some ( r[ 0 ] .clone ( ) ) ;
372
break ;
373
}
374
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
375
}
376
let fourth = fourth.expect ( " a fourth run" ) ;
377
let ( _ , overview) = call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
378
assert! ( overview.contains ( " src=\" /andy/demo/raw/main/.flow/stamp\" " ) , " {overview}" ) ;
379
assert! ( overview.contains ( " href=\" /andy/demo/file/main/.flow/run\" " ) ) ;
380
assert! ( overview.contains ( " src=\" /andy/demo/raw/main/img/logo.png\" " ) , " images use the raw route" ) ;
381
let ( st, _ , ct) = head ( & h.app, " /andy/demo/raw/main/img/logo.png" ) .await;
382
assert_eq! ( ( st, ct.as_str ( ) ) , ( StatusCode:: OK , " image/png" ) ) ;
383
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/raw/main/.flow/stamp" ) .await;
384
assert! ( st.is_redirection ( ) & & loc = = " /andy/demo/flow/stamp/main" , " {st} {loc}" ) ;
385
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/flow/stamp/main" ) .await;
386
assert! ( st.is_redirection ( ) & & loc = = format! ( " /andy/demo/flow/stamp/{} " , fourth.heads) , " a bookmark's stamp names its state: {loc}" ) ;
387
let ( st, _ , ct) = head ( & h.app, & loc) .await;
388
assert_eq! ( ( st, ct.as_str ( ) ) , ( StatusCode:: OK , " image/svg+xml; charset=utf-8" ) ) ;
389
let ( _ , svg) = call ( & h.app, " GET" , & loc, None , None ) .await;
390
assert! ( svg.starts_with ( " <svg" ) & & svg.contains ( " error ·" ) & & svg.contains ( & fourth.heads[ .. 12 ] ) , " {svg}" ) ;
391
let ( _ , svg1) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/stamp/{} " , run.heads) , None , None ) .await;
392
assert! ( svg1.contains ( " failed ·" ) & & svg1.contains ( & format! ( " run #{} failed" , run.id) ) , " an older state keeps its own result: {svg1}" ) ;
393
assert! ( svg1.contains ( & build.receipt_hash) , " receipt hashes in the metadata" ) ;
394
assert_eq! ( svg1.matches ( " text-anchor=\" middle\" " ) .count ( ) , 3 , " one glyph per job" ) ;
395
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/file/main/.flow/run" ) .await;
396
assert! ( st.is_redirection ( ) & & loc = = " /andy/demo/flow/at/main" , " {loc}" ) ;
397
let ( st, loc, _ ) = head ( & h.app, " /andy/demo/flow/at/main" ) .await;
398
assert! ( st.is_redirection ( ) & & loc = = format! ( " /andy/demo/flow/{} " , fourth.id) , " {loc}" ) ;
399
42fa9554 Andy McCall 1 month ago ↶
400
401
let work2 = tempfile:: tempdir( ) .unwrap ( ) ;
402
let c2 = client ( & format! ( " {} /other/plain" , h.base) , work2.path ( ) ) ;
403
write ( work2.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
404
c2.snapshot ( " test" ) .unwrap ( ) ;
405
c2.commit ( " Pipeline" , & [ ] ) .unwrap ( ) ;
406
assert! ( c2.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
407
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 500 ) ) .await;
408
let row = db:: repos:: get( & h.app.db, " other" , " plain" ) .await.unwrap ( ) .unwrap ( ) ;
409
assert! ( db:: flow:: runs( & h.app.db, row.id, 10 , None ) .await.unwrap ( ) .is_empty ( ) ) ;
410
let ( _ , page) = call ( & h.app, " GET" , " /other/plain/flow" , None , None ) .await;
411
assert! ( page.contains ( " not enabled for this repository" ) ) ;
736f6813 Andy McCall 1 month ago ↶
412
let ( _ , loc, _ ) = head ( & h.app, " /other/plain/flow/stamp/main" ) .await;
413
let ( _ , svg) = call ( & h.app, " GET" , & loc, None , None ) .await;
414
assert! ( svg.contains ( " no run ·" ) , " {svg}" ) ;
415
let ( _ , loc, _ ) = head ( & h.app, " /other/plain/flow/at/main" ) .await;
416
assert_eq! ( loc, " /other/plain/flow" ) ;
42fa9554 Andy McCall 1 month ago ↶
417
}
fd3db644 Andy McCall 1 month ago ↶
418
419
# [ tokio ::test ( flavor = " multi_thread" ) ]
420
async fn claims_carry_pinned_subrepos_and_the_job_token_reads_only_those ( ) {
421
let h = hub ( ) .await;
25091e5c Andy McCall 1 month ago ↶
422
for n in [ " base" , " lib" , " vault" , " other" , " app" ] {
fd3db644 Andy McCall 1 month ago ↶
423
create ( & h.app, " andy" , n) .await;
424
}
425
let push = | name : & str , file : & str | {
426
let work = tempfile:: tempdir( ) .unwrap ( ) ;
427
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
428
write ( work.path ( ) , file, " x\n " ) ;
429
c.snapshot ( " t" ) .unwrap ( ) ;
430
c.commit ( " first" , & [ ] ) .unwrap ( ) ;
431
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
432
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
433
( work, heads)
434
} ;
25091e5c Andy McCall 1 month ago ↶
435
let ( _b, base_heads) = push ( " base" , " base.rs" ) ;
436
437
let lib_work = tempfile:: tempdir( ) .unwrap ( ) ;
438
let lib = client ( & format! ( " {} /andy/lib" , h.base) , lib_work.path ( ) ) ;
439
lib.submodule_add ( & format! ( " {} /andy/base" , h.base) , " deps/base" , Some ( " main" ) ) .unwrap ( ) ;
440
write ( lib_work.path ( ) , " lib.rs" , " x\n " ) ;
441
lib.snapshot ( " t" ) .unwrap ( ) ;
442
lib.commit ( " lib on base" , & [ ] ) .unwrap ( ) ;
443
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
444
let lib_heads = lib.bookmark_heads ( ) .unwrap ( ) ;
fd3db644 Andy McCall 1 month ago ↶
445
let ( _v, _ ) = push ( " vault" , " secret.rs" ) ;
446
let ( _o, _ ) = push ( " other" , " o.rs" ) ;
447
448
449
let work = tempfile:: tempdir( ) .unwrap ( ) ;
450
let app = client ( & format! ( " {} /andy/app" , h.base) , work.path ( ) ) ;
451
app.submodule_add ( & format! ( " {} /andy/lib" , h.base) , " deps/lib" , Some ( " main" ) ) .unwrap ( ) ;
a4f01d51 Andy McCall 1 month ago ↶
452
2bbe88a8 Andy McCall 1 month ago ↶
453
454
fd3db644 Andy McCall 1 month ago ↶
455
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE name = 'vault'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
a4f01d51 Andy McCall 1 month ago ↶
456
let bare_work = tempfile:: tempdir( ) .unwrap ( ) ;
457
let mut bare = Repo:: init( bare_work.path ( ) ) .unwrap ( ) ;
458
bare.config.set ( " remote.origin.url" , & format! ( " {} /andy/bare" , h.base) ) .unwrap ( ) ;
459
bare.save_config ( ) .unwrap ( ) ;
460
assert! ( bare.submodule_add ( & format! ( " {} /andy/vault" , h.base) , " deps/vault" , Some ( " main" ) ) .is_err ( ) , " not without a token" ) ;
370035a4 Andy McCall 1 month ago ↶
461
app.submodule_add ( & format! ( " {} /andy/vault" , h.base) , " deps/vault" , Some ( " main" ) ) .unwrap ( ) ;
a4f01d51 Andy McCall 1 month ago ↶
462
let nested = Repo:: open( & work.path ( ) .join ( " deps/vault" ) ) .unwrap ( ) ;
2bbe88a8 Andy McCall 1 month ago ↶
463
assert! ( nested.config.remote_tokens.is_empty ( ) , " no token is copied into a subrepo" ) ;
464
nested.fetch ( " origin" ) .unwrap ( ) ;
370035a4 Andy McCall 1 month ago ↶
465
fd3db644 Andy McCall 1 month ago ↶
466
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
467
write ( work.path ( ) , " Makefile" , " all:\n " ) ;
468
app.snapshot ( " t" ) .unwrap ( ) ;
469
app.commit ( " app with deps" , & [ ] ) .unwrap ( ) ;
470
assert! ( app.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
471
assert! ( ! runs_of ( & h.app, " andy" , " app" ) .await.is_empty ( ) ) ;
472
473
let job = claim ( & h.app) .await.unwrap ( ) ;
474
let subs = job[ " subrepos" ] .as_array ( ) .cloned ( ) .unwrap_or_default ( ) ;
25091e5c Andy McCall 1 month ago ↶
475
assert_eq! ( subs.len ( ) , 2 , " lib and what lib pins; vault is private and the pusher has no account: {subs:?}" ) ;
476
assert_eq! ( subs[ 1 ] [ " path" ] , " deps/lib/deps/base" , " nested under its parent, after it" ) ;
477
assert_eq! ( subs[ 1 ] [ " heads" ] , serde_json:: json! ( base_heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) ) ) ;
370035a4 Andy McCall 1 month ago ↶
478
let _ = std:: fs:: remove_dir_all( work.path ( ) .join ( " deps/vault2" ) ) ;
fd3db644 Andy McCall 1 month ago ↶
479
assert_eq! ( subs[ 0 ] [ " path" ] , " deps/lib" ) ;
480
assert_eq! ( subs[ 0 ] [ " repo" ] , " andy/lib" ) ;
481
assert_eq! ( subs[ 0 ] [ " heads" ] , serde_json:: json! ( lib_heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) ) ) ;
482
assert! ( subs[ 0 ] [ " clone_url" ] .as_str ( ) .unwrap ( ) .ends_with ( " /andy/lib" ) ) ;
afd0be38 Andy McCall 1 month ago ↶
483
484
let job_id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
485
let lines = db:: flow:: log( & h.app.db, job_id, 0 , 100 ) .await.unwrap ( ) ;
486
assert! (
487
lines.iter ( ) .any ( | l | l.stream = = " flow" & & l.text.contains ( " subrepo deps/vault (andy/vault) is left out" ) ) ,
488
" {:?}" ,
489
lines.iter ( ) .map ( | l | & l.text) .collect:: < Vec< _ > > ( )
490
) ;
fd3db644 Andy McCall 1 month ago ↶
491
492
493
let token = job[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
494
let read = | name : & str | {
495
let opts = RemoteOptions { token: Some ( token.clone ( ) ) , .. Default :: default( ) } ;
496
open_remote ( & format! ( " {} /andy/{name} " , h.base) , & opts) .and_then ( | r | r.refs ( ) ) .is_ok ( )
497
} ;
25091e5c Andy McCall 1 month ago ↶
498
assert! ( read ( " app" ) & & read ( " lib" ) & & read ( " base" ) ) ;
fd3db644 Andy McCall 1 month ago ↶
499
assert! ( ! read ( " vault" ) , " not pinned for this pusher" ) ;
500
assert! ( ! read ( " other" ) , " public, but not the job's to read" ) ;
501
}
941abdd6 Andy McCall 1 month ago ↶
502
503
# [ tokio ::test ( flavor = " multi_thread" , worker_threads = 8 ) ]
504
async fn concurrent_log_appends_never_fail_on_a_locked_database ( ) {
505
let h = hub ( ) .await;
506
create ( & h.app, " andy" , " demo" ) .await;
507
let work = tempfile:: tempdir( ) .unwrap ( ) ;
508
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
509
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
510
write ( work.path ( ) , " Makefile" , " all:\n " ) ;
511
c.snapshot ( " t" ) .unwrap ( ) ;
512
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
513
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
514
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
515
let job = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .remove ( 0 ) ;
516
517
518
let mut tasks = Vec :: new( ) ;
519
for w in 0 .. 16 {
520
let pool = h.app.db.clone ( ) ;
521
tasks.push ( tokio:: spawn( async move {
522
let records: Vec< flow_core:: api:: Record> =
523
( 0 .. 20 ) .map ( | i | flow_core:: api:: Record { t: i, stream: " out" .into ( ) , text: format! ( " writer {w} line {i} " ) } ) .collect ( ) ;
524
db:: flow:: append_log( & pool, job.id, & records, 100_000 ) .await
525
} ) ) ;
526
}
527
for t in tasks {
528
assert_eq! ( t.await.unwrap ( ) .unwrap ( ) , 20 , " every append succeeds" ) ;
529
}
530
let lines = db:: flow:: log( & h.app.db, job.id, 0 , 1_000 ) .await.unwrap ( ) ;
531
assert_eq! ( lines.len ( ) , 320 ) ;
532
let seqs: std:: collections:: BTreeSet< i64 > = lines.iter ( ) .map ( | l | l.seq ) .collect ( ) ;
533
assert_eq! ( seqs.len ( ) , 320 , " sequence numbers are unique" ) ;
534
}
9008bf6c Andy McCall 1 month ago ↶
535
536
537
# [ tokio ::test ( flavor = " multi_thread" ) ]
538
async fn a_repository_setting_turns_flow_on_or_off ( ) {
539
let h = hub ( ) .await;
540
create ( & h.app, " other" , " plain" ) .await;
541
create ( & h.app, " andy" , " quiet" ) .await;
a0d4bbfb Andy McCall 1 month ago ↶
542
543
create ( & h.app, " andy" , " first" ) .await;
544
{
545
let work = tempfile:: tempdir( ) .unwrap ( ) ;
546
let c = client ( & format! ( " {} /andy/first" , h.base) , work.path ( ) ) ;
547
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
548
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
549
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
550
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
551
assert_eq! ( runs_of ( & h.app, " andy" , " first" ) .await.len ( ) , 1 ) ;
552
}
9008bf6c Andy McCall 1 month ago ↶
553
let row = db:: repos:: get( & h.app.db, " other" , " plain" ) .await.unwrap ( ) .unwrap ( ) ;
554
db:: repos:: set_flow( & h.app.db, row.id, " on" ) .await.unwrap ( ) ;
555
let quiet = db:: repos:: get( & h.app.db, " andy" , " quiet" ) .await.unwrap ( ) .unwrap ( ) ;
556
db:: repos:: set_flow( & h.app.db, quiet.id, " off" ) .await.unwrap ( ) ;
557
for ( owner, name) in [ ( " other" , " plain" ) , ( " andy" , " quiet" ) ] {
558
let work = tempfile:: tempdir( ) .unwrap ( ) ;
559
let c = client ( & format! ( " {} /{owner} /{name} " , h.base) , work.path ( ) ) ;
560
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
561
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
562
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
563
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
564
}
a0d4bbfb Andy McCall 1 month ago ↶
565
let plain = runs_of ( & h.app, " other" , " plain" ) .await;
566
assert_eq! ( plain.len ( ) , 1 , " turned on" ) ;
55673c65 Andy McCall 19 days ago ↶
567
568
bf3d0754 Andy McCall 1 month ago ↶
569
let a = claim ( & h.app) .await.unwrap ( ) ;
570
assert_eq! ( a[ " repo" ] , " andy/first" ) ;
571
let ( _ , page) = call ( & h.app, " GET" , " /other/plain/flow/1" , None , None ) .await;
55673c65 Andy McCall 19 days ago ↶
572
assert! ( page.contains ( " Waiting for a runner: 1st in line" ) & & page.contains ( " Running now: andy/first #1" ) , " {page}" ) ;
bf3d0754 Andy McCall 1 month ago ↶
573
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/other/plain/flow/runs/1" , None , None ) .await;
55673c65 Andy McCall 19 days ago ↶
574
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " queue_position" ] , 1 ) ;
bf3d0754 Andy McCall 1 month ago ↶
575
let ( _ , list) = call ( & h.app, " GET" , " /other/plain/flow" , None , None ) .await;
55673c65 Andy McCall 19 days ago ↶
576
assert! ( list.contains ( " queued, 1st in line" ) , " {list}" ) ;
a0d4bbfb Andy McCall 1 month ago ↶
577
578
579
assert_eq! ( plain[ 0 ] .number, 1 ) ;
90326c96 Andy McCall 1 month ago ↶
580
let checks = checks_when ( & h.app, row.id, & plain[ 0 ] .state, 3 ) .await;
a0d4bbfb Andy McCall 1 month ago ↶
581
assert! ( checks.iter ( ) .all ( | c | c.url.ends_with ( " /other/plain/flow/1" ) ) , " {checks:?}" ) ;
582
let ( st, page) = call ( & h.app, " GET" , " /other/plain/flow/1" , None , None ) .await;
583
assert! ( st = = StatusCode:: OK & & page.contains ( " Run #1" ) , " {st}" ) ;
9008bf6c Andy McCall 1 month ago ↶
584
assert! ( runs_of ( & h.app, " andy" , " quiet" ) .await.is_empty ( ) , " turned off despite andy/*" ) ;
585
}
1edac4b6 Andy McCall 1 month ago ↶
586
587
588
589
# [ tokio ::test ( flavor = " multi_thread" ) ]
590
async fn runs_start_without_a_push_and_finished_runs_re_run ( ) {
591
let h = hub ( ) .await;
592
593
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
594
create ( & h.app, " andy" , " demo" ) .await;
595
let url = format! ( " {} /andy/demo" , h.base) ;
596
let work = tempfile:: tempdir( ) .unwrap ( ) ;
597
let c = client ( & url, work.path ( ) ) ;
598
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
599
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
600
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
601
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
602
let first = runs_of ( & h.app, " andy" , " demo" ) .await;
603
assert_eq! ( ( first.len ( ) , first[ 0 ] .trigger.as_str ( ) ) , ( 1 , " push" ) ) ;
604
605
606
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
607
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
608
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
609
assert_eq! ( v[ " number" ] , 2 ) ;
610
assert! ( v[ " url" ] .as_str ( ) .unwrap ( ) .ends_with ( " /andy/demo/flow/2" ) ) ;
611
let second = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
612
assert_eq! ( ( second.trigger.as_str ( ) , second.actor.as_str ( ) , second.state.as_str ( ) ) , ( " manual" , " andy" , first[ 0 ] .state.as_str ( ) ) ) ;
3d086bc3 Andy McCall 8 days ago ↶
613
614
let ( st, body) = call ( & h.app, " GET" , " /api/v1/user/flow/runs" , Some ( " site" ) , None ) .await;
615
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
616
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
617
assert_eq! ( v.as_array ( ) .unwrap ( ) .len ( ) , 2 , " {v}" ) ;
618
assert_eq! (
619
( v[ 0 ] [ " repository" ] .as_str ( ) , v[ 0 ] [ " number" ] .as_i64 ( ) , v[ 1 ] [ " number" ] .as_i64 ( ) ) ,
620
( Some ( " andy/demo" ) , Some ( 2 ) , Some ( 1 ) ) ,
621
" {v}"
622
) ;
623
assert! ( ! v[ 0 ] [ " jobs" ] .as_array ( ) .unwrap ( ) .is_empty ( ) , " with its jobs: {v}" ) ;
624
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/user/flow/runs?before={} " , v[ 0 ] [ " created_ms" ] ) , Some ( " site" ) , None ) .await;
625
let older: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
626
assert! ( older.as_array ( ) .unwrap ( ) .iter ( ) .all ( | r | r[ " number" ] ! = 2 ) , " before pages back: {older}" ) ;
627
assert_eq! ( call ( & h.app, " GET" , " /api/v1/user/flow/runs" , None , None ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1edac4b6 Andy McCall 1 month ago ↶
628
assert_eq! ( db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) .len ( ) , 3 ) ;
629
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/2" , None , None ) .await;
630
assert! ( page.contains ( " started by andy" ) , " the run page says what started it" ) ;
631
4f692533 Andy McCall 1 month ago ↶
632
633
let head = first[ 0 ] .head_list ( ) [ 0 ] .to_string ( ) ;
634
let ( st, body) =
635
call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( format! ( r # "{{ "change":"{head} "}} "# ) ) ) .await;
636
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
637
let on_change = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, 3 ) .await.unwrap ( ) .unwrap ( ) ;
638
assert_eq! (
639
( on_change.bookmark.as_str ( ) , on_change.heads.as_str ( ) , on_change.state.as_str ( ) ) ,
640
( " " , head.as_str ( ) , first[ 0 ] .state.as_str ( ) )
641
) ;
642
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/3" , None , None ) .await;
643
assert! ( page.contains ( " title=\" Run on one change, not a bookmark\" >change</span>" ) , " run page" ) ;
644
let ( st, body) =
645
call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( format! ( r # "{{ "change":"{} "}} "# , " ab" .repeat ( 32 ) ) ) )
646
.await;
647
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " no change" ) , " {st} {body}" ) ;
648
649
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
650
assert! ( page.contains ( " Sign in with write access to run the pipeline." ) & & ! page.contains ( " primary-btn" ) ) ;
651
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/bookmarks" , None , None ) .await;
652
assert! ( ! page.contains ( " /flow/start" ) , " no run buttons for anonymous viewers" ) ;
653
1edac4b6 Andy McCall 1 month ago ↶
654
655
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/1/rerun" , Some ( " site" ) , None ) .await;
656
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " still queued" ) , " {st} {body}" ) ;
657
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"bookmark":"nope"}"# .into ( ) ) ) .await;
658
assert_eq! ( st, StatusCode:: CONFLICT ) ;
659
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/99/rerun" , Some ( " site" ) , None ) .await;
660
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
661
662
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , None , Some ( " {}" .into ( ) ) ) .await;
663
assert_ne! ( st, StatusCode:: CREATED ) ;
664
665
666
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
667
for r in [ & first[ 0 ] , & second] {
668
for j in db:: flow:: jobs( & h.app.db, r.id) .await.unwrap ( ) {
3b1bc27f Andy McCall 20 days ago
669
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE id = $1" ) .bind ( j.id) .execute ( & h.app.db) .await.unwrap ( ) ;
1edac4b6 Andy McCall 1 month ago ↶
670
}
3b1bc27f Andy McCall 20 days ago
671
sqlx:: query( " UPDATE flow_runs SET status = 'passed', finished_ms = 1 WHERE id = $1" ) .bind ( r.id) .execute ( & h.app.db) .await.unwrap ( ) ;
1edac4b6 Andy McCall 1 month ago ↶
672
}
673
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs/1/rerun" , Some ( " site" ) , None ) .await;
674
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
4f692533 Andy McCall 1 month ago ↶
675
let third = db:: flow:: run( & h.app.db, row.id, 4 ) .await.unwrap ( ) .unwrap ( ) ;
348f8c4b Andy McCall 1 month ago ↶
676
assert_eq! (
677
( third.trigger_label ( ) .as_str ( ) , third.status.as_str ( ) , third.state.as_str ( ) ) ,
678
( " rerun:1" , " queued" , first[ 0 ] .state.as_str ( ) )
679
) ;
c889911b Andy McCall 1 month ago ↶
680
assert_eq! ( third.trigger, " rerun:1:push" , " a rerun keeps the event it repeats" ) ;
1edac4b6 Andy McCall 1 month ago ↶
681
let checks = db:: hooks:: checks_for_state( & h.app.db, row.id, & third.state) .await.unwrap ( ) ;
682
assert! (
4f692533 Andy McCall 1 month ago ↶
683
checks.iter ( ) .all ( | c | c.status = = " queued" & & c.url.ends_with ( " /flow/4" ) ) ,
1edac4b6 Andy McCall 1 month ago ↶
684
" the re-run's checks replace the old ones: {checks:?}"
685
) ;
4f692533 Andy McCall 1 month ago ↶
686
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/4" , None , None ) .await;
1edac4b6 Andy McCall 1 month ago ↶
687
assert! ( page.contains ( " re-run of #1 by andy" ) ) ;
688
assert! ( ! page.contains ( " RE-RUN" ) , " anonymous viewers get no button" ) ;
689
0650d94e Andy McCall 25 days ago ↶
690
691
692
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"jobs":["test"]}"# .into ( ) ) ) .await;
693
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
694
let n = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " number" ] .as_i64 ( ) .unwrap ( ) ;
695
let only = db:: flow:: run( & h.app.db, first[ 0 ] .repo_id, n) .await.unwrap ( ) .unwrap ( ) ;
696
let mut names: Vec< String> = db:: flow:: jobs( & h.app.db, only.id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
697
names.sort ( ) ;
698
assert_eq! ( names, [ " build" , " test" ] , " test and the job it depends on, not lint" ) ;
699
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( r # "{"jobs":["deploy"]}"# .into ( ) ) ) .await;
700
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not in the pipeline" ) & & body.contains ( " build, lint, test" ) , " {st} {body}" ) ;
701
1edac4b6 Andy McCall 1 month ago ↶
702
703
db:: repos:: set_flow( & h.app.db, row.id, " off" ) .await.unwrap ( ) ;
704
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
705
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not enabled" ) , " {body}" ) ;
4f692533 Andy McCall 1 month ago ↶
706
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow" , None , None ) .await;
707
assert! ( page.contains ( " Flow is off for this repository" ) , " and the Flow tab says so" ) ;
90326c96 Andy McCall 1 month ago ↶
708
}
709
710
711
712
# [ tokio ::test ( flavor = " multi_thread" ) ]
713
async fn runs_jobs_and_logs_are_in_the_api ( ) {
714
let h = hub ( ) .await;
715
create ( & h.app, " andy" , " demo" ) .await;
716
let url = format! ( " {} /andy/demo" , h.base) ;
717
let work = tempfile:: tempdir( ) .unwrap ( ) ;
718
let c = client ( & url, work.path ( ) ) ;
719
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
720
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
721
let head = c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
722
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
723
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
724
725
726
let a = claim ( & h.app) .await.unwrap ( ) ;
727
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
728
let recs =
729
serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " stdout" , " text" : " one" } , { " t" : 3 , " stream" : " stderr" , " text" : " three" } ] } ) ;
730
let ( st, _ ) = call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( recs.to_string ( ) ) ) .await;
731
assert! ( st.is_success ( ) , " {st}" ) ;
732
733
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs" , None , None ) .await;
734
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
735
let runs: Vec< hub_api:: FlowRun> = serde_json:: from_str( & body) .unwrap ( ) ;
736
assert_eq! ( runs.len ( ) , 1 ) ;
737
assert_eq! ( ( runs[ 0 ] .number, runs[ 0 ] .state.as_str ( ) , runs[ 0 ] .trigger.as_str ( ) ) , ( 1 , run.state.as_str ( ) , " push" ) ) ;
738
assert! ( runs[ 0 ] .url.ends_with ( " /andy/demo/flow/1" ) & & runs[ 0 ] .jobs.is_empty ( ) ) ;
739
740
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , None , None ) .await;
741
let one: hub_api:: FlowRun = serde_json:: from_str( & body) .unwrap ( ) ;
742
let names: Vec< ( & str , & str ) > = one.jobs.iter ( ) .map ( | j | ( j.name.as_str ( ) , j.status.as_str ( ) ) ) .collect ( ) ;
743
assert_eq! ( names.len ( ) , 3 ) ;
744
let running = one.jobs.iter ( ) .find ( | j | j.status = = " running" ) .unwrap ( ) ;
745
assert_eq! ( running.name, a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) ) ;
746
assert! ( one.jobs.iter ( ) .any ( | j | j.name = = " test" & & j.depends_on = = [ " build" ] ) ) ;
747
748
let job = running.name.clone ( ) ;
749
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/1/jobs/{job} /log" ) , None , None ) .await;
750
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
751
let lines: Vec< hub_api:: FlowLogLine> = serde_json:: from_str( & body) .unwrap ( ) ;
752
assert! ( lines.iter ( ) .any ( | l | l.text = = " three" ) , " {body}" ) ;
753
let last = lines.last ( ) .unwrap ( ) .seq;
754
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/1/jobs/{job} /log?after={last} " ) , None , None ) .await;
755
assert_eq! ( body, " []" , " nothing after the last line" ) ;
756
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
757
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1/jobs/nope/log" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
758
759
760
let by = | key : String| {
761
let app = h.app.clone ( ) ;
762
async move {
763
let ( st, body) = call ( & app, " GET" , & format! ( " /api/v1/repos/andy/demo/checks/{key} " ) , None , None ) .await;
764
assert_eq! ( st, StatusCode:: OK , " {key}: {body}" ) ;
765
serde_json:: from_str:: < Vec< hub_api:: Check> > ( & body) .unwrap ( ) .len ( )
766
}
767
} ;
768
checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
769
assert_eq! ( by ( run.state.clone ( ) ) .await, 3 ) ;
770
assert_eq! ( by ( head.to_hex ( ) ) .await, 3 , " a single head names its state" ) ;
771
assert_eq! ( by ( " main" .into ( ) ) .await, 3 ) ;
772
let ( st, _ ) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/checks/no-such-bookmark" , None , None ) .await;
773
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
774
775
776
let ( st, body) = call ( & h.app, " GET" , " /api/v1/users/andy/repos" , None , None ) .await;
777
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
778
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
779
assert_eq! ( v[ " status" ] , 404 , " {body}" ) ;
1edac4b6 Andy McCall 1 month ago ↶
780
}
cc19697f Andy McCall 1 month ago ↶
781
782
async fn get_bytes ( app : & Shared, path : & str ) -> ( StatusCode, Vec< u8 > ) {
783
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( Request:: get( path) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
784
let status = res.status ( ) ;
785
( status, res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) )
786
}
787
788
# [ tokio ::test ( flavor = " multi_thread" ) ]
789
async fn signed_receipts_are_checked_kept_and_shown ( ) {
790
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectRef, ObjectType} ;
791
use oxy_core:: identity:: SecretKey;
792
use oxy_core:: objects:: StateObject;
793
use oxy_core:: receipt:: SignedReceipt;
794
let h = hub ( ) .await;
795
create ( & h.app, " andy" , " demo" ) .await;
796
let url = format! ( " {} /andy/demo" , h.base) ;
797
let work = tempfile:: tempdir( ) .unwrap ( ) ;
798
let c = client ( & url, work.path ( ) ) ;
799
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
800
c.snapshot ( " test" ) .unwrap ( ) ;
801
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
802
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
803
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
804
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
805
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads.clone ( ) ) .encode ( ) ) ;
806
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
807
let sk = SecretKey:: generate( ) ;
808
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
809
let sign = | r : & serde_json:: Value, state : ObjectRef, sk : & SecretKey, id : & oxy_core:: identity:: Identity| {
810
let r: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
811
let run_url = format! ( " {url} /flow/{} " , run.number) ;
812
hex:: encode( SignedReceipt:: sign( r.to_oxy ( ) , repo_id, state, & run_url, id, sk) .encode ( ) )
813
} ;
814
815
let a = claim ( & h.app) .await.unwrap ( ) ;
816
let b = claim ( & h.app) .await.unwrap ( ) ;
817
let ph = a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
818
let ( a_id, b_id) = ( a[ " job_id" ] .as_i64 ( ) .unwrap ( ) , b[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
819
820
let rb = receipt ( " build" , & ph, 0 ) ;
821
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : rb, " signed_receipt" : sign ( & rb, state, & sk, & runner) } ) ;
822
assert_eq! ( finish ( & h.app, a_id, body) .await, StatusCode:: NO_CONTENT ) ;
823
824
let other = SecretKey:: generate( ) ;
825
let rl = receipt ( " lint" , & ph, 0 ) ;
826
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 300 , " receipt" : rl,
827
" signed_receipt" : sign ( & rl, state, & other, & other.identity ( " mallory" , " " , 1 ) ) } ) ;
828
assert_eq! ( finish ( & h.app, b_id, body) .await, StatusCode:: NO_CONTENT ) ;
829
830
let t = claim ( & h.app) .await.unwrap ( ) ;
831
let rt = receipt ( " test" , & ph, 0 ) ;
832
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 500 , " receipt" : rt,
833
" signed_receipt" : sign ( & rt, ObjectRef( [ 9 ; 32 ] ) , & sk, & runner) } ) ;
834
assert_eq! ( finish ( & h.app, t[ " job_id" ] .as_i64 ( ) .unwrap ( ) , body) .await, StatusCode:: NO_CONTENT ) ;
835
836
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
837
let job = | n : & str | jobs.iter ( ) .find ( | j | j.name = = n ) .unwrap ( ) .clone ( ) ;
838
assert_eq! ( job ( " build" ) .status, " passed" ) ;
839
assert_eq! ( ( job ( " build" ) .receipt_attest.len ( ) , job ( " build" ) .receipt_signer.as_str ( ) ) , ( 64 , " test-1 (OxygenFlow runner)" ) ) ;
840
assert_eq! ( job ( " lint" ) .status, " passed" , " the job's result stands" ) ;
841
assert! ( job ( " lint" ) .receipt_attest.is_empty ( ) ) ;
842
assert! ( job ( " lint" ) .receipt_note.contains ( " not the key this runner signs with" ) , " {}" , job ( " lint" ) .receipt_note) ;
843
assert! ( job ( " test" ) .receipt_note.contains ( " another state" ) , " {}" , job ( " test" ) .receipt_note) ;
844
845
846
847
let kept = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .receipts ( & heads) .unwrap ( ) ;
848
assert_eq! ( kept.len ( ) , 1 ) ;
849
let attest = job ( " build" ) .receipt_attest;
850
let ( st, bytes) = get_bytes ( & h.app, & format! ( " /andy/demo/receipts/{attest} " ) ) .await;
851
assert_eq! ( st, StatusCode:: OK ) ;
852
let sr = SignedReceipt:: decode( & bytes) .unwrap ( ) ;
853
sr.verify ( ) .unwrap ( ) ;
854
assert_eq! ( ( sr.state ( ) , sr.receipt.job_name.as_str ( ) ) , ( state, " build" ) ) ;
855
assert_eq! ( get_bytes ( & h.app, & format! ( " /andy/demo/receipts/{} " , " 0" .repeat ( 64 ) ) ) .await.0 , StatusCode:: NOT_FOUND ) ;
856
857
858
859
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
860
assert! ( page.contains ( " signed receipt</a> by test-1" ) , " run page" ) ;
861
assert! ( page.contains ( " receipt not kept" ) , " run page" ) ;
862
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/change/{} " , heads[ 0 ] .to_hex ( ) ) , None , None ) .await;
863
assert! ( page.contains ( " <dt>Receipts</dt>" ) & & page.contains ( & format! ( " /andy/demo/receipts/{attest} " ) ) , " change page" ) ;
864
}
a3c8288b Andy McCall 1 month ago ↶
865
866
async fn upload ( app : & Shared, job : i64 , name : & str , bytes : & [u8 ]) -> ( StatusCode, String) {
867
let req = Request:: post( format! ( " /api/v1/flow/jobs/{job} /artifacts/{name} " ) )
868
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
869
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
870
.body ( Body:: from( bytes.to_vec ( ) ) )
871
.unwrap ( ) ;
872
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
873
let status = res.status ( ) ;
874
( status, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
875
}
876
877
# [ tokio ::test ( flavor = " multi_thread" ) ]
878
async fn artifacts_are_kept_named_by_the_receipt_and_served ( ) {
879
let h = hub ( ) .await;
880
create ( & h.app, " andy" , " demo" ) .await;
881
let url = format! ( " {} /andy/demo" , h.base) ;
882
let work = tempfile:: tempdir( ) .unwrap ( ) ;
883
let c = client ( & url, work.path ( ) ) ;
884
write (
885
work.path ( ) ,
886
" .flow/pipeline.kdl" ,
887
& format! (
888
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n }} \n }} \n "
889
) ,
890
) ;
891
c.snapshot ( " test" ) .unwrap ( ) ;
892
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
893
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
894
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
895
let a = claim ( & h.app) .await.unwrap ( ) ;
896
assert_eq! ( a[ " job" ] [ " outputs" ] , serde_json:: json! ( [ { " name" : " tool" , " path" : " /src/out/tool" } ] ) , " the runner is told" ) ;
897
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
898
899
let body: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i % 253 ) as u8 ) .collect ( ) ;
900
let ( st, text) = upload ( & h.app, id, " other" , & body) .await;
901
assert_eq! ( st, StatusCode:: BAD_REQUEST , " an undeclared artifact: {text}" ) ;
902
let ( st, text) = upload ( & h.app, id, " tool" , & body) .await;
903
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
904
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
905
assert_eq! ( blob, oxy_core:: repo:: Repo:: blob_ref( & body) .to_hex ( ) , " the same blob the runner computes" ) ;
906
907
908
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
909
assert_eq! (
910
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r.clone ( ) } ) ) .await,
911
StatusCode:: BAD_REQUEST
912
) ;
913
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
914
assert_eq! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r } ) ) .await, StatusCode:: NO_CONTENT ) ;
915
916
917
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
918
let link = format! ( " /andy/demo/flow/{} /artifacts/build/tool" , run.number) ;
919
assert! ( page.contains ( & link) , " run page lists the artifact" ) ;
16ee5d19 Andy McCall 18 days ago ↶
920
921
922
let ( _ , list) = call ( & h.app, " GET" , " /andy/demo/artifacts" , None , None ) .await;
923
assert! ( list.contains ( & link) & & list.contains ( & format! ( " #{} </a>" , run.number) ) , " {list}" ) ;
924
assert! ( list.contains ( " <summary class=\" on\" >Artifacts</summary>" ) & & list.contains ( " href=\" /andy/demo/images\" " ) , " {list}" ) ;
a3c8288b Andy McCall 1 month ago ↶
925
let ( st, got) = get_bytes ( & h.app, & link) .await;
926
assert_eq! ( ( st, got = = body) , ( StatusCode:: OK , true ) ) ;
927
assert_eq! ( get_bytes ( & h.app, & format! ( " /andy/demo/flow/{} /artifacts/build/none" , run.number) ) .await.0 , StatusCode:: NOT_FOUND ) ;
076c7a7c Andy McCall 19 days ago ↶
928
929
930
let prov = format! ( " {link} /provenance" ) ;
931
assert_eq! ( get_bytes ( & h.app, & prov) .await.0 , StatusCode:: BAD_REQUEST , " no kept receipt, no provenance" ) ;
932
sqlx:: query( " UPDATE flow_jobs SET receipt_attest = 'a1b2', receipt_signer = 'test-1 runner' WHERE id = $1" )
933
.bind ( id)
934
.execute ( & h.app.db)
935
.await
936
.unwrap ( ) ;
937
let ( st, env) = get_bytes ( & h.app, & prov) .await;
938
assert_eq! ( st, StatusCode:: OK ) ;
939
let env: serde_json:: Value = serde_json:: from_slice( & env) .unwrap ( ) ;
940
assert_eq! ( env[ " payloadType" ] , " application/vnd.in-toto+json" ) ;
681db69e Andy McCall 19 days ago ↶
941
let unb64 = | t : & str | oxy_core:: dsse:: base64_decode( t) .expect ( " base64" ) ;
076c7a7c Andy McCall 19 days ago ↶
942
let payload = unb64 ( env[ " payload" ] .as_str ( ) .unwrap ( ) ) ;
943
let sig = unb64 ( env[ " signatures" ] [ 0 ] [ " sig" ] .as_str ( ) .unwrap ( ) ) ;
944
let signed = oxygenhub:: provenance:: pae( " application/vnd.in-toto+json" , & payload) ;
945
ring:: signature:: UnparsedPublicKey:: new( & ring:: signature:: ED25519 , h.app.hub_key.identity.ed25519_pk)
946
.verify ( & signed, & sig)
947
.expect ( " the hub's key signed the envelope" ) ;
948
let statement: serde_json:: Value = serde_json:: from_slice( & payload) .unwrap ( ) ;
949
use sha2:: Digest;
950
assert_eq! ( statement[ " subject" ] [ 0 ] [ " digest" ] [ " sha256" ] , hex:: encode( sha2:: Sha256:: digest( & body) ) ) ;
951
assert_eq! ( statement[ " predicateType" ] , " https://slsa.dev/provenance/v1" ) ;
952
assert_eq! ( statement[ " predicate" ] [ " buildDefinition" ] [ " externalParameters" ] [ " job" ] , " build" ) ;
953
assert_eq! ( statement[ " predicate" ] [ " runDetails" ] [ " byproducts" ] [ 0 ] [ " digest" ] [ " oxygen-attestation" ] , " a1b2" ) ;
954
let ( _ , pem) = get_bytes ( & h.app, " /.well-known/oxyhub-signing.pem" ) .await;
955
let pem = String :: from_utf8( pem) .unwrap ( ) ;
956
let der = unb64 ( pem.lines ( ) .filter ( | l | ! l.starts_with ( " -----" ) ) .collect:: < String> ( ) .as_str ( ) ) ;
957
assert_eq! ( & der[ der.len ( ) - 32 .. ] , & h.app.hub_key.identity.ed25519_pk[ .. ] , " the PEM holds the hub's key" ) ;
958
sqlx:: query( " UPDATE flow_jobs SET receipt_attest = '', receipt_signer = '' WHERE id = $1" ) .bind ( id) .execute ( & h.app.db) .await.unwrap ( ) ;
10708ab8 Andy McCall 1 month ago ↶
959
45977501 Andy McCall 25 days ago ↶
960
961
962
{
963
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
964
let mut hosted = oxy_core:: repo:: Repo:: open( & h.app.repos.dir ( & name) ) .unwrap ( ) ;
965
hosted.config.set ( " core.gc-grace" , " 0" ) .unwrap ( ) ;
966
hosted.save_config ( ) .unwrap ( ) ;
967
}
968
let done = oxygenhub:: maintenance:: gc_all( & h.app) .await;
969
assert! ( done.iter ( ) .all ( | d | ! d.failed) , " {done:?}" ) ;
970
let ( st, got) = get_bytes ( & h.app, & link) .await;
971
assert_eq! ( ( st, got = = body) , ( StatusCode:: OK , true ) , " the artifact survives gc" ) ;
972
10708ab8 Andy McCall 1 month ago ↶
973
974
975
let form = | f : String| {
976
Request:: post( " /register" ) .header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) .body ( Body:: from( f) ) .unwrap ( )
977
} ;
978
let res = oxygenhub:: router( h.app.clone ( ) )
c8684d44 Andy McCall 19 days ago
979
.oneshot ( form ( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" .into ( ) ) )
10708ab8 Andy McCall 1 month ago ↶
980
.await
981
.unwrap ( ) ;
982
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
983
let with_cookie = | method : & str , path : & str , body : Option< String> | {
984
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
985
if body.is_some ( ) {
986
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
987
}
988
req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( )
989
} ;
990
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
991
db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
992
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( with_cookie ( " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None ) ) .await.unwrap ( ) ;
993
let page = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
994
let attach = format! ( " /andy/demo/flow/{} /artifacts/build/tool/attach" , run.number) ;
995
assert! ( page.contains ( & attach) , " a writer is offered the release" ) ;
996
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
997
let csrf = page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
998
let release_id = db:: releases:: list( & h.app.db, row.id) .await.unwrap ( ) [ 0 ] .id;
999
let res = oxygenhub:: router( h.app.clone ( ) )
1000
.oneshot ( with_cookie ( " POST" , & attach, Some ( format! ( " _csrf={csrf} &release={release_id} " ) ) ) )
1001
.await
1002
.unwrap ( ) ;
1003
assert_eq! ( res.status ( ) , StatusCode:: SEE_OTHER ) ;
1004
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/releases/tag/v1" , None , None ) .await;
1005
assert! (
1006
page.contains ( " >tool</a>" ) & & page.contains ( & format! ( " built by <a href=\" /andy/demo/flow/{0} \" >run #{0} </a> build" , run.number) ) ,
1007
" {page}"
1008
) ;
1009
let ( _ , api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases" , None , None ) .await;
1010
let api: serde_json:: Value = serde_json:: from_str( & api) .unwrap ( ) ;
1011
let asset = & api[ 0 ] [ " assets" ] [ 0 ] ;
1012
assert_eq! (
1013
( asset[ " name" ] .as_str ( ) , asset[ " built_by" ] [ " run" ] .as_i64 ( ) , asset[ " built_by" ] [ " job" ] .as_str ( ) ) ,
1014
( Some ( " tool" ) , Some ( run.number) , Some ( " build" ) )
1015
) ;
1016
assert! ( asset[ " built_by" ] [ " receipt" ] .is_null ( ) , " that receipt was not signed" ) ;
97fc47fe Andy McCall 19 days ago ↶
1017
assert! ( asset[ " built_by" ] [ " provenance_url" ] .is_null ( ) , " no signed receipt, no provenance" ) ;
1018
1019
1020
sqlx:: query( " UPDATE flow_jobs SET receipt_attest = 'a1b2', receipt_signer = 'earth-1' WHERE run_id = $1 AND name = 'build'" )
1021
.bind ( run.id)
1022
.execute ( & h.app.db)
1023
.await
1024
.unwrap ( ) ;
1025
let want = format! ( " /andy/demo/flow/{} /artifacts/build/tool/provenance" , run.number) ;
1026
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/releases/tag/v1" , None , None ) .await;
1027
assert! ( page.contains ( & format! ( " href=\" {want} \" " ) ) , " {page}" ) ;
1028
let ( _ , api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases" , None , None ) .await;
1029
let api: serde_json:: Value = serde_json:: from_str( & api) .unwrap ( ) ;
1030
assert! ( api[ 0 ] [ " assets" ] [ 0 ] [ " built_by" ] [ " provenance_url" ] .as_str ( ) .unwrap ( ) .ends_with ( & want) ) ;
a3c8288b Andy McCall 1 month ago ↶
1031
}
df940d75 Andy McCall 1 month ago ↶
1032
d29070fa Andy McCall 1 month ago ↶
1033
1034
a36bab6b Andy McCall 25 days ago ↶
1035
async fn upload_report_file ( app : & Shared, job : i64 , name : & str , path : & str , bytes : & [u8 ]) -> ( StatusCode, String) {
1036
let req = Request:: post( format! ( " /api/v1/flow/jobs/{job} /reports/{name} /files" ) )
1037
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
1038
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
1039
.header ( " X-Flow-Report-Path" , path)
1040
.body ( Body:: from( bytes.to_vec ( ) ) )
1041
.unwrap ( ) ;
1042
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
1043
let status = res.status ( ) ;
1044
( status, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1045
}
1046
1047
1048
1049
1050
# [ tokio ::test ( flavor = " multi_thread" ) ]
1051
async fn reports_are_kept_file_by_file_and_indexed ( ) {
1052
let h = hub ( ) .await;
1053
create ( & h.app, " andy" , " demo" ) .await;
1054
let url = format! ( " {} /andy/demo" , h.base) ;
1055
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1056
let c = client ( & url, work.path ( ) ) ;
1057
write (
1058
work.path ( ) ,
1059
" .flow/pipeline.kdl" ,
1060
& format! (
1061
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" perf\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n report \" frame-timing\" {{ \n path \" /src/out/report\" \n when \" always\" \n }} \n }} \n }} \n "
1062
) ,
1063
) ;
1064
c.snapshot ( " test" ) .unwrap ( ) ;
1065
c.commit ( " Measure frames" , & [ ] ) .unwrap ( ) ;
1066
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1067
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1068
let a = claim ( & h.app) .await.unwrap ( ) ;
1069
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1070
1071
let toml = b " kind = \" performance\" \n title = \" Frame timing\" \n summary = \" Median 16.4 ms\" \n \n [metrics]\n median_ms = { value = 16.4, unit = \" ms\" , better = \" lower\" , fail_above = 20 }\n \n [context]\n target_fps = 30\n " ;
1072
let page = b " <p>frames</p>" .to_vec ( ) ;
1073
let data: Vec< u8 > = ( 0 .. 100_000 u32 ) .map ( | i | ( i % 241 ) as u8 ) .collect ( ) ;
c8df33a6 Andy McCall 25 days ago ↶
1074
let ( page_bytes, data_bytes) = ( page.clone ( ) , data.clone ( ) ) ;
a36bab6b Andy McCall 25 days ago ↶
1075
let files: [ ( & str , & [ u8 ] ) ; 3 ] = [ ( " data/frames.csv" , & data) , ( " index.html" , & page) , ( " report.toml" , toml) ] ;
1076
1077
assert_eq! ( upload_report_file ( & h.app, id, " tool" , " report.toml" , toml) .await.0 , StatusCode:: BAD_REQUEST , " not a report" ) ;
1078
assert_eq! ( upload_report_file ( & h.app, id, " frame-timing" , " ../x" , toml) .await.0 , StatusCode:: BAD_REQUEST , " not a clean path" ) ;
1079
let mut listed = vec! [ ] ;
1080
for ( path, bytes) in files {
1081
let ( st, text) = upload_report_file ( & h.app, id, " frame-timing" , path, bytes) .await;
1082
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1083
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1084
assert_eq! ( blob, oxy_core:: repo:: Repo:: blob_ref( bytes) .to_hex ( ) ) ;
1085
listed.push ( serde_json:: json! ( { " path" : path, " blob" : blob, " size" : bytes.len ( ) } ) ) ;
1086
}
1087
let mut missing = listed.clone ( ) ;
1088
missing.push ( serde_json:: json! ( { " path" : " assets/x.js" , " blob" : " ab" .repeat ( 32 ) , " size" : 1 } ) ) ;
1089
let ( st, text) =
1090
upload ( & h.app, id, " frame-timing" , & serde_json:: to_vec( & serde_json:: json! ( { " version" : 1 , " files" : missing } ) ) .unwrap ( ) ) .await;
1091
assert_eq! ( st, StatusCode:: BAD_REQUEST , " a file never uploaded: {text}" ) ;
1092
assert! ( text.contains ( " assets/x.js" ) , " {text}" ) ;
1093
let manifest = serde_json:: to_vec( & serde_json:: json! ( { " version" : 1 , " files" : listed } ) ) .unwrap ( ) ;
1094
let ( st, text) = upload ( & h.app, id, " frame-timing" , & manifest) .await;
1095
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1096
1097
let ( kind, title, summary, files_n, bytes) : ( String , String , String , i64 , i64 ) =
3b1bc27f Andy McCall 20 days ago
1098
sqlx:: query_as( " SELECT kind, title, summary, files, bytes FROM reports WHERE job_id = $1 AND name = 'frame-timing'" )
a36bab6b Andy McCall 25 days ago ↶
1099
.bind ( id)
1100
.fetch_one ( & h.app.db)
1101
.await
1102
.unwrap ( ) ;
1103
assert_eq! ( ( kind.as_str ( ) , title.as_str ( ) , summary.as_str ( ) ) , ( " performance" , " Frame timing" , " Median 16.4 ms" ) ) ;
1104
assert_eq! ( ( files_n, bytes) , ( 3 , ( data.len ( ) + page.len ( ) + toml.len ( ) ) as i64 ) ) ;
3b1bc27f Andy McCall 20 days ago
1105
let rid: i64 = sqlx:: query_scalar( " SELECT id FROM reports WHERE job_id = $1" ) .bind ( id) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
a36bab6b Andy McCall 25 days ago ↶
1106
let m = oxygenhub:: reports:: metrics( & h.app.db, rid) .await.unwrap ( ) ;
1107
assert_eq! ( ( m[ 0 ] .key.as_str ( ) , m[ 0 ] .value, m[ 0 ] .fail_above) , ( " median_ms" , 16. 4 , Some ( 20. 0 ) ) ) ;
1108
let log = db:: flow:: log( & h.app.db, id, 0 , 1000 ) .await.unwrap ( ) ;
1109
assert! ( log.iter ( ) .any ( | r | r.text.contains ( " report frame-timing: performance \" Frame timing\" , 1 metric" ) ) , " {log:?}" ) ;
c8df33a6 Andy McCall 25 days ago ↶
1110
1111
1112
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
1113
let c = checks.iter ( ) .find ( | c | c.name = = " report/frame-timing/median_ms" ) .expect ( " a check per threshold" ) ;
1114
assert_eq! ( c.status, " passed" , " {c:?}" ) ;
1115
assert! ( c.summary.contains ( " 16.4 ms" ) & & c.url.contains ( " /reports/frame-timing?id=" ) , " {c:?}" ) ;
1116
1117
1118
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
1119
assert! ( page.contains ( " >Reports</a>" ) , " the tab" ) ;
1120
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/reports" , None , None ) .await;
1121
assert! ( st = = StatusCode:: OK & & page.contains ( " Frame timing" ) & & page.contains ( " median_ms" ) , " {st}" ) ;
1122
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/reports/frame-timing" , None , None ) .await;
1123
assert! ( st = = StatusCode:: OK & & page.contains ( " fails above 20" ) & & page.contains ( " sandbox=\" allow-scripts\" " ) , " {st}" ) ;
1124
let src = page.split ( " class=\" rep-frame\" src=\" " ) .nth ( 1 ) .unwrap ( ) .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1125
assert! ( src.ends_with ( " /index.html" ) , " {src}" ) ;
1126
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( Request:: get( & src) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
1127
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
1128
let csp = res.headers ( ) [ header:: CONTENT_SECURITY_POLICY ] .to_str ( ) .unwrap ( ) .to_string ( ) ;
1129
assert! ( csp.starts_with ( " sandbox allow-scripts;" ) & & csp.contains ( " default-src 'none'" ) , " {csp}" ) ;
d74ce22f Andy McCall 20 days ago ↶
1130
1131
assert! (
1132
res.headers ( ) .get ( header:: X_FRAME_OPTIONS ) .is_none ( ) & & res.headers ( ) .get ( header:: CONTENT_SECURITY_POLICY_REPORT_ONLY ) .is_none ( )
1133
) ;
1134
assert_eq! ( res.headers ( ) [ header:: X_CONTENT_TYPE_OPTIONS ] , " nosniff" ) ;
c8df33a6 Andy McCall 25 days ago ↶
1135
assert_eq! ( res.headers ( ) [ header:: ACCESS_CONTROL_ALLOW_ORIGIN ] , " *" ) ;
f3e5e2bc Andy McCall 25 days ago ↶
1136
1137
let served = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ;
1138
assert! ( served.starts_with ( & page_bytes) , " the report's page" ) ;
1139
assert! ( String :: from_utf8_lossy( & served) .contains ( " /static/report-frame.js" ) , " with the frame script" ) ;
1140
let ( st, _ ) = get_bytes ( & h.app, " /static/report.css" ) .await;
1141
assert_eq! ( st, StatusCode:: OK , " the stylesheet a report may link" ) ;
c8df33a6 Andy McCall 25 days ago ↶
1142
let data = src.replace ( " /index.html" , " /data/frames.csv" ) ;
1143
assert_eq! ( get_bytes ( & h.app, & data) .await, ( StatusCode:: OK , data_bytes.clone ( ) ) ) ;
1144
95b525b0 Andy McCall 24 days ago ↶
1145
1146
1147
let first: i64 = sqlx:: query_scalar( " SELECT id FROM reports WHERE name = 'frame-timing'" ) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
1148
let second: i64 = sqlx:: query_scalar(
1149
" INSERT INTO reports (repo_id, name, kind, title, summary, context, run_id, job_id, run_number, job, bookmark, state, manifest, files, bytes, created_ms) \
3b1bc27f Andy McCall 20 days ago
1150
SELECT repo_id, name, kind, title, 'Median 18.0 ms', context, run_id, NULL, 2, job, bookmark, state, manifest, files, bytes, created_ms + 1000 FROM reports WHERE id = $1 RETURNING id" ,
95b525b0 Andy McCall 24 days ago ↶
1151
)
1152
.bind ( first)
1153
.fetch_one ( & h.app.db)
1154
.await
1155
.unwrap ( ) ;
1156
sqlx:: query(
3b1bc27f Andy McCall 20 days ago
1157
" INSERT INTO report_metrics (report_id, key, value, unit, better, fail_above) VALUES ($1, 'median_ms', 18.0, 'ms', 'lower', 20)" ,
95b525b0 Andy McCall 24 days ago ↶
1158
)
1159
.bind ( second)
1160
.execute ( & h.app.db)
1161
.await
1162
.unwrap ( ) ;
1163
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/reports/frame-timing" , None , None ) .await;
1164
assert_eq! ( page.matches ( " data-tl-stop" ) .count ( ) , 2 , " a stop per run" ) ;
1165
assert! ( page.contains ( " Median 18.0 ms" ) , " the newest run is shown" ) ;
1166
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/reports/frame-timing?id={first} &compare={second} &play=3" ) , None , None ) .await;
1167
assert! ( page.contains ( " This run against run #2" ) & & page.contains ( " -1.6" ) , " every metric against the other run" ) ;
1168
let stop = page.split ( " data-tl-stop" ) .next ( ) .unwrap_or ( " " ) .rsplit ( " href=" ) .next ( ) .unwrap_or ( " " ) .to_string ( ) ;
1169
assert! ( page.contains ( " data-play=\" 3\" " ) , " playing" ) ;
1170
assert! ( stop.contains ( " play" ) & & stop.contains ( ' 3' ) , " playing carries on from stop to stop: {stop}" ) ;
1171
assert! ( page.contains ( " data-report-runs=" ) , " the frame interface knows the runs" ) ;
fac6853c Andy McCall 7 days ago ↶
1172
1173
1174
1175
let n = run.number;
1176
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/perf/report" ) , None , None ) .await;
1177
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1178
let r: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1179
assert_eq! ( ( r[ " id" ] .as_i64 ( ) , r[ " name" ] .as_str ( ) , r[ " kind" ] .as_str ( ) ) , ( Some ( first) , Some ( " frame-timing" ) , Some ( " performance" ) ) ) ;
1180
assert_eq! ( ( r[ " metrics" ] [ 0 ] [ " key" ] .as_str ( ) , r[ " metrics" ] [ 0 ] [ " fail_above" ] .as_f64 ( ) ) , ( Some ( " median_ms" ) , Some ( 20. 0 ) ) ) ;
1181
assert_eq! ( r[ " context" ] [ " target_fps" ] , 30 ) ;
1182
let paths: Vec< & str > = r[ " manifest" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | f | f[ " path" ] .as_str ( ) .unwrap ( ) ) .collect ( ) ;
1183
assert_eq! ( paths, [ " data/frames.csv" , " index.html" , " report.toml" ] ) ;
1184
assert! ( r[ " url" ] .as_str ( ) .unwrap ( ) .ends_with ( & format! ( " /andy/demo/reports/frame-timing?id={first} " ) ) ) ;
1185
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/perf/reports" ) , None , None ) .await;
1186
assert! ( st = = StatusCode:: OK & & body.contains ( " \" frame-timing\" " ) & & ! body.contains ( " manifest" ) , " {st} {body}" ) ;
1187
let res = oxygenhub:: router( h.app.clone ( ) )
1188
.oneshot (
1189
Request:: get( format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/perf/report/data/frames.csv" ) ) .body ( Body:: empty( ) ) .unwrap ( ) ,
1190
)
1191
.await
1192
.unwrap ( ) ;
1193
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
1194
assert_eq! ( res.headers ( ) [ header:: CONTENT_TYPE ] , " text/csv; charset=utf-8" ) ;
1195
assert_eq! ( res.headers ( ) [ header:: CONTENT_SECURITY_POLICY ] , " sandbox; default-src 'none'" , " a page in it runs nothing on the hub" ) ;
1196
assert_eq! ( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) , data_bytes) ;
1197
let toml_path = format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/perf/report/report.toml?name=frame-timing" ) ;
1198
assert_eq! ( get_bytes ( & h.app, & toml_path) .await, ( StatusCode:: OK , toml.to_vec ( ) ) ) ;
1199
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/perf/report/nope.csv" ) , None , None ) .await;
1200
assert! ( st = = StatusCode:: NOT_FOUND & & body.contains ( " has no file nope.csv" ) & & body.contains ( " report.toml" ) , " {st} {body}" ) ;
1201
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/perf/report?name=other" ) , None , None ) .await;
1202
assert! ( st = = StatusCode:: NOT_FOUND & & body.contains ( " it published frame-timing" ) , " {st} {body}" ) ;
1203
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/{n} /jobs/nojob/report" ) , None , None ) .await;
1204
assert! ( st = = StatusCode:: NOT_FOUND & & body.contains ( " its jobs: perf" ) , " {st} {body}" ) ;
1205
1206
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/reports" , None , None ) .await;
1207
let latest: Vec< serde_json:: Value> = serde_json:: from_str( & body) .unwrap ( ) ;
1208
assert_eq! ( ( latest.len ( ) , latest[ 0 ] [ " id" ] .as_i64 ( ) , latest[ 0 ] [ " run" ] .as_i64 ( ) ) , ( 1 , Some ( second) , Some ( 2 ) ) ) ;
1209
assert_eq! ( latest[ 0 ] [ " metrics" ] [ 0 ] [ " value" ] .as_f64 ( ) , Some ( 18. 0 ) ) ;
1210
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/reports/frame-timing" , None , None ) .await;
1211
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " id" ] .as_i64 ( ) , Some ( second) , " newest on its bookmark" ) ;
1212
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/reports/frame-timing?id={first} " ) , None , None ) .await;
1213
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " summary" ] , " Median 16.4 ms" ) ;
1214
let page_path = format! ( " /api/v1/repos/andy/demo/reports/frame-timing/files/index.html?id={first} " ) ;
1215
assert_eq! ( get_bytes ( & h.app, & page_path) .await, ( StatusCode:: OK , page_bytes.clone ( ) ) , " as published, no frame script" ) ;
1216
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/reports/frame-timing?bookmark=elsewhere" , None , None ) .await;
1217
assert! ( st = = StatusCode:: NOT_FOUND & & body.contains ( " never ran on elsewhere" ) , " {st} {body}" ) ;
1218
let ( st, _ ) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/reports/nothing" , None , None ) .await;
1219
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
1220
1221
let phases = std:: collections:: BTreeMap:: from( [ ( " run" .to_string ( ) , 1200 u64 ) , ( " image" .to_string ( ) , 300 ) ] ) ;
1222
db:: flow:: set_phases( & h.app.db, id, & phases) .await.unwrap ( ) ;
1223
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/flow/runs/{n} " ) , None , None ) .await;
1224
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1225
assert! ( v[ " jobs" ] [ 0 ] [ " queued_ms" ] .as_i64 ( ) .is_some ( ) , " {body}" ) ;
1226
assert_eq! ( v[ " jobs" ] [ 0 ] [ " phases" ] , serde_json:: json! ( [ { " name" : " image" , " ms" : 300 } , { " name" : " run" , " ms" : 1200 } ] ) ) ;
c8df33a6 Andy McCall 25 days ago ↶
1227
let forged = src.replacen ( " /files/" , " /files/9" , 1 ) ;
1228
assert_eq! ( get_bytes ( & h.app, & forged) .await.0 , StatusCode:: NOT_FOUND ) ;
1229
assert_eq! ( get_bytes ( & h.app, & src.replace ( " /index.html" , " /secret.txt" ) ) .await.0 , StatusCode:: NOT_FOUND ) ;
a36bab6b Andy McCall 25 days ago ↶
1230
let _ = run;
1231
1232
1233
{
1234
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
1235
let mut hosted = oxy_core:: repo:: Repo:: open( & h.app.repos.dir ( & name) ) .unwrap ( ) ;
1236
hosted.config.set ( " core.gc-grace" , " 0" ) .unwrap ( ) ;
1237
hosted.save_config ( ) .unwrap ( ) ;
1238
}
1239
let done = oxygenhub:: maintenance:: gc_all( & h.app) .await;
1240
assert! ( done.iter ( ) .all ( | d | ! d.failed) , " {done:?}" ) ;
1241
let hosted = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
1242
for ( path, bytes) in files {
1243
assert_eq! ( hosted.read_blob ( & oxy_core:: repo:: Repo:: blob_ref( bytes) ) .unwrap ( ) , bytes, " {path} survives gc" ) ;
1244
}
1245
}
1246
d29070fa Andy McCall 1 month ago ↶
1247
# [ tokio ::test ( flavor = " multi_thread" ) ]
1248
async fn a_failing_job_keeps_an_artifact_outside_its_receipt ( ) {
1249
let h = hub ( ) .await;
1250
create ( & h.app, " andy" , " demo" ) .await;
1251
let url = format! ( " {} /andy/demo" , h.base) ;
1252
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1253
let c = client ( & url, work.path ( ) ) ;
1254
write (
1255
work.path ( ) ,
1256
" .flow/pipeline.kdl" ,
1257
& format! (
336c953a Andy McCall 1 month ago ↶
1258
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" test\" {{ \n sandbox \" alpine\" \n exec \" make\" \" test\" \n output {{ \n artifact \" junit\" {{ \n path \" /src/out/junit.xml\" \n when \" always\" \n }} \n }} \n }} \n "
d29070fa Andy McCall 1 month ago ↶
1259
) ,
1260
) ;
1261
c.snapshot ( " test" ) .unwrap ( ) ;
1262
c.commit ( " Test" , & [ ] ) .unwrap ( ) ;
1263
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1264
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1265
let a = claim ( & h.app) .await.unwrap ( ) ;
336c953a Andy McCall 1 month ago ↶
1266
assert_eq! ( a[ " job" ] [ " outputs" ] [ 0 ] [ " always" ] , true , " the runner is told to keep it from a failing job" ) ;
d29070fa Andy McCall 1 month ago ↶
1267
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
0b203bbf Andy McCall 1 month ago ↶
1268
1269
let xml = br # "<testsuites><testsuite name="pytest"><testcase classname="tests.test_io" name="test_read" time="0.5"/><testcase classname="tests.test_io" name="test_write" time="1.5"><failure message="disk full">E OSError: disk full</failure></testcase></testsuite></testsuites>"# ;
1270
let ( st, text) = upload ( & h.app, id, " junit" , xml) .await;
d29070fa Andy McCall 1 month ago ↶
1271
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
1272
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1273
let mut r = receipt ( " test" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 1 ) ;
1274
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
1275
assert_eq! (
1276
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 1 , " duration_ms" : 700 , " receipt" : r.clone ( ) } ) ) .await,
1277
StatusCode:: BAD_REQUEST ,
1278
" a failing receipt names no outputs"
1279
) ;
1280
r[ " outputs" ] = serde_json:: json! ( [ ] ) ;
04683947 Andy McCall 9 days ago ↶
1281
1282
let phases = serde_json:: json! ( { " image" : 9000 , " prepare" : 1200 , " run" : 3400 , " finish" : 500 } ) ;
1283
assert_eq! (
1284
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 1 , " duration_ms" : 700 , " receipt" : r, " phases" : phases } ) ) .await,
1285
StatusCode:: NO_CONTENT
1286
) ;
d29070fa Andy McCall 1 month ago ↶
1287
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/flow/{} " , run.number) , None , None ) .await;
04683947 Andy McCall 9 days ago ↶
1288
let at = page.find ( " Where its time went: " ) .expect ( " the job's phases" ) ;
1289
let line = & page[ at.. at + 120 ] ;
1290
let order: Vec< usize > = [ " image" , " prepare" , " run" , " finish" ] .iter ( ) .map ( | p | line.find ( p) .expect ( p) ) .collect ( ) ;
1291
assert! ( order.windows ( 2 ) .all ( | w | w[ 0 ] < w[ 1 ] ) , " in the order they happened: {line}" ) ;
d29070fa Andy McCall 1 month ago ↶
1292
assert! ( page.contains ( & format! ( " /andy/demo/flow/{} /artifacts/test/junit" , run.number) ) , " listed" ) ;
1293
assert! ( page.contains ( " not in its receipt" ) , " marked" ) ;
0b203bbf Andy McCall 1 month ago ↶
1294
assert! ( page.contains ( " 2 tests: 1 failed" ) , " counts beside the job" ) ;
1295
assert! ( page.contains ( " tests.test_io::test_write" ) & & page.contains ( " disk full" ) & & page.contains ( " OSError" ) , " the failure" ) ;
1296
assert! ( page.contains ( " tests: 2 tests: 1 failed in 2.0 s" ) , " said in the job's log" ) ;
d29070fa Andy McCall 1 month ago ↶
1297
}
1298
df940d75 Andy McCall 1 month ago ↶
1299
1300
1301
1302
# [ tokio ::test ( flavor = " multi_thread" ) ]
1303
async fn a_release_is_audited_from_authors_to_receipts ( ) {
1304
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectType} ;
1305
use oxy_core:: identity:: SecretKey;
1306
use oxy_core:: objects:: StateObject;
1307
use oxy_core:: receipt:: SignedReceipt;
1308
let h = hub ( ) .await;
1309
create ( & h.app, " andy" , " demo" ) .await;
1310
let url = format! ( " {} /andy/demo" , h.base) ;
1311
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1312
let mut c = client ( & url, work.path ( ) ) ;
1313
c.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
1314
write (
1315
work.path ( ) ,
1316
" .flow/pipeline.kdl" ,
1317
& format! (
1318
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n }} \n }} \n "
1319
) ,
1320
) ;
1321
c.snapshot ( " test" ) .unwrap ( ) ;
1322
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
1323
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
1324
c.tag_create ( " v1" , & heads, " first" ) .unwrap ( ) ;
1325
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1326
1327
1328
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
1329
let a = claim ( & h.app) .await.unwrap ( ) ;
1330
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1331
let body: Vec< u8 > = b " #!/bin/sh\n echo tool\n " .to_vec ( ) ;
1332
let ( _ , text) = upload ( & h.app, id, " tool" , & body) .await;
1333
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1334
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
1335
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
1336
let heads_run: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1337
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads_run) .encode ( ) ) ;
1338
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
1339
let sk = SecretKey:: generate( ) ;
1340
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
1341
let fr: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
1342
let signed = hex:: encode( SignedReceipt:: sign( fr.to_oxy ( ) , repo_id, state, & format! ( " {url} /flow/1" ) , & runner, & sk) .encode ( ) ) ;
1343
let done = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 900 , " receipt" : r, " signed_receipt" : signed } ) ;
1344
assert_eq! ( finish ( & h.app, id, done) .await, StatusCode:: NO_CONTENT ) ;
1345
1346
1347
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1348
let rel = db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
1349
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " tool" , " " , & body) .await.unwrap ( ) ;
1350
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " notes.txt" , " " , b " by hand\n " ) .await.unwrap ( ) ;
1351
1352
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1" , None , None ) .await;
1353
assert_eq! ( st, StatusCode:: OK ) ;
1354
for want in [
1355
" Tag v1 is signed" ,
1356
" The one change in the release is authenticated" ,
1357
" tool: signed receipt from run #1 build" ,
1358
" a runner key of this hub" ,
edcef275 Andy McCall 1 month ago ↶
1359
" The tagged state is one change" ,
df940d75 Andy McCall 1 month ago ↶
1360
" tool: built from the tagged state" ,
1361
" tool: pinned environment" ,
1362
" notes.txt: no build found" ,
1363
" <svg class=\" trust-graph\" " ,
bf5101d3 Andy McCall 1 month ago ↶
1364
1365
1366
" Tag signed by" ,
1367
" 1 of 1 changes signed or sealed" ,
1368
" 1 of 2 assets built and attested by OxygenFlow" ,
1369
" attested build" ,
1370
" uploaded by hand" ,
1371
" receipt verified" ,
1372
" class=\" shackle\" " ,
1373
" <title>notes.txt:" ,
df940d75 Andy McCall 1 month ago ↶
1374
] {
1375
assert! ( page.contains ( want) , " missing {want:?}" ) ;
1376
}
bf5101d3 Andy McCall 1 month ago ↶
1377
let svg = & page[ page.find ( " <svg class=\" trust-graph\" " ) .unwrap ( ) .. ] ;
1378
assert! ( svg[ .. 200 ] .contains ( " width=\" " ) & & svg[ .. 200 ] .contains ( " height=\" " ) , " the graph has its own size: {}" , & svg[ .. 200 ] ) ;
df940d75 Andy McCall 1 month ago ↶
1379
let ( _ , releases) = call ( & h.app, " GET" , " /andy/demo/releases" , None , None ) .await;
1380
assert! ( releases.contains ( " /andy/demo/releases/trust/v1" ) , " the release links its audit" ) ;
1381
1382
1383
let ( st, json) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1?format=json" , None , None ) .await;
1384
assert_eq! ( st, StatusCode:: OK ) ;
1385
let t: serde_json:: Value = serde_json:: from_str( & json) .unwrap ( ) ;
1386
assert_eq! ( ( t[ " tag" ] .as_str ( ) , t[ " tag_signature" ] .as_str ( ) , t[ " changes" ] .as_i64 ( ) ) , ( Some ( " v1" ) , Some ( " valid" ) , Some ( 1 ) ) ) ;
1387
assert_eq! ( t[ " authors" ] [ 0 ] [ " who" ] [ " name" ] , " Andy" ) ;
1388
let tool = t[ " assets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | a | a[ " name" ] = = " tool" ) .unwrap ( ) ;
1389
let hex_receipt = tool[ " build" ] [ " signed_receipt" ] .as_str ( ) .unwrap ( ) ;
1390
let sr = SignedReceipt:: decode( & hex:: decode( hex_receipt) .unwrap ( ) ) .unwrap ( ) ;
1391
sr.verify ( ) .unwrap ( ) ;
1392
assert_eq! ( sr.receipt.outputs[ 0 ] .to_hex ( ) , blob) ;
1393
let audit = t[ " audit" ] .as_array ( ) .unwrap ( ) ;
1394
assert! ( audit.iter ( ) .any ( | c | c[ " verdict" ] = = " warn" & & c[ " title" ] .as_str ( ) .unwrap ( ) .starts_with ( " notes.txt" ) ) ) ;
1395
assert! ( ! audit.iter ( ) .any ( | c | c[ " verdict" ] = = " fail" ) , " {audit:?}" ) ;
1396
1397
let ( st, api) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases/trust/v1" , None , None ) .await;
1398
assert_eq! ( st, StatusCode:: OK ) ;
1399
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & api) .unwrap ( ) [ " assets" ] .as_array ( ) .unwrap ( ) .len ( ) , 2 ) ;
1400
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/demo/releases/trust/v9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
1401
assert_eq! ( call ( & h.app, " GET" , " /andy/demo/releases/trust/v9" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
1402
}
b6b7e4cf Andy McCall 1 month ago ↶
1403
1404
1405
1406
1407
# [ tokio ::test ( flavor = " multi_thread" ) ]
1408
async fn runners_register_show_their_history_and_are_drained_and_removed ( ) {
1409
use oxy_core:: identity:: SecretKey;
1410
let h = hub ( ) .await;
1411
create ( & h.app, " andy" , " demo" ) .await;
1412
1413
let res = oxygenhub:: router( h.app.clone ( ) )
1414
.oneshot (
1415
Request:: post( " /register" )
1416
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
c8684d44 Andy McCall 19 days ago
1417
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery&setup=test-setup" ) )
b6b7e4cf Andy McCall 1 month ago ↶
1418
.unwrap ( ) ,
1419
)
1420
.await
1421
.unwrap ( ) ;
1422
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1423
let admin = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
1424
db:: accounts:: set_admin( & h.app.db, admin.id, true ) .await.unwrap ( ) ;
1425
let web = | method : & 'static str , path : String, body : Option< String> | {
1426
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
1427
async move {
1428
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
1429
if body.is_some ( ) {
1430
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1431
}
1432
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
1433
let st = res.status ( ) ;
1434
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1435
}
1436
} ;
1437
let csrf_of = | page : & str | {
1438
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
1439
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1440
} ;
1441
let ( st, page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1442
assert_eq! ( st, StatusCode:: OK ) ;
1443
assert! ( page.contains ( " No runners yet" ) ) ;
1444
assert_eq! ( call ( & h.app, " GET" , " /admin/runners" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
1445
1446
1447
let ( _ , page) = web ( " POST" , " /admin/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1448
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1449
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1450
assert! ( token.starts_with ( " flowreg_" ) , " {token}" ) ;
1451
1452
1453
let sk = SecretKey:: generate( ) ;
1454
let identity = sk.identity ( " plex (OxygenFlow runner)" , " " , 1 ) ;
1455
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ " big-disk" ] , " max_jobs" : 2 , " memory_mib" : 16384 , " cpus" : 4 } ) ;
1456
let reg = serde_json:: json! ( { " token" : token, " name" : " plex" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1457
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1458
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1459
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1460
assert! ( credential.starts_with ( " flowrunner_" ) ) ;
1461
assert_eq! (
1462
call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await.0 ,
1463
StatusCode:: FORBIDDEN ,
1464
" used once"
1465
) ;
1466
1467
1468
let claim_as = | who : String| {
1469
let app = h.app.clone ( ) ;
1470
let info = info.clone ( ) ;
1471
async move {
1472
let body = serde_json:: json! ( { " runner" : " ignored" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
1473
call ( & app, " POST" , " /api/v1/flow/claim" , Some ( & who) , Some ( body) ) .await
1474
}
1475
} ;
1476
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: NO_CONTENT , " nothing queued" ) ;
1477
assert_eq! ( claim ( & h.app) .await, None ) ;
1478
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1479
assert! (
1480
page.contains ( " >plex</strong>" ) & & page.contains ( " st-online" ) & & page.contains ( " x86_64 · linux" ) & & page.contains ( " big-disk" ) ,
1481
" {page}"
1482
) ;
1483
assert! ( page.contains ( " >test-1</strong>" ) & & page.contains ( " hub.toml" ) , " the configured runner, by its claimed name" ) ;
1484
let plex = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
1485
assert_eq! ( plex.key, identity.hash ( ) .to_hex ( ) , " bound at registration" ) ;
1486
1487
1488
let url = format! ( " {} /andy/demo" , h.base) ;
1489
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1490
let c = client ( & url, work.path ( ) ) ;
1491
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1492
c.snapshot ( " test" ) .unwrap ( ) ;
1493
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1494
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1495
runs_of ( & h.app, " andy" , " demo" ) .await;
1496
let ( st, body) = claim_as ( credential.clone ( ) ) .await;
1497
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1498
let job: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1ec29691 Andy McCall 8 days ago ↶
1499
1500
let kept: String =
1501
sqlx:: query_scalar( " SELECT runner_version FROM flow_jobs WHERE runner_id = $1" ) .bind ( plex.id) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
1502
assert_eq! ( kept, " 0.1.0" ) ;
b6b7e4cf Andy McCall 1 month ago ↶
1503
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1504
assert! (
1505
page.contains ( " andy/demo" ) & & page.contains ( job[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) ) & & page.contains ( " Running now</dt><dd>1 job" ) ,
1506
" {page}"
1507
) ;
b66e70b7 Andy McCall 1 month ago ↶
1508
1509
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
e9b9619c Andy McCall 19 days ago ↶
1510
assert! ( page.contains ( " id=\" queue\" " ) & & page.contains ( " >plex</a></td>" ) , " {page}" ) ;
1511
1512
1513
assert! ( page.contains ( " running 1 of " ) & & page.contains ( " /admin/runners?runner=plex#queue" ) , " {page}" ) ;
1514
let ( _ , only) = web ( " GET" , " /admin/runners?runner=plex" .into ( ) , None ) .await;
1515
assert! ( only.contains ( " >plex</a></td>" ) & & only.contains ( " of " ) & & only.contains ( " >all</a>" ) , " {only}" ) ;
1516
let ( _ , none) = web ( " GET" , " /admin/runners?runner=nobody&status=running" .into ( ) , None ) .await;
1517
assert! ( none.contains ( " Nothing matches." ) , " {none}" ) ;
1518
1519
sqlx:: query( " UPDATE flow_runners SET last_seen_ms = 0 WHERE id = $1" ) .bind ( plex.id) .execute ( & h.app.db) .await.unwrap ( ) ;
1520
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1521
assert! ( page.contains ( " st-offline" ) , " quiet for long: offline" ) ;
1522
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/heartbeat" , Some ( & credential) , None ) .await.0 , StatusCode:: NO_CONTENT ) ;
1523
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/heartbeat" , Some ( " flowrunner_nope" ) , None ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1524
let plex_now = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
1525
assert_eq! ( plex_now.connectivity ( ) , " online" ) ;
b66e70b7 Andy McCall 1 month ago ↶
1526
let ( _ , tokens) = web ( " GET" , " /settings/tokens" .into ( ) , None ) .await;
1527
let ( _ , made) = web ( " POST" , " /settings/tokens" .into ( ) , Some ( format! ( " _csrf={} &name=admin&scopes=user%3Aread" , csrf_of ( & tokens) ) ) ) .await;
1528
let i = made.find ( " oxyhub_" ) .unwrap ( ) ;
1529
let admin_token: String = made[ i.. ] .chars ( ) .take_while ( | c | c.is_ascii_alphanumeric ( ) | | * c = = ' _' ) .collect ( ) ;
1530
let ( st, body) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( & admin_token) , None ) .await;
1531
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1532
let listed: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1533
let plex_row = listed.as_array ( ) .unwrap ( ) .iter ( ) .find ( | r | r[ " name" ] = = " plex" ) .unwrap ( ) ;
1534
assert_eq! ( ( plex_row[ " running" ] .as_i64 ( ) , plex_row[ " arch" ] .as_str ( ) ) , ( Some ( 1 ) , Some ( " x86_64" ) ) ) ;
7f5f6c05 Andy McCall 1 month ago ↶
1535
1536
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1537
let ( st, _ ) = web (
1538
" POST" ,
1539
format! ( " /admin/runners/{} /limits" , plex.id) ,
1540
Some ( format! ( " _csrf={} &labels=gpu%2C+plex&timeout_minutes=5" , csrf_of ( & page) ) ) ,
1541
)
1542
.await;
1543
assert_eq! ( st, StatusCode:: SEE_OTHER ) ;
1544
let ( _ , page) = web ( " GET" , format! ( " /admin/runners/{} " , plex.id) , None ) .await;
1545
assert! ( page.contains ( " >gpu<" ) & & page.contains ( " 5 min for its jobs" ) , " {page}" ) ;
1546
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( & admin_token) , None ) .await;
1547
let listed: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1548
let plex_row = listed.as_array ( ) .unwrap ( ) .iter ( ) .find ( | r | r[ " name" ] = = " plex" ) .unwrap ( ) ;
1549
assert! ( plex_row[ " labels" ] .as_array ( ) .unwrap ( ) .iter ( ) .any ( | l | l = = " gpu" ) , " {plex_row}" ) ;
1550
let ( st, _ ) = web (
1551
" POST" ,
1552
format! ( " /admin/runners/{} /limits" , plex.id) ,
1553
Some ( format! ( " _csrf={} &labels=bad%20label%21&timeout_minutes=" , csrf_of ( & page) ) ) ,
1554
)
1555
.await;
1556
assert_eq! ( st, StatusCode:: BAD_REQUEST , " a label with a space or punctuation is refused" ) ;
b66e70b7 Andy McCall 1 month ago ↶
1557
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/runners/queue" , Some ( & admin_token) , None ) .await;
1558
let queue: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1559
assert! ( queue.as_array ( ) .unwrap ( ) .iter ( ) .any ( | q | q[ " status" ] = = " running" & & q[ " runner" ] = = " plex" ) , " {queue}" ) ;
1560
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/runners/{} /jobs" , plex.id) , Some ( & admin_token) , None ) .await;
1561
let jobs: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1562
assert_eq! ( jobs[ 0 ] [ " repo" ] , " andy/demo" , " {jobs}" ) ;
1563
assert_eq! ( call ( & h.app, " GET" , " /api/v1/runners" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
b6b7e4cf Andy McCall 1 month ago ↶
1564
1565
1566
let ( _ , page) = web ( " GET" , " /admin/runners" .into ( ) , None ) .await;
1567
let csrf = csrf_of ( & page) ;
1568
web ( " POST" , format! ( " /admin/runners/{} /state" , plex.id) , Some ( format! ( " _csrf={csrf} &state=draining" ) ) ) .await;
1569
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: NO_CONTENT , " a draining runner takes nothing" ) ;
1570
let ( _ , page) = web ( " POST" , format! ( " /admin/runners/{} /remove" , plex.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1571
assert! ( page.contains ( " is running jobs: drain it first" ) , " {page}" ) ;
1572
1573
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1574
let done = call (
1575
& h.app,
1576
" POST" ,
1577
& format! ( " /api/v1/flow/jobs/{id} /finish" ) ,
1578
Some ( & credential) ,
1579
Some ( r # "{"exit_code":0,"duration_ms":5}"# .into ( ) ) ,
1580
)
1581
.await;
1582
assert_eq! ( done.0 , StatusCode:: NO_CONTENT ) ;
1583
web ( " POST" , format! ( " /admin/runners/{} /state" , plex.id) , Some ( format! ( " _csrf={csrf} &state=disabled" ) ) ) .await;
1584
assert_eq! ( claim_as ( credential.clone ( ) ) .await.0 , StatusCode:: FORBIDDEN ) ;
1585
let ( _ , page) = web ( " POST" , format! ( " /admin/runners/{} /remove" , plex.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1586
assert! ( page.contains ( " Removed plex." ) , " {page}" ) ;
1587
assert_eq! ( claim_as ( credential) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1588
}
74be9796 Andy McCall 1 month ago ↶
1589
1590
1591
1592
# [ tokio ::test ( flavor = " multi_thread" ) ]
1593
async fn scoped_runners_take_only_their_scope ( ) {
1594
use oxy_core:: identity:: SecretKey;
1595
let h = hub ( ) .await;
1596
create ( & h.app, " andy" , " demo" ) .await;
1597
create ( & h.app, " andy" , " other" ) .await;
1598
let register = | scope : db:: runners:: Scope, name : & 'static str | {
1599
let app = h.app.clone ( ) ;
1600
async move {
1601
let token = format! ( " flowreg_{name} " ) ;
1602
db:: runners:: add_registration( & app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & scope) .await.unwrap ( ) ;
1603
let identity = SecretKey:: generate( ) .identity ( name, " " , 1 ) ;
1604
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
1605
let reg = serde_json:: json! ( { " token" : token, " name" : name, " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1606
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1607
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1608
serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( )
1609
}
1610
} ;
1611
let demo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1612
let only_demo = register ( db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: Some ( demo.id) } , " demo-runner" ) .await;
1613
let carols = register ( db:: runners:: Scope { owner: " carol" .into ( ) , repo_id: None } , " carol-runner" ) .await;
1614
let andys = register ( db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: None } , " andy-runner" ) .await;
1615
let claim_as = | who : String| {
1616
let app = h.app.clone ( ) ;
1617
async move {
1618
let body = serde_json:: json! ( { " runner" : " x" , " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
1619
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( & who) , Some ( body) ) .await;
1620
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) )
1621
}
1622
} ;
1623
1624
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1625
let c = client ( & format! ( " {} /andy/other" , h.base) , work.path ( ) ) ;
1626
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1627
c.snapshot ( " t" ) .unwrap ( ) ;
1628
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
1629
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1630
assert! ( ! runs_of ( & h.app, " andy" , " other" ) .await.is_empty ( ) ) ;
1631
assert_eq! ( claim_as ( only_demo.clone ( ) ) .await, None , " nothing of andy/demo is queued" ) ;
1632
assert_eq! ( claim_as ( carols) .await, None , " nothing of carol's" ) ;
1633
assert_eq! ( claim_as ( andys) .await.as_deref ( ) , Some ( " andy/other" ) , " the owner's runner takes its repositories' jobs" ) ;
1634
1635
assert_eq! ( claim ( & h.app) .await.unwrap ( ) [ " repo" ] , " andy/other" ) ;
1636
assert_eq! ( claim_as ( only_demo) .await, None , " still nothing of its own" ) ;
1637
}
7974adb0 Andy McCall 1 month ago ↶
1638
5714347a Andy McCall 11 days ago ↶
1639
1640
1641
1642
# [ tokio ::test ( flavor = " multi_thread" ) ]
1643
async fn a_user_registers_a_runner_for_their_own_repositories ( ) {
1644
use oxy_core:: identity:: SecretKey;
1645
let h = hub ( ) .await;
1646
create ( & h.app, " andy" , " demo" ) .await;
1647
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " " , " salt" , false ) .await.unwrap ( ) ;
1648
let pat = " oxyhub_phone_test_token" ;
1649
db:: accounts:: create_token( & h.app.db, andy.id, " phone" , & oxygenhub:: auth:: token_hash( pat) , " repo:read,repo:write" , " " , None )
1650
.await
1651
.unwrap ( ) ;
1652
let read_only = " oxyhub_read_only_token" ;
1653
db:: accounts:: create_token( & h.app.db, andy.id, " ro" , & oxygenhub:: auth:: token_hash( read_only) , " repo:read" , " " , None ) .await.unwrap ( ) ;
1654
assert_eq! ( call ( & h.app, " POST" , " /api/v1/user/runner-token" , Some ( read_only) , None ) .await.0 , StatusCode:: FORBIDDEN ) ;
1655
assert_eq! ( call ( & h.app, " POST" , " /api/v1/user/runner-token" , None , None ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
1656
let ( st, body) = call ( & h.app, " POST" , " /api/v1/user/runner-token" , Some ( pat) , None ) .await;
1657
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
1658
let got: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
1659
let token = got[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1660
assert! ( token.starts_with ( " flowreg_" ) & & got[ " owner" ] = = " andy" , " {body}" ) ;
1661
let identity = SecretKey:: generate( ) .identity ( " andy's phone" , " " , 1 ) ;
1662
let info = serde_json:: json! ( { " arch" : " aarch64" , " os" : " android" , " version" : " 0.3.1" , " labels" : [ " android" ] , " max_jobs" : 1 } ) ;
1663
let reg = serde_json:: json! ( { " token" : token, " name" : " pixel" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1664
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1665
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1666
let phone = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " pixel" ) .unwrap ( ) ;
1667
assert_eq! ( ( phone.scope_owner.as_str ( ) , phone.scope_repo_id) , ( " andy" , None ) , " andy's repositories, all of them, and no one else's" ) ;
1668
}
1669
7974adb0 Andy McCall 1 month ago ↶
1670
1671
1672
1673
# [ tokio ::test ( flavor = " multi_thread" ) ]
1674
async fn repository_admins_add_and_remove_their_own_runners ( ) {
1675
use oxy_core:: identity:: SecretKey;
1676
let h = hub ( ) .await;
1677
let res = oxygenhub:: router( h.app.clone ( ) )
1678
.oneshot (
1679
Request:: post( " /register" )
1680
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
c8684d44 Andy McCall 19 days ago
1681
.body ( Body:: from( " username=andy&email=andy%40example.com&password=correct-horse-battery&setup=test-setup" ) )
7974adb0 Andy McCall 1 month ago ↶
1682
.unwrap ( ) ,
1683
)
1684
.await
1685
.unwrap ( ) ;
1686
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
1687
create ( & h.app, " andy" , " demo" ) .await;
1688
let demo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
1689
let web = | method : & 'static str , path : String, body : Option< String> | {
1690
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
1691
async move {
1692
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
1693
if body.is_some ( ) {
1694
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
1695
}
1696
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
1697
let st = res.status ( ) ;
1698
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
1699
}
1700
} ;
1701
let csrf_of = | page : & str | {
1702
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
1703
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
1704
} ;
c7067c7b Andy McCall 16 days ago
1705
let ( st, page) = web ( " GET" , " /andy/demo/settings/runners" .into ( ) , None ) .await;
7974adb0 Andy McCall 1 month ago ↶
1706
assert_eq! ( st, StatusCode:: OK ) ;
c7067c7b Andy McCall 16 days ago
1707
assert! ( page.contains ( " <h2>Runners</h2>" ) , " the owner sees the page" ) ;
7974adb0 Andy McCall 1 month ago ↶
1708
let ( _ , page) = web ( " POST" , " /andy/demo/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1709
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1710
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1711
let identity = SecretKey:: generate( ) .identity ( " mine" , " " , 1 ) ;
1712
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
1713
let reg = serde_json:: json! ( { " token" : token, " name" : " mine" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1714
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1715
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1716
let mine = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " mine" ) .unwrap ( ) ;
1717
assert_eq! ( mine.scope ( ) , db:: runners:: Scope { owner: " andy" .into ( ) , repo_id: Some ( demo.id) } ) ;
1718
1719
claim ( & h.app) .await;
1720
let site = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " test-1" ) .unwrap ( ) ;
c7067c7b Andy McCall 16 days ago
1721
let ( _ , page) = web ( " GET" , " /andy/demo/settings/runners" .into ( ) , None ) .await;
7974adb0 Andy McCall 1 month ago ↶
1722
assert! ( page.contains ( " <strong>mine</strong>" ) & & page.contains ( " this repository" ) , " {page}" ) ;
1723
assert! ( page.contains ( " <strong>test-1</strong>" ) & & page.contains ( " the whole site" ) , " {page}" ) ;
1724
let csrf = csrf_of ( & page) ;
1725
let ( st, _ ) = web ( " POST" , format! ( " /andy/demo/settings/runners/{} /remove" , site.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1726
assert_eq! ( st, StatusCode:: NOT_FOUND , " not the repository's own" ) ;
c7067c7b Andy McCall 16 days ago
1727
let ( st, _ ) = web ( " POST" , format! ( " /andy/demo/settings/runners/{} /remove" , mine.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
1728
assert_eq! ( st, StatusCode:: SEE_OTHER , " back to the Runners page" ) ;
1729
let ( _ , page) = web ( " GET" , " /andy/demo/settings/runners?notice=x" .into ( ) , None ) .await;
1730
assert! ( ! page.contains ( " <strong>mine</strong>" ) , " {page}" ) ;
7974adb0 Andy McCall 1 month ago ↶
1731
assert! ( db:: runners:: get( & h.app.db, mine.id) .await.unwrap ( ) .is_none ( ) ) ;
1732
1733
1734
let ( _ , page) = web ( " GET" , " /orgs/new" .into ( ) , None ) .await;
1735
web ( " POST" , " /orgs/new" .into ( ) , Some ( format! ( " _csrf={} &name=acme&display_name=Acme" , csrf_of ( & page) ) ) ) .await;
1736
let ( st, page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
1737
assert_eq! ( st, StatusCode:: OK ) ;
1738
let ( _ , page) = web ( " POST" , " /orgs/acme/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1739
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
1740
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
1741
let identity = SecretKey:: generate( ) .identity ( " acme-runner" , " " , 1 ) ;
1742
let reg = serde_json:: json! ( { " token" : token, " name" : " acme-runner" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1743
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await.0 , StatusCode:: OK ) ;
1744
let acme = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " acme-runner" ) .unwrap ( ) ;
1745
assert_eq! ( acme.scope ( ) , db:: runners:: Scope { owner: " acme" .into ( ) , repo_id: None } ) ;
1746
let ( _ , page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
1747
assert! ( page.contains ( " <strong>acme-runner</strong>" ) & & page.contains ( " the organisation's repositories" ) , " {page}" ) ;
1748
let ( _ , page) = web ( " POST" , format! ( " /orgs/acme/settings/runners/{} /remove" , acme.id) , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
1749
assert! ( page.contains ( " acme-runner removed" ) , " {page}" ) ;
1750
}
c4c87f48 Andy McCall 1 month ago ↶
1751
1752
1753
1754
# [ tokio ::test ( flavor = " multi_thread" ) ]
1755
async fn jobs_that_ask_for_labels_wait_for_a_runner_with_them ( ) {
1756
use oxy_core:: identity:: SecretKey;
1757
let h = hub ( ) .await;
1758
create ( & h.app, " andy" , " demo" ) .await;
1759
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1760
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
1761
let cross = " job \" cross\" {\n sandbox \" alpine\" \n runs-on \" riscv64\" \n exec \" true\" \n }\n " ;
1762
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( cross) ) ;
1763
c.snapshot ( " t" ) .unwrap ( ) ;
1764
c.commit ( " pipeline" , & [ ] ) .unwrap ( ) ;
1765
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1766
assert! ( ! runs_of ( & h.app, " andy" , " demo" ) .await.is_empty ( ) ) ;
1767
1768
let mut taken = Vec :: new( ) ;
1769
while let Some ( j) = claim ( & h.app) .await {
1770
taken.push ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
1771
}
1772
assert! ( ! taken.contains ( & " cross" .to_string ( ) ) & & taken.contains ( & " lint" .to_string ( ) ) , " {taken:?}" ) ;
1773
let queue = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
1774
let waiting = queue.iter ( ) .find ( | q | q.job = = " cross" ) .unwrap ( ) ;
1775
assert_eq! ( ( waiting.status.as_str ( ) , waiting.needs ( ) ) , ( " queued" , vec! [ " riscv64" .to_string ( ) ] ) ) ;
1776
1777
let token = " flowreg_riscv" ;
1778
db:: runners:: add_registration( & h.app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & Default :: default( ) ) .await.unwrap ( ) ;
1779
let identity = SecretKey:: generate( ) .identity ( " visionfive" , " " , 1 ) ;
1780
let info = serde_json:: json! ( { " arch" : " riscv64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ " riscv64" ] , " max_jobs" : 1 } ) ;
1781
let reg = serde_json:: json! ( { " token" : token, " name" : " visionfive" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
1782
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
1783
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1784
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1785
let claim_body = serde_json:: json! ( { " runner" : " x" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
1786
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( claim_body) ) .await;
1787
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1788
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job" ] [ " name" ] , " cross" ) ;
1789
}
070a7b64 Andy McCall 1 month ago ↶
1790
1791
1792
1793
1794
1795
# [ tokio ::test ( flavor = " multi_thread" ) ]
1796
async fn a_job_no_runner_reports_is_ended_after_the_time_limit ( ) {
1797
let h = hub ( ) .await;
1798
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1799
create ( & h.app, " andy" , " lost" ) .await;
1800
let url = format! ( " {} /andy/lost" , h.base) ;
1801
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1802
let c = client ( & url, work.path ( ) ) ;
1803
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1804
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1805
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1806
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1807
assert_eq! ( runs_of ( & h.app, " andy" , " lost" ) .await.len ( ) , 1 ) ;
1808
let job = claim ( & h.app) .await.expect ( " a job to claim" ) ;
1809
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1810
1811
1812
assert_eq! ( oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) , 0 ) ;
1813
1814
let old = db:: now_ms( ) - 24 * 3600 * 1000 ;
3b1bc27f Andy McCall 20 days ago
1815
sqlx:: query( " UPDATE flow_jobs SET started_ms = $1 WHERE id = $2" ) .bind ( old) .bind ( id) .execute ( & h.app.db) .await.unwrap ( ) ;
070a7b64 Andy McCall 1 month ago ↶
1816
assert_eq! ( oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) , 1 ) ;
1817
let j = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
1818
assert_eq! ( j.status, " error" ) ;
1819
assert! ( j.error.starts_with ( " lost:" ) , " {}" , j.error) ;
1820
let run = & runs_of ( & h.app, " andy" , " lost" ) .await[ 0 ] ;
1821
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
1822
assert! ( jobs.iter ( ) .all ( | j | j.status ! = " running" ) , " nothing left running" ) ;
1823
1824
assert_ne! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 } ) ) .await, StatusCode:: NO_CONTENT ) ;
1825
assert_eq! ( db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .status, " error" ) ;
1826
}
b0612e7d Andy McCall 1 month ago ↶
1827
1828
1829
1830
# [ tokio ::test ( flavor = " multi_thread" ) ]
1831
async fn a_runner_is_offered_only_jobs_whose_image_it_holds ( ) {
1832
let h = hub ( ) .await;
1833
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1834
create ( & h.app, " andy" , " images" ) .await;
1835
let url = format! ( " {} /andy/images" , h.base) ;
1836
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1837
let c = client ( & url, work.path ( ) ) ;
1838
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1839
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1840
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1841
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1842
assert_eq! ( runs_of ( & h.app, " andy" , " images" ) .await.len ( ) , 1 ) ;
1843
let claim_with = | images : Vec< String> | {
1844
let body = serde_json:: json! ( { " runner" : " test-1" , " runtimes" : [ " microvm" ] , " images" : images } ) .to_string ( ) ;
1845
let app = h.app.clone ( ) ;
1846
async move {
1847
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body) ) .await;
1848
( st, body)
1849
}
1850
} ;
1851
let ( st, _ ) = claim_with ( vec! [ " ab" .repeat ( 32 ) ] ) .await;
1852
assert_eq! ( st, StatusCode:: NO_CONTENT , " no job for a runner without the image" ) ;
1853
let jobs = db:: flow:: jobs( & h.app.db, runs_of ( & h.app, " andy" , " images" ) .await[ 0 ] .id) .await.unwrap ( ) ;
1854
let rootfs: serde_json:: Value = serde_json:: from_str( & jobs[ 0 ] .sandbox) .unwrap ( ) ;
1855
let rootfs = rootfs[ " rootfs" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
1856
let ( st, body) = claim_with ( vec! [ " ab" .repeat ( 32 ) , rootfs] ) .await;
1857
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
1858
}
eef677c4 Andy McCall 1 month ago ↶
1859
1860
1861
1862
1863
# [ tokio ::test ( flavor = " multi_thread" ) ]
1864
async fn a_job_its_runner_no_longer_holds_is_ended_at_its_next_claim ( ) {
1865
let h = hub ( ) .await;
1866
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1867
create ( & h.app, " andy" , " held" ) .await;
1868
let url = format! ( " {} /andy/held" , h.base) ;
1869
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1870
let c = client ( & url, work.path ( ) ) ;
1871
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
1872
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1873
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
1874
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1875
assert_eq! ( runs_of ( & h.app, " andy" , " held" ) .await.len ( ) , 1 ) ;
1876
let id = claim ( & h.app) .await.expect ( " a job to claim" ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
1877
let claim_holding = | holding : Vec< i64 > | {
1878
let body = serde_json:: json! ( { " runner" : " test-1" , " runtimes" : [ " microvm" ] , " holding" : holding } ) .to_string ( ) ;
1879
let app = h.app.clone ( ) ;
1880
async move { call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body) ) .await.0 }
1881
} ;
1882
claim_holding ( vec! [ id] ) .await;
1883
assert_eq! ( db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .status, " running" , " a held job stays" ) ;
1884
claim_holding ( vec! [ ] ) .await;
1885
let j = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
1886
assert_eq! ( j.status, " error" ) ;
1887
assert! ( j.error.starts_with ( " lost:" ) & & j.error.contains ( " no longer holds" ) , " {}" , j.error) ;
1888
let run = & runs_of ( & h.app, " andy" , " held" ) .await[ 0 ] ;
1889
assert! ( db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .iter ( ) .all ( | j | j.status ! = " running" ) , " the run settles" ) ;
1890
}
d7ce41ac Andy McCall 1 month ago ↶
1891
1892
1893
1894
1895
# [ tokio ::test ( flavor = " multi_thread" ) ]
1896
async fn a_scoped_job_runs_only_when_a_push_touches_its_paths ( ) {
1897
let h = hub ( ) .await;
1898
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1899
create ( & h.app, " andy" , " scoped" ) .await;
1900
let url = format! ( " {} /andy/scoped" , h.base) ;
1901
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1902
let c = client ( & url, work.path ( ) ) ;
1903
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \" packaging/**\" \n exec \" true\" \n }\n " ;
1904
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1905
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1906
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
1907
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1908
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1909
let names = | app : Shared, n : usize | async move {
1910
let mut runs = Vec :: new( ) ;
1911
for _ in 0 .. 100 {
1912
runs = runs_of ( & app, " andy" , " scoped" ) .await;
1913
if runs.len ( ) > = n {
1914
break ;
1915
}
1916
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1917
}
1918
assert_eq! ( runs.len ( ) , n, " runs" ) ;
1919
let mut j: Vec< String> = db:: flow:: jobs( & app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1920
j.sort ( ) ;
745d225d Andy McCall 1 month ago ↶
1921
3b1bc27f Andy McCall 20 days ago
1922
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = $1" ) .bind ( runs[ 0 ] .id) .execute ( & app.db) .await.unwrap ( ) ;
d7ce41ac Andy McCall 1 month ago ↶
1923
j
1924
} ;
1925
assert_eq! ( names ( h.app.clone ( ) , 1 ) .await, [ " build" , " dist" , " lint" , " test" ] , " the first run has every job" ) ;
1926
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
1927
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
1928
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1929
assert_eq! ( names ( h.app.clone ( ) , 2 ) .await, [ " build" , " lint" , " test" ] , " dist is out of scope" ) ;
1930
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
1931
c.commit ( " Release 0.1.1" , & [ ] ) .unwrap ( ) ;
1932
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1933
assert_eq! ( names ( h.app.clone ( ) , 3 ) .await, [ " build" , " dist" , " lint" , " test" ] , " a version bump runs dist" ) ;
1934
}
5dead1f4 Andy McCall 1 month ago ↶
1935
367f34b8 Andy McCall 15 days ago ↶
1936
1937
1938
1939
# [ tokio ::test ( flavor = " multi_thread" ) ]
1940
async fn a_push_can_skip_its_run ( ) {
1941
let h = hub ( ) .await;
1942
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
1943
create ( & h.app, " andy" , " skip" ) .await;
1944
let url = format! ( " {} /andy/skip" , h.base) ;
1945
let work = tempfile:: tempdir( ) .unwrap ( ) ;
1946
let c = client ( & url, work.path ( ) ) ;
1947
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \n exec \" true\" \n }\n " ;
1948
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
1949
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
1950
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
1951
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
1952
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1953
let newest = | app : Shared, n : usize | async move {
1954
for _ in 0 .. 100 {
1955
let runs = runs_of ( & app, " andy" , " skip" ) .await;
1956
if runs.len ( ) > = n {
1957
assert_eq! ( runs.len ( ) , n, " runs" ) ;
1958
let r = runs[ 0 ] .clone ( ) ;
1959
let mut jobs: Vec< String> = db:: flow:: jobs( & app.db, r.id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
1960
jobs.sort ( ) ;
1961
if r.status ! = " skipped" {
1962
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = $1" ) .bind ( r.id) .execute ( & app.db) .await.unwrap ( ) ;
1963
}
1964
return ( r, jobs) ;
1965
}
1966
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
1967
}
1968
panic! ( " no run {n}" ) ;
1969
} ;
1970
assert_eq! ( newest ( h.app.clone ( ) , 1 ) .await.1 , [ " build" , " dist" , " lint" , " test" ] ) ;
1971
1972
1973
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
1974
c.commit ( " Bump the version [Skip CI]" , & [ ] ) .unwrap ( ) ;
1975
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1976
let ( run, jobs) = newest ( h.app.clone ( ) , 2 ) .await;
1977
assert_eq! ( run.status, " skipped" ) ;
1978
assert! ( jobs.is_empty ( ) , " {jobs:?}" ) ;
1979
assert! ( run.error.starts_with ( " [skip ci] in change " ) & & run.error.contains ( " , pushed by " ) , " {}" , run.error) ;
1980
assert! ( run.finished_ms.is_some ( ) ) ;
1981
let ( _ , page) = call ( & h.app, " GET" , " /andy/skip/flow/2" , None , None ) .await;
1982
assert! ( page.contains ( " the push asked to skip this run" ) , " {page}" ) ;
1983
1984
1985
1986
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
1987
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
1988
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1989
assert_eq! ( newest ( h.app.clone ( ) , 3 ) .await.1 , [ " build" , " dist" , " lint" , " test" ] , " the skipped bump still runs dist" ) ;
1990
1991
1992
let row = db:: repos:: get( & h.app.db, " andy" , " skip" ) .await.unwrap ( ) .unwrap ( ) ;
1993
db:: repos:: set_flow_skip( & h.app.db, row.id, " others" ) .await.unwrap ( ) ;
1994
write ( work.path ( ) , " test.sh" , " echo ok again\n " ) ;
1995
c.commit ( " Docs only [skip flow]" , & [ ] ) .unwrap ( ) ;
1996
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
1997
let ( run, jobs) = newest ( h.app.clone ( ) , 4 ) .await;
1998
assert_ne! ( run.status, " skipped" ) ;
1999
assert_eq! ( jobs, [ " build" , " lint" , " test" ] ) ;
2000
let row = db:: repos:: get( & h.app.db, " andy" , " skip" ) .await.unwrap ( ) .unwrap ( ) ;
2001
assert! ( oxygenhub:: flow:: skip_allowed( & row, " feature" ) & & ! oxygenhub:: flow:: skip_allowed( & row, " main" ) ) ;
2002
db:: repos:: set_flow_skip( & h.app.db, row.id, " never" ) .await.unwrap ( ) ;
2003
let row = db:: repos:: get( & h.app.db, " andy" , " skip" ) .await.unwrap ( ) .unwrap ( ) ;
2004
assert! ( ! oxygenhub:: flow:: skip_allowed( & row, " feature" ) ) ;
2005
}
2006
5588bb36 Andy McCall 15 days ago ↶
2007
2008
2009
2010
# [ tokio ::test ( flavor = " multi_thread" ) ]
2011
async fn a_push_reuses_a_pass_on_the_same_state ( ) {
2012
let h = hub ( ) .await;
2013
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2014
create ( & h.app, " andy" , " reuse" ) .await;
2015
let url = format! ( " {} /andy/reuse" , h.base) ;
2016
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2017
let c = client ( & url, work.path ( ) ) ;
2018
let gated = " job \" deploy\" {\n sandbox \" alpine\" \n depends-on \" test\" \n approval {\n by \" maintain\" \n }\n exec \" true\" \n }\n " ;
2019
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( gated) ) ;
2020
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2021
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
2022
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2023
let first = runs_of ( & h.app, " andy" , " reuse" ) .await.remove ( 0 ) ;
2024
2025
sqlx:: query( " UPDATE flow_jobs SET status = 'passed', exit_code = 0, receipt_hash = 'r-' || name WHERE run_id = $1" )
2026
.bind ( first.id)
2027
.execute ( & h.app.db)
2028
.await
2029
.unwrap ( ) ;
2030
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = $1" ) .bind ( first.id) .execute ( & h.app.db) .await.unwrap ( ) ;
2031
let heads: Vec< oxy_core:: hash:: ChangeHash> = first.head_list ( ) .into_iter ( ) .filter_map ( oxy_core:: hash:: ChangeHash:: from_hex) .collect ( ) ;
2032
let row = db:: repos:: get( & h.app.db, " andy" , " reuse" ) .await.unwrap ( ) .unwrap ( ) ;
2033
assert! ( row.flow_reuse, " on by default" ) ;
2034
let again = | row : db:: repos:: RepoRow| {
2035
let ( app, state, heads) = ( h.app.clone ( ) , first.state.clone ( ) , heads.clone ( ) ) ;
2036
async move {
2037
let n =
2038
oxygenhub:: flow:: start_run_scoped( & app, & row, " main" , & state, & heads, " andy" , " push" , None ) .await.unwrap ( ) .expect ( " a run" ) ;
2039
let run = db:: flow:: run( & app.db, row.id, n) .await.unwrap ( ) .unwrap ( ) ;
2040
let jobs = db:: flow:: jobs( & app.db, run.id) .await.unwrap ( ) ;
2041
( run, jobs)
2042
}
2043
} ;
2044
let ( run, jobs) = again ( row.clone ( ) ) .await;
2045
let by = | name : & str | jobs.iter ( ) .find ( | j | j.name = = name ) .unwrap ( ) .clone ( ) ;
2046
for name in [ " build" , " test" , " lint" ] {
2047
let j = by ( name) ;
2048
assert_eq! ( ( j.status.as_str ( ) , j.reused_from ( ) ) , ( " passed" , Some ( first.number) ) , " {name}: {j:?}" ) ;
2049
assert_eq! ( j.receipt_hash, format! ( " r-{name} " ) , " the pass's receipt stands for it" ) ;
2050
}
2051
let deploy = by ( " deploy" ) ;
2052
assert! ( deploy.status = = " queued" & & deploy.reused_from ( ) .is_none ( ) , " a gated job runs again: {deploy:?}" ) ;
2053
assert_eq! ( run.status, " queued" , " the run waits for the gated job" ) ;
2054
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/reuse/flow/{} " , run.number) , None , None ) .await;
2055
assert! ( page.contains ( " data-reused" ) & & page.contains ( & format! ( " /andy/reuse/flow/{} " , first.number) ) , " {page}" ) ;
2056
2057
let checks = db:: hooks:: checks_for_state( & h.app.db, row.id, & first.state) .await.unwrap ( ) ;
2058
assert! (
2059
checks.iter ( ) .any ( | c | c.name = = " flow/build" & & c.status = = " passed" & & c.summary.contains ( " reused" ) ) ,
2060
" a reused pass reports its check: {checks:?}"
2061
) ;
2062
2063
2064
db:: repos:: set_flow_reuse( & h.app.db, row.id, false ) .await.unwrap ( ) ;
2065
let row = db:: repos:: get( & h.app.db, " andy" , " reuse" ) .await.unwrap ( ) .unwrap ( ) ;
2066
let ( _ , jobs) = again ( row) .await;
2067
assert! ( jobs.iter ( ) .all ( | j | j.status = = " queued" & & j.reused_from ( ) .is_none ( ) ) , " {jobs:?}" ) ;
2068
}
2069
42cd46c9 Andy McCall 9 days ago ↶
2070
2071
2072
2073
# [ tokio ::test ( flavor = " multi_thread" ) ]
2074
async fn a_fork_reuses_what_upstream_proved ( ) {
2075
let h = hub ( ) .await;
2076
for who in [ " andy" , " bob" ] {
2077
db:: accounts:: create_account( & h.app.db, who, & format! ( " {who} @example.com" ) , " x" , " salt" , true ) .await.unwrap ( ) ;
2078
}
2079
create ( & h.app, " andy" , " game" ) .await;
2080
create ( & h.app, " bob" , " game" ) .await;
2081
let up = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
2082
let fork = db:: repos:: get( & h.app.db, " bob" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
2083
db:: repos:: set_forked_from( & h.app.db, fork.id, up.id) .await.unwrap ( ) ;
2084
2085
sqlx:: query( " UPDATE repositories SET flow = 'on' WHERE id = $1" ) .bind ( fork.id) .execute ( & h.app.db) .await.unwrap ( ) ;
2086
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2087
let mut c = client ( & format! ( " {} /andy/game" , h.base) , work.path ( ) ) ;
2088
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2089
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2090
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
2091
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2092
let first = runs_of ( & h.app, " andy" , " game" ) .await.remove ( 0 ) ;
06b75635 Andy McCall 8 days ago ↶
2093
2094
2095
{
2096
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectType} ;
2097
use oxy_core:: identity:: SecretKey;
2098
use oxy_core:: objects:: StateObject;
2099
use oxy_core:: receipt:: SignedReceipt;
2100
let heads: Vec< ChangeHash> = first.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
2101
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads) .encode ( ) ) ;
2102
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/game" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
2103
let sk = SecretKey:: generate( ) ;
2104
let runner = sk.identity ( " test-1 (OxygenFlow runner)" , " " , 1 ) ;
2105
let run_url = format! ( " {} /andy/game/flow/{} " , h.base, first.number) ;
2106
while let Some ( j) = claim ( & h.app) .await {
2107
let r = receipt ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
2108
let parsed: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
2109
let signed = hex:: encode( SignedReceipt:: sign( parsed.to_oxy ( ) , repo_id, state, & run_url, & runner, & sk) .encode ( ) ) ;
2110
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 5 , " receipt" : r, " signed_receipt" : signed } ) ;
2111
assert_eq! ( finish ( & h.app, j[ " job_id" ] .as_i64 ( ) .unwrap ( ) , body) .await, StatusCode:: NO_CONTENT ) ;
2112
}
2113
}
2114
let up_jobs = db:: flow:: jobs( & h.app.db, first.id) .await.unwrap ( ) ;
2115
assert! ( up_jobs.iter ( ) .all ( | j | j.status = = " passed" & & j.receipt_attest.len ( ) = = 64 ) , " {up_jobs:?}" ) ;
42cd46c9 Andy McCall 9 days ago ↶
2116
2117
c.config.set ( " remote.origin.url" , & format! ( " {} /bob/game" , h.base) ) .unwrap ( ) ;
2118
c.save_config ( ) .unwrap ( ) ;
2119
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2120
let run = runs_of ( & h.app, " bob" , " game" ) .await.remove ( 0 ) ;
2121
assert_eq! ( run.state, first.state) ;
2122
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
2123
for j in & jobs {
2124
assert_eq! ( ( j.status.as_str ( ) , j.reused_from ( ) , j.reused_repo ( ) ) , ( " passed" , Some ( first.number) , Some ( " andy/game" ) ) , " {j:?}" ) ;
06b75635 Andy McCall 8 days ago ↶
2125
let from = up_jobs.iter ( ) .find ( | u | u.name = = j.name ) .unwrap ( ) ;
2126
assert_eq! ( j.receipt_attest, from.receipt_attest, " upstream's signed receipt stands for it" ) ;
42cd46c9 Andy McCall 9 days ago ↶
2127
}
2128
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /bob/game/flow/{} " , run.number) , None , None ) .await;
2129
assert! ( page.contains ( & format! ( " href=\" /andy/game/flow/{} \" >andy/game run #{} </a>" , first.number, first.number) ) , " {page}" ) ;
2130
let checks = db:: hooks:: checks_for_state( & h.app.db, fork.id, & run.state) .await.unwrap ( ) ;
2131
assert! ( checks.iter ( ) .any ( | c | c.summary.contains ( " passed in andy/game run #" ) ) , " {checks:?}" ) ;
06b75635 Andy McCall 8 days ago ↶
2132
2133
2134
2135
let lint = up_jobs.iter ( ) .find ( | j | j.name = = " lint" ) .unwrap ( ) ;
2136
sqlx:: query( " UPDATE flow_jobs SET receipt_attest = $1 WHERE id = $2" )
2137
.bind ( " ab" .repeat ( 32 ) )
2138
.bind ( lint.id)
2139
.execute ( & h.app.db)
2140
.await
2141
.unwrap ( ) ;
2142
create ( & h.app, " bob" , " game2" ) .await;
2143
let fork2 = db:: repos:: get( & h.app.db, " bob" , " game2" ) .await.unwrap ( ) .unwrap ( ) ;
2144
db:: repos:: set_forked_from( & h.app.db, fork2.id, up.id) .await.unwrap ( ) ;
2145
sqlx:: query( " UPDATE repositories SET flow = 'on' WHERE id = $1" ) .bind ( fork2.id) .execute ( & h.app.db) .await.unwrap ( ) ;
2146
c.config.set ( " remote.origin.url" , & format! ( " {} /bob/game2" , h.base) ) .unwrap ( ) ;
2147
c.save_config ( ) .unwrap ( ) ;
2148
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2149
let jobs2 = db:: flow:: jobs( & h.app.db, runs_of ( & h.app, " bob" , " game2" ) .await.remove ( 0 ) .id) .await.unwrap ( ) ;
2150
let status = | name : & str | jobs2.iter ( ) .find ( | j | j.name = = name ) .map ( | j | ( j.status.clone ( ) , j.reused_repo ( ) .map ( String :: from) ) ) .unwrap ( ) ;
2151
assert_eq! ( status ( " lint" ) , ( " queued" .to_string ( ) , None ) , " {jobs2:?}" ) ;
2152
assert_eq! ( status ( " build" ) , ( " passed" .to_string ( ) , Some ( " andy/game" .to_string ( ) ) ) , " {jobs2:?}" ) ;
2153
sqlx:: query( " UPDATE flow_jobs SET receipt_attest = $1 WHERE id = $2" )
2154
.bind ( & lint.receipt_attest)
2155
.bind ( lint.id)
2156
.execute ( & h.app.db)
2157
.await
2158
.unwrap ( ) ;
42cd46c9 Andy McCall 9 days ago ↶
2159
91c306aa Andy McCall 8 days ago ↶
2160
2161
2162
for who in [ " carol" ] {
2163
db:: accounts:: create_account( & h.app.db, who, & format! ( " {who} @example.com" ) , " x" , " salt" , false ) .await.unwrap ( ) ;
2164
}
2165
let push_to = | owner : & 'static str , name : & 'static str | {
2166
let ( app, base) = ( h.app.clone ( ) , h.base.clone ( ) ) ;
2167
let path = work.path ( ) .to_path_buf ( ) ;
2168
async move {
2169
create ( & app, owner, name) .await;
2170
let r = db:: repos:: get( & app.db, owner, name) .await.unwrap ( ) .unwrap ( ) ;
2171
sqlx:: query( " UPDATE repositories SET flow = 'on' WHERE id = $1" ) .bind ( r.id) .execute ( & app.db) .await.unwrap ( ) ;
2172
let mut c = Repo:: open( & path) .unwrap ( ) ;
2173
c.config.set ( " remote.origin.url" , & format! ( " {base} /{owner} /{name} " ) ) .unwrap ( ) ;
2174
c.save_config ( ) .unwrap ( ) ;
2175
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2176
let run = runs_of ( & app, owner, name) .await.remove ( 0 ) ;
2177
db:: flow:: jobs( & app.db, run.id) .await.unwrap ( )
2178
}
2179
} ;
2180
let jobs = push_to ( " carol" , " tetris-clone" ) .await;
2181
assert! ( jobs.iter ( ) .all ( | j | j.status = = " passed" & & j.reused_repo ( ) .is_some ( ) ) , " {jobs:?}" ) ;
2182
2183
2184
2185
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE owner IN ('andy', 'bob')" ) .execute ( & h.app.db) .await.unwrap ( ) ;
2186
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE owner = 'carol'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
2187
sqlx:: query( " DELETE FROM flow_jobs WHERE run_id IN (SELECT id FROM flow_runs WHERE repo_id IN (SELECT id FROM repositories WHERE owner = 'carol'))" )
2188
.execute ( & h.app.db)
2189
.await
2190
.unwrap ( ) ;
2191
let jobs = push_to ( " carol" , " another" ) .await;
42cd46c9 Andy McCall 9 days ago ↶
2192
assert! ( jobs.iter ( ) .all ( | j | j.status = = " queued" & & j.reused_from ( ) .is_none ( ) ) , " {jobs:?}" ) ;
2193
}
2194
367f34b8 Andy McCall 15 days ago ↶
2195
# [ test ]
2196
fn skip_markers_are_the_forges_spellings_in_any_case ( ) {
2197
use oxygenhub:: flow:: skip_marker;
2198
assert_eq! ( skip_marker ( " Fix typo [skip flow]" ) , Some ( " [skip flow]" ) ) ;
2199
assert_eq! ( skip_marker ( " [CI SKIP] docs" ) , Some ( " [ci skip]" ) ) ;
2200
assert_eq! ( skip_marker ( " Docs\n \n [Skip CI]" ) , Some ( " [skip ci]" ) ) ;
2201
assert_eq! ( skip_marker ( " skip ci without brackets" ) , None ) ;
3b3a51af Andy McCall 15 days ago ↶
2202
2203
assert_eq! ( skip_marker ( " Document the `[skip flow]` marker" ) , None ) ;
2204
assert_eq! ( skip_marker ( " Example:\n \n ```\n git commit -m 'x [skip ci]'\n ```\n " ) , None ) ;
2205
assert_eq! ( skip_marker ( " `code` then [skip ci]" ) , Some ( " [skip ci]" ) ) ;
2206
assert_eq! ( skip_marker ( " an odd ` backtick [skip ci]" ) , Some ( " [skip ci]" ) ) ;
367f34b8 Andy McCall 15 days ago ↶
2207
}
2208
745d225d Andy McCall 1 month ago ↶
2209
2210
2211
2212
# [ tokio ::test ( flavor = " multi_thread" ) ]
2213
async fn runs_are_cancelled_by_hand_or_by_a_newer_push ( ) {
2214
let h = hub ( ) .await;
2215
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2216
create ( & h.app, " andy" , " stop" ) .await;
2217
let url = format! ( " {} /andy/stop" , h.base) ;
2218
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2219
let c = client ( & url, work.path ( ) ) ;
2220
let extra = " job \" dist\" {\n sandbox \" alpine\" \n scope \" Cargo.toml\" \n exec \" true\" \n }\n " ;
2221
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
2222
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2223
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.0\" \n " ) ;
2224
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
2225
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2226
let wait_runs = | app : Shared, n : usize | async move {
2227
for _ in 0 .. 100 {
2228
let runs = runs_of ( & app, " andy" , " stop" ) .await;
2229
if runs.len ( ) > = n {
2230
return runs;
2231
}
2232
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2233
}
2234
panic! ( " no run {n}" ) ;
2235
} ;
2236
let first = wait_runs ( h.app.clone ( ) , 1 ) .await;
3b1bc27f Andy McCall 20 days ago
2237
sqlx:: query( " UPDATE flow_runs SET status = 'passed' WHERE id = $1" ) .bind ( first[ 0 ] .id) .execute ( & h.app.db) .await.unwrap ( ) ;
2238
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE run_id = $1" ) .bind ( first[ 0 ] .id) .execute ( & h.app.db) .await.unwrap ( ) ;
745d225d Andy McCall 1 month ago ↶
2239
2240
2241
write ( work.path ( ) , " Cargo.toml" , " version = \" 0.1.1\" \n " ) ;
2242
c.commit ( " Release 0.1.1" , & [ ] ) .unwrap ( ) ;
2243
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2244
wait_runs ( h.app.clone ( ) , 2 ) .await;
2245
let job = claim ( & h.app) .await.expect ( " a job of run 2" ) ;
2246
let id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2247
write ( work.path ( ) , " test.sh" , " echo still ok\n " ) ;
2248
c.commit ( " Edit the test" , & [ ] ) .unwrap ( ) ;
2249
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2250
let runs = wait_runs ( h.app.clone ( ) , 3 ) .await;
2251
let third = & runs[ 0 ] ;
2252
let mut second = runs[ 1 ] .clone ( ) ;
2253
for _ in 0 .. 100 {
2254
second = db:: flow:: run_by_id( & h.app.db, runs[ 1 ] .id) .await.unwrap ( ) .unwrap ( ) ;
2255
if ! second.live ( ) {
2256
break ;
2257
}
2258
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2259
}
2260
assert_eq! ( second.status, " cancelled" , " a newer push supersedes a run still going" ) ;
2261
let jobs = db:: flow:: jobs( & h.app.db, second.id) .await.unwrap ( ) ;
2262
assert! ( jobs.iter ( ) .all ( | j | j.status = = " cancelled" & & j.error = = " superseded by run #3" ) , " {jobs:?}" ) ;
2263
let mut names: Vec< String> = db:: flow:: jobs( & h.app.db, third.id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
2264
names.sort ( ) ;
2265
assert_eq! ( names, [ " build" , " dist" , " lint" , " test" ] , " run 3 also checks what run 2 would have: the version bump" ) ;
2266
let mut checks = vec! [ ] ;
2267
for _ in 0 .. 100 {
2268
checks = db:: hooks:: checks_for_state( & h.app.db, second.repo_id, & second.state) .await.unwrap ( ) ;
2269
if checks.iter ( ) .all ( | c | c.status = = " cancelled" ) {
2270
break ;
2271
}
2272
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2273
}
2274
assert! ( checks.iter ( ) .all ( | c | c.status = = " cancelled" ) , " {checks:?}" ) ;
2275
2276
let ( st, _ ) =
2277
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( r # "{"records":[]}"# .into ( ) ) ) .await;
2278
assert_eq! ( st, StatusCode:: CONFLICT ) ;
2279
2280
2281
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , Some ( " site" ) , None ) .await;
2282
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2283
let third = db:: flow:: run_by_id( & h.app.db, third.id) .await.unwrap ( ) .unwrap ( ) ;
2284
assert_eq! ( third.status, " cancelled" ) ;
2285
assert! ( db:: flow:: jobs( & h.app.db, third.id) .await.unwrap ( ) .iter ( ) .all ( | j | j.error = = " cancelled by andy" ) ) ;
2286
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , Some ( " site" ) , None ) .await;
2287
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " already cancelled" ) , " {st} {body}" ) ;
2288
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/stop/flow/runs/3/cancel" , None , None ) .await;
2289
assert_ne! ( st, StatusCode:: OK , " anonymous callers cannot cancel" ) ;
2290
let ( _ , page) = call ( & h.app, " GET" , " /andy/stop/flow/3" , None , None ) .await;
2291
assert! ( page.contains ( " ⊘" ) & & ! page.contains ( " CANCEL</button>" ) , " a cancelled run shows so, with no button" ) ;
2292
}
2293
5dead1f4 Andy McCall 1 month ago ↶
2294
2295
2296
2297
# [ tokio ::test ( flavor = " multi_thread" ) ]
2298
async fn a_job_runs_under_its_own_time_limit_within_the_sites ( ) {
2299
let h = hub ( ) .await;
2300
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2301
db:: accounts:: set_setting( & h.app.db, oxygenhub:: flow:: SETTING_DEFAULT , " 30" ) .await.unwrap ( ) ;
2302
db:: accounts:: set_setting( & h.app.db, oxygenhub:: flow:: SETTING_MAX , " 120" ) .await.unwrap ( ) ;
2303
create ( & h.app, " andy" , " limits" ) .await;
2304
let url = format! ( " {} /andy/limits" , h.base) ;
2305
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2306
let c = client ( & url, work.path ( ) ) ;
2307
let extra = " job \" slow\" {\n sandbox \" alpine\" \n timeout \" 90m\" \n exec \" true\" \n }\n job \" huge\" {\n sandbox \" alpine\" \n timeout \" 5h\" \n exec \" true\" \n }\n " ;
2308
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
2309
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2310
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2311
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2312
assert_eq! ( runs_of ( & h.app, " andy" , " limits" ) .await.len ( ) , 1 ) ;
2313
let mut limits = std:: collections:: HashMap:: new( ) ;
2314
let mut ids = std:: collections:: HashMap:: new( ) ;
2315
while let Some ( j) = claim ( & h.app) .await {
2316
let name = j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2317
limits.insert ( name.clone ( ) , j[ " timeout_s" ] .as_u64 ( ) .unwrap ( ) ) ;
2318
ids.insert ( name, j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
2319
}
2320
assert_eq! ( limits[ " slow" ] , 5400 , " its own" ) ;
2321
assert_eq! ( limits[ " huge" ] , 7200 , " cut to the site's maximum" ) ;
2322
assert_eq! ( limits[ " build" ] , 1800 , " the site's default" ) ;
2323
2324
let started = db:: now_ms( ) - 40 * 60 * 1000 ;
3b1bc27f Andy McCall 20 days ago
2325
sqlx:: query( " UPDATE flow_jobs SET started_ms = $1" ) .bind ( started) .execute ( & h.app.db) .await.unwrap ( ) ;
5dead1f4 Andy McCall 1 month ago ↶
2326
oxygenhub:: flow:: reap_lost_jobs( & h.app) .await.unwrap ( ) ;
2327
let status = | id : i64 | {
2328
let app = h.app.clone ( ) ;
2329
async move { db:: flow:: job( & app.db, id) .await.unwrap ( ) .unwrap ( ) .status }
2330
} ;
2331
assert_eq! ( status ( ids[ " build" ] ) .await, " error" , " past its 30 minutes" ) ;
2332
assert_eq! ( status ( ids[ " slow" ] ) .await, " running" , " within its 90" ) ;
2333
assert_eq! ( status ( ids[ " huge" ] ) .await, " running" , " within its 120" ) ;
2334
}
c889911b Andy McCall 1 month ago ↶
2335
2336
2337
2338
2339
# [ tokio ::test ( flavor = " multi_thread" ) ]
2340
async fn on_decides_which_events_start_a_job ( ) {
2341
let h = hub ( ) .await;
2342
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2343
create ( & h.app, " andy" , " events" ) .await;
2344
let url = format! ( " {} /andy/events" , h.base) ;
2345
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2346
let c = client ( & url, work.path ( ) ) ;
2347
let text = format! (
2348
" on {{ \n push \" main\" \n manual\n }} \n {} job \" dist\" {{ \n sandbox \" alpine\" \n on {{ \n tag \" v*\" \n }} \n exec \" true\" \n }} \n " ,
2349
pipeline ( " " )
2350
) ;
2351
2352
let text = text.replacen (
2353
" on {\n push \" main\" \n manual\n }\n schema-version 1\n " ,
2354
" schema-version 1\n on {\n push \" main\" \n manual\n }\n " ,
2355
1 ,
2356
) ;
2357
write ( work.path ( ) , " .flow/pipeline.kdl" , & text) ;
2358
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2359
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
2360
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2361
let jobs_of_latest = | app : Shared, n : usize | async move {
2362
let mut runs = Vec :: new( ) ;
2363
for _ in 0 .. 100 {
2364
runs = runs_of ( & app, " andy" , " events" ) .await;
2365
if runs.len ( ) > = n {
2366
break ;
2367
}
2368
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2369
}
2370
assert_eq! ( runs.len ( ) , n, " runs" ) ;
2371
let mut j: Vec< String> = db:: flow:: jobs( & app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
2372
j.sort ( ) ;
2373
( runs[ 0 ] .clone ( ) , j)
2374
} ;
2375
let ( _ , j) = jobs_of_latest ( h.app.clone ( ) , 1 ) .await;
2376
assert_eq! ( j, [ " build" , " lint" , " test" ] , " a push to main: no dist" ) ;
2377
2378
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
2379
c.tag_create ( " v1.0.0" , & heads, " first" ) .unwrap ( ) ;
2380
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2381
let ( run, j) = jobs_of_latest ( h.app.clone ( ) , 2 ) .await;
2382
assert_eq! ( j, [ " dist" ] , " a tag runs only what names it" ) ;
2383
assert_eq! ( run.trigger, " tag:v1.0.0" ) ;
2384
2385
let row = db:: repos:: get( & h.app.db, " andy" , " events" ) .await.unwrap ( ) .unwrap ( ) ;
2386
db:: flow:: finish(
2387
& h.app.db,
2388
db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) [ 0 ] .id,
2389
db:: flow:: Outcome { status: " passed" , exit_code: Some ( 0 ) , error: " " , receipt: " " , receipt_hash: " " } ,
2390
)
2391
.await
2392
.unwrap ( ) ;
2393
let run = db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) ;
2394
oxygenhub:: flow:: rerun( & h.app, & row, & run, " andy" ) .await.unwrap ( ) ;
2395
let ( _ , j) = jobs_of_latest ( h.app.clone ( ) , 3 ) .await;
2396
assert_eq! ( j, [ " dist" ] , " the rerun is the tag's run again" ) ;
2397
2398
c.bookmark_create ( " develop" , None ) .unwrap ( ) ;
2399
assert! ( c.push_bookmark ( " origin" , " develop" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2400
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
2401
assert_eq! ( runs_of ( & h.app, " andy" , " events" ) .await.len ( ) , 3 , " no run for develop" ) ;
348f8c4b Andy McCall 1 month ago ↶
2402
}
2403
2404
2405
2406
# [ tokio ::test ( flavor = " multi_thread" ) ]
2407
async fn scheduled_jobs_run_when_due ( ) {
2408
let h = hub ( ) .await;
2409
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2410
create ( & h.app, " andy" , " nightly" ) .await;
2411
let url = format! ( " {} /andy/nightly" , h.base) ;
2412
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2413
let c = client ( & url, work.path ( ) ) ;
2414
2415
let extra = " job \" nightly\" {\n sandbox \" alpine\" \n on {\n schedule \" 17 4 29 2 *\" \n }\n exec \" true\" \n }\n " ;
2416
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
2417
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2418
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
2419
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2420
assert_eq! ( runs_of ( & h.app, " andy" , " nightly" ) .await.len ( ) , 1 ) ;
2421
let first = runs_of ( & h.app, " andy" , " nightly" ) .await;
2422
let names: Vec< String> = db:: flow:: jobs( & h.app.db, first[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
2423
assert! ( ! names.contains ( & " nightly" .to_string ( ) ) , " a push does not run a scheduled job" ) ;
2424
use flow_core:: pipeline:: When;
2425
let not_yet = When { minute: 16 , hour: 4 , day: 29 , month: 2 , weekday: 4 } ;
2426
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, not_yet) .await, 0 ) ;
2427
let due = When { minute: 17 , .. not_yet } ;
2428
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, due) .await, 1 ) ;
2429
let runs = runs_of ( & h.app, " andy" , " nightly" ) .await;
2430
assert_eq! ( runs.len ( ) , 2 ) ;
2431
assert_eq! ( runs[ 0 ] .trigger, " schedule" ) ;
2432
let names: Vec< String> = db:: flow:: jobs( & h.app.db, runs[ 0 ] .id) .await.unwrap ( ) .into_iter ( ) .map ( | j | j.name ) .collect ( ) ;
2433
assert_eq! ( names, [ " nightly" ] ) ;
2434
assert_eq! ( runs[ 0 ] .bookmark, " main" , " on the default bookmark" ) ;
5f3fa151 Andy McCall 10 days ago ↶
2435
2436
2437
write ( work.path ( ) , " .flow/pipeline.kdl" , " schema-version 1\n " ) ;
2438
c.commit ( " No jobs" , & [ ] ) .unwrap ( ) ;
2439
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
f87a74ff Andy McCall 4 days ago ↶
2440
2441
2442
let mut runs = runs_of ( & h.app, " andy" , " nightly" ) .await;
2443
for _ in 0 .. 150 {
2444
if runs.len ( ) = = 3 {
2445
break ;
2446
}
2447
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2448
runs = runs_of ( & h.app, " andy" , " nightly" ) .await;
2449
}
5f3fa151 Andy McCall 10 days ago ↶
2450
assert_eq! ( runs.len ( ) , 3 ) ;
2451
assert! ( runs[ 0 ] .error.contains ( " at least one job" ) , " {}" , runs[ 0 ] .error) ;
2452
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, due) .await, 0 ) ;
2453
assert_eq! ( oxygenhub:: flow:: run_due( & h.app, not_yet) .await, 0 ) ;
2454
assert_eq! ( runs_of ( & h.app, " andy" , " nightly" ) .await.len ( ) , 3 , " no scheduled runs of a broken pipeline" ) ;
c889911b Andy McCall 1 month ago ↶
2455
}
2a83f530 Andy McCall 1 month ago ↶
2456
2457
# [ tokio ::test ( flavor = " multi_thread" ) ]
2458
async fn variables_from_three_levels_reach_a_job_inner_overriding_outer ( ) {
2459
use oxygenhub:: levels:: Scope;
2460
use oxygenhub:: variables;
2461
let h = hub ( ) .await;
2462
let app = & h.app;
2463
2464
let res = oxygenhub:: router( app.clone ( ) )
2465
.oneshot (
2466
Request:: post( " /register" )
2467
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
c8684d44 Andy McCall 19 days ago
2468
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
2a83f530 Andy McCall 1 month ago ↶
2469
.unwrap ( ) ,
2470
)
2471
.await
2472
.unwrap ( ) ;
2473
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
2474
let andy = db:: accounts:: account_by_name( & app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
2475
db:: accounts:: create_org( & app.db, " acme" , " Acme" , andy.id) .await.unwrap ( ) ;
2476
create ( app, " acme" , " app" ) .await;
2477
let page = | path : & str , form : Option< String> | {
2478
let mut req =
2479
Request:: builder( ) .method ( if form.is_some ( ) { " POST" } else { " GET" } ) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
2480
if form.is_some ( ) {
2481
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
2482
}
2483
let req = req.body ( Body:: from( form.unwrap_or_default ( ) ) ) .unwrap ( ) ;
c7067c7b Andy McCall 16 days ago
2484
let ( app, cookie) = ( app.clone ( ) , cookie.clone ( ) ) ;
2a83f530 Andy McCall 1 month ago ↶
2485
async move {
c7067c7b Andy McCall 16 days ago
2486
let mut res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
2487
2488
let back =
2489
res.headers ( ) .get ( header:: LOCATION ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .filter ( | l | l.contains ( " /settings/" ) ) .map ( String :: from) ;
2490
if let Some ( to) = back {
2491
let req = Request:: get( to) .header ( header:: COOKIE , cookie) .body ( Body:: empty( ) ) .unwrap ( ) ;
2492
res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
2493
}
2a83f530 Andy McCall 1 month ago ↶
2494
let status = res.status ( ) ;
2495
let body = String :: from_utf8( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ) .unwrap ( ) ;
2496
( status, body)
2497
}
2498
} ;
2499
let csrf_of = | page : & str | {
2500
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
2501
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
2502
} ;
2503
2504
2505
2506
variables:: set( & app.db, Scope:: Site, " CC" , " gcc" ) .await.unwrap ( ) ;
2507
variables:: set( & app.db, Scope:: Site, " LANG" , " C" ) .await.unwrap ( ) ;
2508
let org = db:: accounts:: org_by_name( & app.db, " acme" ) .await.unwrap ( ) .unwrap ( ) ;
2509
variables:: set( & app.db, Scope:: Org( org.id) , " CC" , " clang" ) .await.unwrap ( ) ;
c7067c7b Andy McCall 16 days ago
2510
let ( st, settings) = page ( " /acme/app/settings/variables" , None ) .await;
2a83f530 Andy McCall 1 month ago ↶
2511
assert_eq! ( st, StatusCode:: OK , " {settings}" ) ;
2512
assert! ( settings.contains ( " Inherited" ) & & settings.contains ( " the acme organisation" ) & & settings.contains ( " overridden" ) , " {settings}" ) ;
2513
let csrf = csrf_of ( & settings) ;
2514
let ( _ , body) = page ( " /acme/app/settings/variables" , Some ( format! ( " _csrf={csrf} &action=set&name=RUST_LOG&value=debug" ) ) ) .await;
2515
assert! ( body.contains ( " RUST_LOG set." ) & & body.contains ( " <code>debug</code>" ) , " {body}" ) ;
2516
let ( _ , body) = page ( " /acme/app/settings/variables" , Some ( format! ( " _csrf={csrf} &action=set&name=FLOW_REPO&value=x" ) ) ) .await;
2517
assert! ( body.contains ( " not FLOW_" ) , " {body}" ) ;
2518
let repo = db:: repos:: get( & app.db, " acme" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
2519
db:: repos:: set_flow( & app.db, repo.id, " on" ) .await.unwrap ( ) ;
2520
let env = variables:: env_for_repo( & app.db, & repo) .await.unwrap ( ) ;
2521
assert_eq! (
2522
env.iter ( ) .map ( | ( k , v ) | ( k.as_str ( ) , v.as_str ( ) ) ) .collect:: < Vec< _ > > ( ) ,
2523
[ ( " CC" , " clang" ) , ( " LANG" , " C" ) , ( " RUST_LOG" , " debug" ) ] ,
2524
" the organisation's CC wins over the site's"
2525
) ;
2526
2527
2528
let url = format! ( " {} /acme/app" , h.base) ;
2529
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2530
let c = client ( & url, work.path ( ) ) ;
2531
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2532
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2533
c.snapshot ( " test" ) .unwrap ( ) ;
2534
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2535
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2536
let runs = runs_of ( app, " acme" , " app" ) .await;
2537
assert_eq! ( runs.len ( ) , 1 , " {runs:?}" ) ;
2538
let got = claim ( app) .await.unwrap_or_else ( | | panic! ( " a job: {:?}" , runs[ 0 ] ) ) ;
2539
assert_eq! ( got[ " env" ] , serde_json:: json! ( { " CC" : " clang" , " LANG" : " C" , " RUST_LOG" : " debug" } ) , " {got}" ) ;
2540
2541
737de0fa Andy McCall 15 days ago ↶
2542
let ( _ , admin) = page ( " /admin/variables" , None ) .await;
2a83f530 Andy McCall 1 month ago ↶
2543
assert! ( admin.contains ( " Variables" ) & & admin.contains ( " <code>gcc</code>" ) , " {admin}" ) ;
2544
let ( _ , body) = page ( " /admin/variables" , Some ( format! ( " _csrf={} &action=delete&name=LANG" , csrf_of ( & admin) ) ) ) .await;
2545
assert! ( body.contains ( " LANG removed." ) , " {body}" ) ;
2546
let got = claim ( app) .await.expect ( " another job" ) ;
2547
assert_eq! ( got[ " env" ] , serde_json:: json! ( { " CC" : " clang" , " RUST_LOG" : " debug" } ) , " {got}" ) ;
2548
}
4ac01a73 Andy McCall 1 month ago ↶
2549
2550
# [ tokio ::test ( flavor = " multi_thread" ) ]
2551
async fn secrets_a_job_declares_come_with_the_claim_and_never_reach_the_log ( ) {
2552
let h = hub ( ) .await;
2553
let app = & h.app;
2554
let res = oxygenhub:: router( app.clone ( ) )
2555
.oneshot (
2556
Request:: post( " /register" )
2557
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
c8684d44 Andy McCall 19 days ago
2558
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
4ac01a73 Andy McCall 1 month ago ↶
2559
.unwrap ( ) ,
2560
)
2561
.await
2562
.unwrap ( ) ;
2563
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
2564
let page = | path : & str , form : Option< String> | {
2565
let mut req =
2566
Request:: builder( ) .method ( if form.is_some ( ) { " POST" } else { " GET" } ) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
2567
if form.is_some ( ) {
2568
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
2569
}
2570
let req = req.body ( Body:: from( form.unwrap_or_default ( ) ) ) .unwrap ( ) ;
c7067c7b Andy McCall 16 days ago
2571
let ( app, cookie) = ( app.clone ( ) , cookie.clone ( ) ) ;
4ac01a73 Andy McCall 1 month ago ↶
2572
async move {
c7067c7b Andy McCall 16 days ago
2573
let mut res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
2574
2575
let back =
2576
res.headers ( ) .get ( header:: LOCATION ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .filter ( | l | l.contains ( " /settings/" ) ) .map ( String :: from) ;
2577
if let Some ( to) = back {
2578
let req = Request:: get( to) .header ( header:: COOKIE , cookie) .body ( Body:: empty( ) ) .unwrap ( ) ;
2579
res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
2580
}
4ac01a73 Andy McCall 1 month ago ↶
2581
let status = res.status ( ) ;
2582
let body = String :: from_utf8( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ) .unwrap ( ) ;
2583
( status, body)
2584
}
2585
} ;
2586
let csrf_of = | page : & str | {
2587
let i = page.find ( " name=\" _csrf\" value=\" " ) .expect ( " csrf field" ) + " name=\" _csrf\" value=\" " .len ( ) ;
2588
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
2589
} ;
2590
create ( app, " andy" , " demo" ) .await;
2591
2592
2593
let ( _ , admin) = page ( " /admin" , None ) .await;
2594
let csrf = csrf_of ( & admin) ;
2595
let ( _ , body) = page ( " /admin/secrets" , Some ( format! ( " _csrf={csrf} &action=set&name=deploy-token&value=site-level-value" ) ) ) .await;
2596
assert! ( body.contains ( " deploy-token set." ) & & ! body.contains ( " site-level-value" ) , " {body}" ) ;
2597
let ( _ , body) = page ( " /admin/secrets" , Some ( format! ( " _csrf={csrf} &action=set&name=bad+name&value=x" ) ) ) .await;
2598
assert! ( body.contains ( " name is 1 to 64 of" ) , " {body}" ) ;
c7067c7b Andy McCall 16 days ago
2599
let ( _ , settings) = page ( " /andy/demo/settings/secrets" , None ) .await;
4ac01a73 Andy McCall 1 month ago ↶
2600
assert! (
2601
settings.contains ( " Secrets" ) & & settings.contains ( " <code>deploy-token</code>" ) & & settings.contains ( " the site</td>" ) ,
2602
" {settings}"
2603
) ;
2604
let csrf_r = csrf_of ( & settings) ;
2605
let ( _ , body) =
2606
page ( " /andy/demo/settings/secrets" , Some ( format! ( " _csrf={csrf_r} &action=set&name=deploy-token&value=hunter22-repo-value" ) ) ) .await;
2607
assert! ( body.contains ( " deploy-token set." ) & & body.contains ( " overridden" ) & & ! body.contains ( " hunter22" ) , " {body}" ) ;
2608
let repo = db:: repos:: get( & app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
2609
assert! (
2610
! String :: from_utf8_lossy( & oxygenhub:: secrets:: list( & app.db, oxygenhub:: levels:: Scope:: Repo( repo.id) ) .await.unwrap ( ) [ 0 ] .value)
2611
.contains ( " hunter22" ) ,
2612
" sealed at rest"
2613
) ;
2614
2615
2616
let url = format! ( " {} /andy/demo" , h.base) ;
2617
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2618
let c = client ( & url, work.path ( ) ) ;
2619
write (
2620
work.path ( ) ,
2621
" .flow/pipeline.kdl" ,
2622
& pipeline ( " job \" deploy\" {\n sandbox \" alpine\" \n secret \" deploy-token\" {\n mount-path \" /run/secrets/token\" \n }\n secret \" missing\" {\n mount-path \" /run/secrets/missing\" \n }\n exec \" deploy\" \n }\n " ) ,
2623
) ;
2624
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2625
c.snapshot ( " test" ) .unwrap ( ) ;
2626
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2627
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2628
let runs = runs_of ( app, " andy" , " demo" ) .await;
2629
let jobs = match runs.first ( ) {
2630
Some ( r) => db:: flow:: jobs( & app.db, r.id) .await.unwrap ( ) .iter ( ) .map ( | j | format! ( " {} :{} " , j.name, j.status) ) .collect:: < Vec< _ > > ( ) ,
2631
None => vec! [ ] ,
2632
} ;
2633
let mut deploy = None ;
2634
for _ in 0 .. 50 {
2635
let Some ( got) = claim ( app) .await else {
2636
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2637
continue ;
2638
} ;
2639
if got[ " job" ] [ " name" ] = = " deploy" {
2640
deploy = Some ( got) ;
2641
break ;
2642
}
2643
assert_eq! ( got[ " secrets" ] , serde_json:: Value:: Null, " {got}" ) ;
2644
}
2645
let got = deploy.unwrap_or_else ( | | panic! ( " the deploy job: runs {runs:?} jobs {jobs:?}" ) ) ;
2646
assert_eq! (
2647
got[ " secrets" ] ,
2648
serde_json:: json! ( [ { " name" : " deploy-token" , " path" : " /run/secrets/token" , " value" : " hunter22-repo-value" } ] ) ,
2649
" the repository's value, the missing one left out: {got}"
2650
) ;
2651
let id = got[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2652
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2653
assert! ( log.iter ( ) .any ( | l | l.stream = = " flow" & & l.text.contains ( " secret \" missing\" is not set" ) ) , " {log:?}" ) ;
2654
2655
2656
let records = serde_json:: json! ( { " records" : [
2657
{ " t" : 1 , " stream" : " out" , " text" : " token=hunter22-repo-value" } ,
2658
{ " t" : 2 , " stream" : " err" , " text" : " b64 aHVudGVyMjItcmVwby12YWx1ZQ== end" } ,
2659
] } ) ;
2660
let ( st, _ ) = call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( records.to_string ( ) ) ) .await;
2661
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2662
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2663
let texts: Vec< & str > = log.iter ( ) .map ( | l | l.text.as_str ( ) ) .collect ( ) ;
2664
assert! ( texts.contains ( & " token=***" ) & & texts.contains ( & " b64 *** end" ) , " {texts:?}" ) ;
2665
assert! ( ! texts.iter ( ) .any ( | t | t.contains ( " hunter22" ) ) , " {texts:?}" ) ;
2666
}
d1727c57 Andy McCall 1 month ago ↶
2667
2668
# [ tokio ::test ( flavor = " multi_thread" ) ]
2669
async fn a_job_s_identity_is_a_signed_token_the_discovery_document_lets_anyone_verify ( ) {
2670
let h = hub ( ) .await;
2671
let app = & h.app;
2672
let ( st, disc) = call ( app, " GET" , " /.well-known/openid-configuration" , None , None ) .await;
2673
assert_eq! ( st, StatusCode:: OK , " {disc}" ) ;
2674
let disc: serde_json:: Value = serde_json:: from_str( & disc) .unwrap ( ) ;
2675
assert_eq! ( disc[ " issuer" ] , h.base.as_str ( ) ) ;
2676
assert_eq! ( disc[ " jwks_uri" ] , format! ( " {} /.well-known/jwks.json" , h.base) ) ;
2677
let ( st, jwks) = call ( app, " GET" , " /.well-known/jwks.json" , None , None ) .await;
2678
assert_eq! ( st, StatusCode:: OK , " {jwks}" ) ;
2679
let jwks: serde_json:: Value = serde_json:: from_str( & jwks) .unwrap ( ) ;
2680
assert_eq! ( jwks[ " keys" ] [ 0 ] [ " alg" ] , " RS256" ) ;
2681
let kid = jwks[ " keys" ] [ 0 ] [ " kid" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2682
2683
create ( app, " andy" , " demo" ) .await;
2684
let url = format! ( " {} /andy/demo" , h.base) ;
2685
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2686
let c = client ( & url, work.path ( ) ) ;
2687
write (
2688
work.path ( ) ,
2689
" .flow/pipeline.kdl" ,
2690
& pipeline ( " job \" deploy\" {\n sandbox \" alpine\" \n identity \" sts.amazonaws.com\" {\n mount-path \" /run/secrets/aws-token\" \n }\n exec \" deploy\" \n }\n " ) ,
2691
) ;
2692
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2693
c.snapshot ( " test" ) .unwrap ( ) ;
2694
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2695
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2696
let mut deploy = None ;
2697
for _ in 0 .. 50 {
2698
let Some ( got) = claim ( app) .await else {
2699
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2700
continue ;
2701
} ;
2702
if got[ " job" ] [ " name" ] = = " deploy" {
2703
deploy = Some ( got) ;
2704
break ;
2705
}
2706
}
2707
let got = deploy.expect ( " the deploy job" ) ;
2708
let secrets = got[ " secrets" ] .as_array ( ) .expect ( " secrets" ) ;
2709
assert_eq! ( secrets.len ( ) , 1 ) ;
2710
assert_eq! (
2711
( secrets[ 0 ] [ " name" ] .as_str ( ) , secrets[ 0 ] [ " path" ] .as_str ( ) ) ,
2712
( Some ( " identity:sts.amazonaws.com" ) , Some ( " /run/secrets/aws-token" ) )
2713
) ;
2714
let token = secrets[ 0 ] [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
2715
let issuer = app.oidc ( ) .await.unwrap ( ) ;
2716
assert_eq! ( issuer.kid, kid, " the key the JWKS publishes signed it" ) ;
2717
let claims = issuer.verify ( & token, " sts.amazonaws.com" , db:: now_ms( ) ) .expect ( " verifies" ) ;
2718
assert_eq! ( claims[ " sub" ] , " repo:andy/demo:bookmark:main" ) ;
2719
assert_eq! ( claims[ " repository" ] , " andy/demo" ) ;
2720
assert_eq! ( claims[ " job" ] , " deploy" ) ;
2721
assert_eq! ( claims[ " run_number" ] , 1 ) ;
2722
assert_eq! ( claims[ " runner" ] , " test-1" ) ;
2723
assert_eq! ( claims[ " iss" ] , h.base.as_str ( ) ) ;
2724
assert! ( claims[ " state" ] .as_str ( ) .unwrap ( ) .len ( ) = = 64 ) ;
2725
2726
let id = got[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2727
let records = serde_json:: json! ( { " records" : [ { " t" : 1 , " stream" : " out" , " text" : format! ( " TOKEN={token} end" ) } ] } ) ;
2728
let ( st, _ ) = call ( app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( records.to_string ( ) ) ) .await;
2729
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
2730
let log = db:: flow:: log( & app.db, id, 0 , 100 ) .await.unwrap ( ) ;
2731
assert! ( log.iter ( ) .any ( | l | l.text = = " TOKEN=*** end" ) , " {log:?}" ) ;
2732
}
efd29a00 Andy McCall 1 month ago ↶
2733
2734
# [ tokio ::test ( flavor = " multi_thread" ) ]
2735
async fn jobs_are_offered_only_to_runners_of_their_architecture ( ) {
2736
let h = hub ( ) .await;
2737
let app = & h.app;
2738
create ( app, " andy" , " demo" ) .await;
2739
let url = format! ( " {} /andy/demo" , h.base) ;
2740
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2741
let c = client ( & url, work.path ( ) ) ;
2742
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " sandbox \" board\" {\n rootfs \" cas://blob/{ROOTFS}\" \n arch \" riscv64\" \n }\n job \" flash\" {\n sandbox \" board\" \n exec \" flash\" \n }\n " ) .replace ( " {ROOTFS}" , & " ab" .repeat ( 32 ) ) ) ;
2743
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2744
c.snapshot ( " test" ) .unwrap ( ) ;
2745
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2746
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2747
let claim_as = | arch : & 'static str , archs : serde_json:: Value| {
2748
let app = app.clone ( ) ;
2749
async move {
2750
let body = serde_json:: json! ( {
2751
" runner" : format! ( " {arch} -1" ) , " runtimes" : [ " microvm" ] ,
2752
" info" : { " arch" : arch, " os" : " linux" , " version" : " t" , " labels" : [ ] , " max_jobs" : 1 } ,
2753
" image_archs" : archs,
2754
} ) ;
2755
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await;
2756
match st {
2757
StatusCode:: NO_CONTENT => None ,
2758
StatusCode:: OK => {
2759
Some ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) )
2760
}
2761
other => panic! ( " claim: {other} {body}" ) ,
2762
}
2763
}
2764
} ;
2765
2766
2767
let rootfs = pipeline ( " " ) .split ( " cas://blob/" ) .nth ( 1 ) .unwrap ( ) [ .. 64 ] .to_string ( ) ;
2768
for _ in 0 .. 50 {
2769
if ! runs_of ( app, " andy" , " demo" ) .await.is_empty ( ) {
2770
break ;
2771
}
2772
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
2773
}
2774
let runs = runs_of ( app, " andy" , " demo" ) .await;
2775
assert_eq! ( runs.len ( ) , 1 , " {runs:?}" ) ;
2776
let mut taken = vec! [ ] ;
2777
while let Some ( j) = claim_as ( " x86_64" , serde_json:: json! ( { rootfs.clone ( ) : " x86_64" } ) ) .await {
2778
taken.push ( j) ;
2779
}
2780
assert_eq! ( taken, [ " build" , " lint" ] , " test waits for build; flash is for riscv64" ) ;
2781
2782
assert_eq! ( claim_as ( " riscv64" , serde_json:: json! ( { } ) ) .await.as_deref ( ) , Some ( " flash" ) ) ;
2783
assert_eq! ( claim_as ( " riscv64" , serde_json:: json! ( { } ) ) .await, None ) ;
2784
let ( _ , queue) = call ( app, " GET" , " /admin/runners" , None , None ) .await;
2785
let _ = queue;
2786
}
4d2e44d7 Andy McCall 1 month ago ↶
2787
2788
2789
2790
2791
2792
# [ tokio ::test ( flavor = " multi_thread" ) ]
2793
async fn a_declined_job_goes_to_another_runner_or_ends_unplaceable ( ) {
2794
let h = hub ( ) .await;
2795
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2796
create ( & h.app, " andy" , " declines" ) .await;
2797
let url = format! ( " {} /andy/declines" , h.base) ;
2798
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2799
let c = client ( & url, work.path ( ) ) ;
2800
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
2801
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2802
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2803
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2804
let run = runs_of ( & h.app, " andy" , " declines" ) .await[ 0 ] .clone ( ) ;
2805
let claim_as = | token : & 'static str , name : & 'static str | {
2806
let app = h.app.clone ( ) ;
2807
async move {
2808
let body = serde_json:: json! ( { " runner" : name, " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
2809
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( token) , Some ( body) ) .await;
2810
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) )
2811
}
2812
} ;
2813
let decline = | token : & 'static str , id : i64 , reason : & 'static str | {
2814
let app = h.app.clone ( ) ;
2815
async move {
2816
let body = serde_json:: json! ( { " reason" : reason } ) .to_string ( ) ;
2817
call ( & app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /decline" ) , Some ( token) , Some ( body) ) .await.0
2818
}
2819
} ;
2820
let job = claim_as ( " runner-secret" , " earth-1" ) .await.expect ( " a job" ) ;
2821
2822
assert_eq! ( decline ( " runner-two" , job, " not mine" ) .await, StatusCode:: CONFLICT , " only the runner it runs on may decline it" ) ;
2823
assert_eq! ( decline ( " runner-secret" , job, " the MicroVM tier needs /dev/kvm" ) .await, StatusCode:: NO_CONTENT ) ;
2824
let j = db:: flow:: job( & h.app.db, job) .await.unwrap ( ) .unwrap ( ) ;
2825
assert_eq! ( ( j.status.as_str ( ) , j.runner.as_str ( ) ) , ( " queued" , " " ) ) ;
2826
assert_eq! ( j.waiting, " declined by earth-1: the MicroVM tier needs /dev/kvm; waiting for another runner" ) ;
2827
2828
let next = claim_as ( " runner-secret" , " earth-1" ) .await.expect ( " the other ready job" ) ;
2829
assert_ne! ( next, job) ;
2830
let page = call ( & h.app, " GET" , & format! ( " /andy/declines/flow/{} " , run.number) , None , None ) .await.1 ;
2831
assert! ( page.contains ( " Waiting: declined by earth-1: the MicroVM tier needs /dev/kvm" ) , " {page}" ) ;
2832
assert_eq! ( claim_as ( " runner-two" , " plex-1" ) .await, Some ( job) , " another runner takes it" ) ;
2833
assert_eq! ( decline ( " runner-two" , job, " rootfs 36fc4e323804 is not on this runner" ) .await, StatusCode:: NO_CONTENT ) ;
2834
assert_eq! ( claim_as ( " runner-three" , " narvi" ) .await, Some ( job) ) ;
2835
assert_eq! ( decline ( " runner-three" , job, " rootfs 36fc4e323804 is for x86_64; this runner is riscv64" ) .await, StatusCode:: NO_CONTENT ) ;
2836
let j = db:: flow:: job( & h.app.db, job) .await.unwrap ( ) .unwrap ( ) ;
2837
assert_eq! ( j.status, " error" ) ;
2838
assert_eq! (
2839
j.error,
2840
" unplaceable: declined by 3 runners: earth-1: the MicroVM tier needs /dev/kvm; plex-1: rootfs 36fc4e323804 is not on this runner; narvi: rootfs 36fc4e323804 is for x86_64; this runner is riscv64"
2841
) ;
2842
let r = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex-1" ) .unwrap ( ) ;
2843
let declined = db:: flow:: declines_of_runner( & h.app.db, r.id, 10 ) .await.unwrap ( ) ;
2844
assert_eq! ( declined.len ( ) , 1 ) ;
2845
assert_eq! ( ( declined[ 0 ] .job_id, declined[ 0 ] .reason.as_str ( ) ) , ( job, " rootfs 36fc4e323804 is not on this runner" ) ) ;
2846
assert_eq! ( r.tier_list ( ) , vec! [ " microvm" ] ) ;
2847
}
554ccacf Andy McCall 1 month ago ↶
2848
ab4df6aa Andy McCall 11 days ago ↶
2849
2850
2851
2852
2853
# [ tokio ::test ( flavor = " multi_thread" ) ]
2854
async fn a_drained_job_goes_back_ahead_of_new_work ( ) {
2855
let h = hub ( ) .await;
2856
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
2857
create ( & h.app, " andy" , " drains" ) .await;
2858
let url = format! ( " {} /andy/drains" , h.base) ;
2859
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2860
let c = client ( & url, work.path ( ) ) ;
e4203e14 Andy McCall 10 days ago ↶
2861
let bulk = " job \" bulk\" {\n sandbox \" alpine\" \n priority \" low\" \n checkpoint \" /src/state\" \n exec \" true\" \n }\n " ;
ab4df6aa Andy McCall 11 days ago ↶
2862
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( bulk) ) ;
2863
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
2864
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
2865
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2866
runs_of ( & h.app, " andy" , " drains" ) .await;
2867
let claim_as = | token : & 'static str , name : & 'static str | {
2868
let app = h.app.clone ( ) ;
2869
async move {
2870
let body = serde_json:: json! ( { " runner" : name, " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
2871
let ( st, body) = call ( & app, " POST" , " /api/v1/flow/claim" , Some ( token) , Some ( body) ) .await;
2872
( st = = StatusCode:: OK ) .then ( | | serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) )
2873
}
2874
} ;
2875
let release = | token : & 'static str , id : i64 , reason : & 'static str | {
2876
let app = h.app.clone ( ) ;
2877
async move {
2878
let body = serde_json:: json! ( { " reason" : reason } ) .to_string ( ) ;
2879
call ( & app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /release" ) , Some ( token) , Some ( body) ) .await.0
2880
}
2881
} ;
2882
let name_of = | id : i64 | {
2883
let app = h.app.clone ( ) ;
2884
async move { db:: flow:: job( & app.db, id) .await.unwrap ( ) .unwrap ( ) .name }
2885
} ;
2886
let build = claim_as ( " runner-secret" , " earth-1" ) .await.unwrap ( ) ;
2887
let lint = claim_as ( " runner-two" , " plex-1" ) .await.unwrap ( ) ;
2888
let low = claim_as ( " runner-three" , " phone" ) .await.unwrap ( ) ;
2889
assert_eq! ( ( name_of ( build) .await, name_of ( lint) .await, name_of ( low) .await) , ( " build" .into ( ) , " lint" .into ( ) , " bulk" .into ( ) ) ) ;
2890
2891
let line = serde_json:: json! ( { " records" : [ { " t" : 1200 , " stream" : " stdout" , " text" : " step 1 of 30" } ] } ) .to_string ( ) ;
2892
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{low} /log" ) , Some ( " runner-three" ) , Some ( line) ) .await;
2893
assert_eq! ( finish ( & h.app, build, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 } ) ) .await, StatusCode:: NO_CONTENT ) ;
e4203e14 Andy McCall 10 days ago ↶
2894
2895
let ( st, body) = call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{lint} /checkpoint" ) , Some ( " runner-two" ) , Some ( " x" .into ( ) ) ) .await;
2896
assert_eq! ( st, StatusCode:: BAD_REQUEST , " lint declares no checkpoint: {body}" ) ;
2897
let ( st, body) =
2898
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{low} /checkpoint" ) , Some ( " runner-three" ) , Some ( " step 32" .into ( ) ) ) .await;
2899
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2900
let saved = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
ab4df6aa Andy McCall 11 days ago ↶
2901
assert_eq! ( release ( " runner-two" , low, " off Wi-Fi" ) .await, StatusCode:: CONFLICT , " only the runner that holds it" ) ;
2902
assert_eq! ( release ( " runner-three" , low, " off Wi-Fi" ) .await, StatusCode:: NO_CONTENT ) ;
2903
let j = db:: flow:: job( & h.app.db, low) .await.unwrap ( ) .unwrap ( ) ;
2904
assert_eq! ( ( j.status.as_str ( ) , j.runner.as_str ( ) , j.handbacks) , ( " queued" , " " , 1 ) ) ;
2905
let log: Vec< String> = db:: flow:: log( & h.app.db, low, 0 , 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.text ) .collect ( ) ;
2906
assert_eq! ( log, [ " step 1 of 30" , " ── handed back by phone: off Wi-Fi; queued again ahead of new work" ] ) ;
2907
assert! ( db:: flow:: declines_of_job( & h.app.db, low) .await.unwrap ( ) .is_empty ( ) , " not a decline" ) ;
2908
2909
e4203e14 Andy McCall 10 days ago ↶
2910
let body = serde_json:: json! ( { " runner" : " phone" , " runtimes" : [ " microvm" ] } ) .to_string ( ) ;
2911
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-three" ) , Some ( body) ) .await;
2912
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
2913
let again: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
2914
assert_eq! ( ( again[ " job_id" ] .as_i64 ( ) , again[ " checkpoint" ] .as_str ( ) ) , ( Some ( low) , Some ( saved.as_str ( ) ) ) , " it resumes: {again}" ) ;
2915
let ( st, bytes) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{low} /checkpoint" ) , Some ( " runner-three" ) , None ) .await;
2916
assert_eq! ( ( st, bytes.as_str ( ) ) , ( StatusCode:: OK , " step 32" ) ) ;
2917
assert! ( db:: flow:: artifacts( & h.app.db, low) .await.unwrap ( ) .is_empty ( ) , " a checkpoint is no artifact of the job" ) ;
ab4df6aa Andy McCall 11 days ago ↶
2918
assert_eq! ( claim_as ( " runner-secret" , " earth-1" ) .await.map ( | id | id ! = low ) , Some ( true ) , " then new work" ) ;
2919
2920
2921
let phone = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " phone" ) .unwrap ( ) ;
2922
sqlx:: query( " UPDATE flow_runners SET last_seen_ms = $1 WHERE id = $2" )
2923
.bind ( db:: now_ms( ) - oxygenhub:: flow:: LEASE_MS - 1000 )
2924
.bind ( phone.id)
2925
.execute ( & h.app.db)
2926
.await
2927
.unwrap ( ) ;
2928
assert_eq! ( oxygenhub:: flow:: requeue_lapsed( & h.app) .await.unwrap ( ) , 1 ) ;
2929
let j = db:: flow:: job( & h.app.db, low) .await.unwrap ( ) .unwrap ( ) ;
2930
assert_eq! ( ( j.status.as_str ( ) , j.handbacks) , ( " queued" , 2 ) ) ;
2931
let log: Vec< String> = db:: flow:: log( & h.app.db, low, 0 , 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.text ) .collect ( ) ;
2932
assert_eq! ( log.last ( ) .unwrap ( ) , " ── the runner phone has not been heard from for 3 minutes; queued again ahead of new work" ) ;
2933
assert_eq! ( claim_as ( " runner-two" , " plex-1" ) .await, Some ( low) , " another runner takes it" ) ;
2934
}
2935
3b650652 Andy McCall 10 days ago ↶
2936
2937
2938
2939
# [ tokio ::test ( flavor = " multi_thread" ) ]
2940
async fn a_claim_carries_the_job_s_expected_time ( ) {
2941
let h = hub ( ) .await;
2942
create ( & h.app, " andy" , " eta" ) .await;
2943
let url = format! ( " {} /andy/eta" , h.base) ;
2944
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2945
let c = client ( & url, work.path ( ) ) ;
2946
let one = " schema-version 1\n sandbox \" alpine\" {\n rootfs \" cas://blob/ROOTFS\" \n }\n job \" build\" {\n sandbox \" alpine\" \n exec \" make\" \n }\n " .replace ( " ROOTFS" , ROOTFS ) ;
2947
write ( work.path ( ) , " .flow/pipeline.kdl" , & one) ;
2948
c.commit ( " One job" , & [ ] ) .unwrap ( ) ;
2949
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2950
runs_of ( & h.app, " andy" , " eta" ) .await;
2951
let first = claim ( & h.app) .await.expect ( " a job" ) ;
2952
assert! ( first.get ( " expected_ms" ) .is_none ( ) , " never passed: no estimate, {first}" ) ;
2953
let id = first[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2954
assert_eq! ( finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 } ) ) .await, StatusCode:: NO_CONTENT ) ;
2955
2956
sqlx:: query( " UPDATE flow_jobs SET started_ms = finished_ms - 42000 WHERE id = $1" ) .bind ( id) .execute ( & h.app.db) .await.unwrap ( ) ;
2957
write ( work.path ( ) , " README" , " again\n " ) ;
2958
c.commit ( " Again" , & [ ] ) .unwrap ( ) ;
2959
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2960
let mut next = None ;
2961
for _ in 0 .. 100 {
2962
if let Some ( j) = claim ( & h.app) .await {
2963
next = Some ( j) ;
2964
break ;
2965
}
2966
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
2967
}
2968
let next = next.expect ( " the second run's job" ) ;
2969
assert_eq! ( next[ " expected_ms" ] , 42_000 , " {next}" ) ;
2970
}
2971
554ccacf Andy McCall 1 month ago ↶
2972
2973
2974
2975
# [ tokio ::test ( flavor = " multi_thread" ) ]
2976
async fn a_coverage_artifact_becomes_a_coverage_report ( ) {
2977
let h = hub ( ) .await;
2978
create ( & h.app, " andy" , " cov" ) .await;
2979
let url = format! ( " {} /andy/cov" , h.base) ;
2980
let work = tempfile:: tempdir( ) .unwrap ( ) ;
2981
let c = client ( & url, work.path ( ) ) ;
2982
write (
2983
work.path ( ) ,
2984
" .flow/pipeline.kdl" ,
2985
& format! (
2986
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" coverage\" {{ \n sandbox \" alpine\" \n exec \" make\" \" coverage\" \n output {{ \n artifact \" coverage\" {{ \n path \" /src/lcov.info\" \n }} \n }} \n }} \n "
2987
) ,
2988
) ;
2989
write ( work.path ( ) , " src/lib.rs" , " pub fn add(a: u32, b: u32) -> u32 {\n a + b\n }\n \n pub fn unused() {\n todo!()\n }\n " ) ;
2990
c.snapshot ( " test" ) .unwrap ( ) ;
2991
c.commit ( " Code and coverage" , & [ ] ) .unwrap ( ) ;
2992
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
2993
let run = runs_of ( & h.app, " andy" , " cov" ) .await.remove ( 0 ) ;
c8df33a6 Andy McCall 25 days ago ↶
2994
assert! ( ! call ( & h.app, " GET" , " /andy/cov" , None , None ) .await.1. contains ( " >Reports</a>" ) , " no tab before a report" ) ;
554ccacf Andy McCall 1 month ago ↶
2995
let id = claim ( & h.app) .await.unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
2996
2997
let ( st, text) = upload ( & h.app, id, " coverage" , b " this is not a report" ) .await;
2998
assert_eq! ( st, StatusCode:: OK , " a bad report never fails the upload: {text}" ) ;
2999
let lcov = " TN:\n SF:/src/src/lib.rs\n FN:1,add\n FN:5,unused\n FNDA:3,add\n FNDA:0,unused\n DA:1,3\n DA:2,3\n DA:5,0\n DA:6,0\n end_of_record\n \
3000
SF:/src/src/gone.rs\n DA:1,1\n end_of_record\n SF:/usr/lib/rustlib/x.rs\n DA:1,1\n end_of_record\n " ;
3001
let ( st, text) = upload ( & h.app, id, " coverage" , lcov.as_bytes ( ) ) .await;
3002
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
3003
let log: Vec< String> = db:: flow:: log( & h.app.db, id, 0 , 50 ) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.text ) .collect ( ) ;
3004
assert! ( log.iter ( ) .any ( | l | l.contains ( " coverage: this is not an LCOV report" ) ) , " {log:?}" ) ;
3005
assert! (
3006
log.iter ( ) .any ( | l | l = = " coverage: 50.0% of lines (2 of 4) in 1 files (2 named files are not in this state and were left out)" ) ,
3007
" {log:?}"
3008
) ;
3009
3010
let ( st, page) = call ( & h.app, " GET" , " /andy/cov/coverage" , None , None ) .await;
3011
assert_eq! ( st, StatusCode:: OK ) ;
c987c865 Andy McCall 1 month ago ↶
3012
assert! (
c8df33a6 Andy McCall 25 days ago ↶
3013
page.contains ( " >Reports</a>" )
c987c865 Andy McCall 1 month ago ↶
3014
& & page.contains ( " 50.0%" )
3015
& & page.contains ( " <strong>2</strong> of <strong>4</strong> relevant lines covered" ) ,
3016
" {page}"
3017
) ;
554ccacf Andy McCall 1 month ago ↶
3018
assert! ( page.contains ( " 1 of 2" ) & & page.contains ( " /andy/cov/coverage/" ) & & page.contains ( " /file/src/lib.rs" ) , " {page}" ) ;
3019
let report = crate_report ( & h.app, run.id) .await;
3020
let ( st, file) = call ( & h.app, " GET" , & format! ( " /andy/cov/coverage/{report} /file/src/lib.rs" ) , None , None ) .await;
3021
assert_eq! ( st, StatusCode:: OK , " {file}" ) ;
3022
assert_eq! ( file.matches ( " class=\" cov-hit\" " ) .count ( ) , 2 , " {file}" ) ;
3023
assert_eq! ( file.matches ( " class=\" cov-miss\" " ) .count ( ) , 2 , " {file}" ) ;
3024
assert! ( file.contains ( " 3×" ) , " {file}" ) ;
3025
assert_eq! ( call ( & h.app, " GET" , & format! ( " /andy/cov/coverage/{report} /file/src/gone.rs" ) , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
3026
let ( _ , badge) = call ( & h.app, " GET" , " /andy/cov/coverage/badge.svg" , None , None ) .await;
3027
assert! ( badge.contains ( " coverage: 50.0%" ) , " {badge}" ) ;
3028
let ( _ , run_page) = call ( & h.app, " GET" , & format! ( " /andy/cov/flow/{} " , run.number) , None , None ) .await;
3029
assert! ( run_page.contains ( " coverage 50.0%" ) , " {run_page}" ) ;
617c2840 Andy McCall 1 month ago ↶
3030
3031
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/cov/coverage" , None , None ) .await;
3032
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
3033
assert_eq! ( ( list[ 0 ] [ " percent" ] .as_f64 ( ) , list[ 0 ] [ " format" ] .as_str ( ) ) , ( Some ( 50. 0 ) , Some ( " lcov" ) ) , " {list}" ) ;
3034
let ( _ , one) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/cov/coverage/{report} " ) , None , None ) .await;
3035
let one: serde_json:: Value = serde_json:: from_str( & one) .unwrap ( ) ;
3036
assert_eq! ( ( one[ " files" ] [ 0 ] [ " path" ] .as_str ( ) , one[ " files" ] [ 0 ] [ " lines_hit" ] .as_i64 ( ) ) , ( Some ( " src/lib.rs" ) , Some ( 2 ) ) , " {one}" ) ;
7edaff01 Andy McCall 1 month ago ↶
3037
3038
3039
3040
let row = db:: repos:: get( & h.app.db, " andy" , " cov" ) .await.unwrap ( ) .unwrap ( ) ;
3041
let check = | checks : Vec< db:: hooks:: Check> | checks.into_iter ( ) .find ( | c | c.name = = " coverage" ) .expect ( " a coverage check" ) ;
3042
let c = check ( db:: hooks:: checks_for_state( & h.app.db, row.id, & run.state) .await.unwrap ( ) ) ;
3043
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " passed" , " 50.0%" ) ) ;
3044
assert! ( c.url.ends_with ( & format! ( " /andy/cov/coverage?report={report} " ) ) , " {}" , c.url) ;
3045
oxygenhub:: coverage:: set_rules( & h.app.db, row.id, oxygenhub:: coverage:: Rules { min: Some ( 60. 0 ) , max_drop: None } ) .await.unwrap ( ) ;
3046
assert_eq! ( upload ( & h.app, id, " coverage" , lcov.as_bytes ( ) ) .await.0 , StatusCode:: OK ) ;
3047
let c = check ( db:: hooks:: checks_for_state( & h.app.db, row.id, & run.state) .await.unwrap ( ) ) ;
3048
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " failed" , " 50.0%: below the minimum of 60%" ) ) ;
554ccacf Andy McCall 1 month ago ↶
3049
}
3050
3051
async fn crate_report ( app : & Shared, run_id : i64 ) -> i64 {
3052
oxygenhub:: coverage:: for_run( & app.db, run_id) .await.unwrap ( ) [ 0 ] .id
3053
}
36e5a9c1 Andy McCall 1 month ago ↶
3054
3055
3056
3057
# [ tokio ::test ( flavor = " multi_thread" ) ]
3058
async fn junit_reports_make_a_tests_tab ( ) {
3059
let h = hub ( ) .await;
3060
create ( & h.app, " andy" , " t" ) .await;
3061
let url = format! ( " {} /andy/t" , h.base) ;
3062
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3063
let c = client ( & url, work.path ( ) ) ;
3064
write (
3065
work.path ( ) ,
3066
" .flow/pipeline.kdl" ,
3067
& format! (
3068
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" test\" {{ \n sandbox \" alpine\" \n exec \" make\" \" test\" \n output {{ \n artifact \" junit\" {{ \n path \" /src/junit.xml\" \n }} \n }} \n }} \n "
3069
) ,
3070
) ;
c8df33a6 Andy McCall 25 days ago ↶
3071
assert! ( ! call ( & h.app, " GET" , " /andy/t" , None , None ) .await.1. contains ( " >Reports</a>" ) ) ;
36e5a9c1 Andy McCall 1 month ago ↶
3072
let case = | name : & str , ok : bool , secs : & str | {
3073
if ok {
3074
format! ( r # "<testcase classname="t.suite" name="{name} " time="{secs} "/>"# )
3075
} else {
3076
format! (
3077
r # "<testcase classname="t.suite" name="{name} " time="{secs} "><failure message="{name} broke">AssertionError</failure></testcase>"#
3078
)
3079
}
3080
} ;
3081
let runs = [
3082
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , true , " 2.5" ) , case ( " c" , true , " 0.2" ) ] ,
3083
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , false , " 2.6" ) , case ( " c" , true , " 0.2" ) , case ( " d" , true , " 0.3" ) ] ,
3084
vec! [ case ( " a" , true , " 0.1" ) , case ( " b" , true , " 2.4" ) , case ( " c" , true , " 0.2" ) , case ( " d" , true , " 0.3" ) ] ,
3085
] ;
3086
for ( i, cases) in runs.iter ( ) .enumerate ( ) {
3087
write ( work.path ( ) , " n.txt" , & i.to_string ( ) ) ;
3088
c.snapshot ( " test" ) .unwrap ( ) ;
3089
c.commit ( & format! ( " Run {i} " ) , & [ ] ) .unwrap ( ) ;
3090
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3091
3092
let mut a = None ;
3093
for _ in 0 .. 100 {
3094
a = claim ( & h.app) .await;
3095
if a.is_some ( ) {
3096
break ;
3097
}
3098
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3099
}
3100
let a = a.expect ( " a job to claim" ) ;
3101
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3102
let xml = format! ( " <testsuites><testsuite name=\" t\" >{} </testsuite></testsuites>" , cases.concat ( ) ) ;
3103
assert_eq! ( upload ( & h.app, id, " junit" , xml.as_bytes ( ) ) .await.0 , StatusCode:: OK ) ;
3104
let failed = cases.iter ( ) .any ( | c | c.contains ( " <failure" ) ) ;
3105
let mut r = receipt ( " test" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , failed as u32 ) ;
3106
if ! failed {
3107
r[ " outputs" ] = serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( xml.as_bytes ( ) ) .to_hex ( ) ] ) ;
3108
}
3109
let code = failed as i32 ;
3110
assert_eq! (
3111
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : code, " duration_ms" : 700 , " receipt" : r } ) ) .await,
3112
StatusCode:: NO_CONTENT
3113
) ;
3114
}
3115
let ( st, page) = call ( & h.app, " GET" , " /andy/t/tests" , None , None ) .await;
3116
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
3117
if let Some ( out) = std:: env:: var_os( " OXYHUB_DUMP_TESTS_PAGE" ) {
3118
std:: fs:: write( out, & page) .unwrap ( ) ;
3119
}
c8df33a6 Andy McCall 25 days ago ↶
3120
assert! ( page.contains ( " >Reports</a>" ) , " the tab" ) ;
36e5a9c1 Andy McCall 1 month ago ↶
3121
assert! ( page.contains ( " 100%" ) & & page.contains ( " of 4 tests passed" ) , " the latest run passes" ) ;
3122
assert! ( page.contains ( " 1 fixed" ) & & page.contains ( " t.suite::b" ) , " b fixed against the run before" ) ;
3123
assert! ( page.contains ( " 1 ✕" ) & & page.contains ( " 2 ✓" ) , " b is flaky: {page}" ) ;
3124
assert! ( page.contains ( " <svg class=\" cov-chart\" " ) , " charts" ) ;
3125
3126
let slow = page.find ( " Slowest tests" ) .unwrap ( ) ;
3127
assert! ( page[ slow.. ] .find ( " t.suite::b" ) .unwrap ( ) < page[ slow.. ] .find ( " t.suite::d" ) .unwrap ( ) ) ;
3128
3129
let row = db:: repos:: get( & h.app.db, " andy" , " t" ) .await.unwrap ( ) .unwrap ( ) ;
3130
let second = oxygenhub:: junit:: on_bookmark( & h.app.db, row.id, " main" , 10 ) .await.unwrap ( ) [ 1 ] .id;
3131
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/t/tests?report={second} " ) , None , None ) .await;
3132
assert! ( page.contains ( " 1 failing" ) & & page.contains ( " Newly failing" ) & & page.contains ( " b broke" ) , " {page}" ) ;
3133
assert! ( page.contains ( " 1 new tests" ) , " d is new" ) ;
617c2840 Andy McCall 1 month ago ↶
3134
3135
3136
3137
let ( st, list) = call ( & h.app, " GET" , " /api/v1/repos/andy/t/tests" , None , None ) .await;
3138
assert_eq! ( st, StatusCode:: OK , " {list}" ) ;
3139
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
3140
assert_eq! ( list.as_array ( ) .unwrap ( ) .len ( ) , 3 ) ;
3141
assert_eq! ( ( list[ 0 ] [ " tests" ] .as_i64 ( ) , list[ 1 ] [ " failed" ] .as_i64 ( ) ) , ( Some ( 4 ) , Some ( 1 ) ) ) ;
3142
let ( _ , one) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/t/tests/{second} ?status=failed" ) , None , None ) .await;
3143
let one: serde_json:: Value = serde_json:: from_str( & one) .unwrap ( ) ;
3144
assert_eq! ( one[ " cases" ] .as_array ( ) .unwrap ( ) .len ( ) , 1 ) ;
3145
assert_eq! ( ( one[ " cases" ] [ 0 ] [ " name" ] .as_str ( ) , one[ " cases" ] [ 0 ] [ " message" ] .as_str ( ) ) , ( Some ( " b" ) , Some ( " b broke" ) ) ) ;
3146
let ( _ , all) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/t/tests/{second} " ) , None , None ) .await;
3147
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & all) .unwrap ( ) [ " cases" ] .as_array ( ) .unwrap ( ) .len ( ) , 4 ) ;
3148
assert_eq! ( call ( & h.app, " GET" , " /api/v1/repos/andy/t/tests/99999" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
3d5c825a Andy McCall 1 month ago ↶
3149
3150
3151
let reports = oxygenhub:: junit:: on_bookmark( & h.app.db, row.id, " main" , 10 ) .await.unwrap ( ) ;
3152
let check = | state : String| {
3153
let app = h.app.clone ( ) ;
3154
async move { db:: hooks:: checks_for_state( & app.db, row.id, & state) .await.unwrap ( ) .into_iter ( ) .find ( | c | c.name = = " tests" ) .unwrap ( ) }
3155
} ;
3156
let c = check ( reports[ 1 ] .state.clone ( ) ) .await;
3157
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " failed" , " 1 failing; 1 new against main: t.suite::b" ) ) ;
3158
let c = check ( reports[ 0 ] .state.clone ( ) ) .await;
3159
assert_eq! ( ( c.status.as_str ( ) , c.summary.as_str ( ) ) , ( " passed" , " 4 passed" ) ) ;
3160
3161
3162
let ( _ , tab) = call ( & h.app, " GET" , " /andy/t/tests" , None , None ) .await;
3163
assert! (
3164
tab.contains ( " /andy/t/tests/case?classname=t.suite&name=b" ) | | tab.contains ( " /andy/t/tests/case?classname=t.suite&name=b" ) ,
3165
" linked"
3166
) ;
3167
let ( st, hist) = call ( & h.app, " GET" , " /andy/t/tests/case?classname=t.suite&name=b" , None , None ) .await;
3168
assert_eq! ( st, StatusCode:: OK , " {hist}" ) ;
3169
assert! ( hist.contains ( " 2 passed" ) & & hist.contains ( " 1 failed" ) & & hist.contains ( " b broke" ) , " {hist}" ) ;
3170
assert_eq! ( call ( & h.app, " GET" , " /andy/t/tests/case?classname=t.suite&name=nope" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
36e5a9c1 Andy McCall 1 month ago ↶
3171
}
67040b15 Andy McCall 25 days ago ↶
3172
3173
3174
3175
# [ tokio ::test ( flavor = " multi_thread" ) ]
3176
async fn a_job_no_runner_can_take_says_why ( ) {
3177
let h = hub ( ) .await;
3178
create ( & h.app, " andy" , " demo" ) .await;
3179
let url = format! ( " {} /andy/demo" , h.base) ;
3180
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3181
let c = client ( & url, work.path ( ) ) ;
3182
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3183
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3184
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3185
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3186
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
3187
3188
3189
let r = db:: runners:: configured( & h.app.db, & blake3:: hash( b " elsewhere" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
3190
db:: runners:: seen( & h.app.db, r.id, None , None ) .await.unwrap ( ) ;
3191
let other = " ab" .repeat ( 32 ) ;
92a797b2 Andy McCall 25 days ago ↶
3192
db:: runners:: set_offer( & h.app.db, r.id, & [ " microvm" .to_string ( ) ] , Some ( & [ other] ) , false , " p" ) .await.unwrap ( ) ;
67040b15 Andy McCall 25 days ago ↶
3193
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3194
3195
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
3196
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
3197
let build = v[ " jobs" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | j | j[ " name" ] = = " build" ) .unwrap ( ) .clone ( ) ;
3198
let why = build[ " waiting" ] .as_str ( ) .unwrap_or_default ( ) .to_string ( ) ;
3199
assert! ( why.contains ( " earth-9 does not hold image" ) & & why.contains ( & ROOTFS [ .. 12 ] ) , " {body}" ) ;
3200
let ( _ , page) = call ( & h.app, " GET" , " /andy/demo/flow/1" , None , None ) .await;
3201
assert! ( page.contains ( " data-waiting" ) & & page.contains ( " does not hold image" ) , " the run page says it" ) ;
13b27cb2 Andy McCall 17 days ago ↶
3202
3203
3204
3205
let mut summary = String :: new( ) ;
3206
for _ in 0 .. 40 {
3207
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
3208
summary = checks.iter ( ) .find ( | c | c.name = = " flow/build" ) .unwrap ( ) .summary.clone ( ) ;
3209
if summary.contains ( " does not hold image" ) {
3210
break ;
3211
}
3212
let build = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .into_iter ( ) .find ( | j | j.name = = " build" ) .unwrap ( ) ;
3213
db:: flow:: set_waiting( & h.app.db, build.id, " " ) .await.unwrap ( ) ;
3214
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3215
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
3216
}
3217
assert! ( summary.contains ( " does not hold image" ) , " {summary}" ) ;
67040b15 Andy McCall 25 days ago ↶
3218
}
92a797b2 Andy McCall 25 days ago ↶
3219
58cbf424 Andy McCall 17 days ago ↶
3220
3221
3222
3223
# [ tokio ::test ( flavor = " multi_thread" ) ]
3224
async fn a_job_waiting_for_a_busy_runner_says_so ( ) {
3225
let h = hub ( ) .await;
3226
create ( & h.app, " andy" , " demo" ) .await;
3227
let url = format! ( " {} /andy/demo" , h.base) ;
3228
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3229
let c = client ( & url, work.path ( ) ) ;
3230
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3231
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3232
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3233
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3234
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
3235
3236
let r = db:: runners:: configured( & h.app.db, & blake3:: hash( b " busy" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
3237
db:: runners:: seen( & h.app.db, r.id, None , None ) .await.unwrap ( ) ;
3238
db:: runners:: set_offer( & h.app.db, r.id, & [ " microvm" .to_string ( ) ] , Some ( & [ ROOTFS .to_string ( ) ] ) , false , " p" ) .await.unwrap ( ) ;
3239
sqlx:: query(
3240
" UPDATE flow_jobs SET status = 'running', runner_id = $1, runner = 'earth-9', started_ms = $2 WHERE run_id = $3 AND name = 'lint'" ,
3241
)
3242
.bind ( r.id)
3243
.bind ( db:: now_ms( ) )
3244
.bind ( run.id)
3245
.execute ( & h.app.db)
3246
.await
3247
.unwrap ( ) ;
3248
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3249
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
3250
let build = jobs.iter ( ) .find ( | j | j.name = = " build" ) .unwrap ( ) ;
3251
assert! ( build.waiting.contains ( " the runners that can take it are busy: earth-9 busy (1 of 1)" ) , " {}" , build.waiting) ;
3252
3253
sqlx:: query( " UPDATE flow_runners SET max_jobs = 2 WHERE id = $1" ) .bind ( r.id) .execute ( & h.app.db) .await.unwrap ( ) ;
3254
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3255
let build = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) .into_iter ( ) .find ( | j | j.name = = " build" ) .unwrap ( ) ;
5d66f27c Andy McCall 9 days ago ↶
3256
assert! ( build.waiting.contains ( " earth-9 has a free slot (1 of 2) but not the memory, CPUs or disk for it now" ) , " {}" , build.waiting) ;
58cbf424 Andy McCall 17 days ago ↶
3257
}
3258
794ff94f Andy McCall 11 days ago ↶
3259
3260
3261
# [ tokio ::test ( flavor = " multi_thread" ) ]
3262
async fn an_expedited_run_moves_the_queue_at_once ( ) {
3263
let h = hub ( ) .await;
3264
for name in [ " first" , " urgent" ] {
3265
create ( & h.app, " andy" , name) .await;
3266
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3267
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
3268
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3269
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3270
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3271
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3272
3273
assert_eq! ( runs_of ( & h.app, " andy" , name) .await.len ( ) , 1 ) ;
3274
}
3275
3276
let r = db:: runners:: configured( & h.app.db, & blake3:: hash( b " busy" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
3277
db:: runners:: seen( & h.app.db, r.id, None , None ) .await.unwrap ( ) ;
3278
db:: runners:: set_offer( & h.app.db, r.id, & [ " microvm" .to_string ( ) ] , Some ( & [ ROOTFS .to_string ( ) ] ) , false , " p" ) .await.unwrap ( ) ;
3279
let first = runs_of ( & h.app, " andy" , " first" ) .await.remove ( 0 ) ;
3280
sqlx:: query(
3281
" UPDATE flow_jobs SET status = 'running', runner_id = $1, runner = 'earth-9', started_ms = $2 WHERE run_id = $3 AND name = 'lint'" ,
3282
)
3283
.bind ( r.id)
3284
.bind ( db:: now_ms( ) )
3285
.bind ( first.id)
3286
.execute ( & h.app.db)
3287
.await
3288
.unwrap ( ) ;
3289
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3290
let db = h.app.db.clone ( ) ;
3291
let build = | run : i64 | {
3292
let db = db.clone ( ) ;
3293
async move { db:: flow:: jobs( & db, run) .await.unwrap ( ) .into_iter ( ) .find ( | j | j.name = = " build" ) .unwrap ( ) }
3294
} ;
3295
let urgent = runs_of ( & h.app, " andy" , " urgent" ) .await.remove ( 0 ) ;
3296
assert! ( build ( urgent.id) .await.waiting.contains ( " 1 ready job comes first" ) , " {}" , build ( urgent.id) .await.waiting) ;
3297
3298
3299
let res = oxygenhub:: router( h.app.clone ( ) )
3300
.oneshot (
3301
Request:: post( " /register" )
3302
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
3303
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
3304
.unwrap ( ) ,
3305
)
3306
.await
3307
.unwrap ( ) ;
3308
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
3309
let page = oxygenhub:: router( h.app.clone ( ) )
3310
.oneshot ( Request:: get( " /andy/urgent/flow/1" ) .header ( header:: COOKIE , cookie.clone ( ) ) .body ( Body:: empty( ) ) .unwrap ( ) )
3311
.await
3312
.unwrap ( ) ;
3313
let page = String :: from_utf8_lossy( & page.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
3314
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
3315
let csrf = page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
3316
let res = oxygenhub:: router( h.app.clone ( ) )
3317
.oneshot (
3318
Request:: post( " /andy/urgent/flow/1/expedite" )
3319
.header ( header:: COOKIE , cookie.clone ( ) )
3320
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
3321
.body ( Body:: from( format! ( " _csrf={csrf} &reason=incident" ) ) )
3322
.unwrap ( ) ,
3323
)
3324
.await
3325
.unwrap ( ) ;
3326
assert! ( res.status ( ) .is_redirection ( ) , " {}" , res.status ( ) ) ;
3327
assert! ( ! build ( urgent.id) .await.waiting.contains ( " come" ) , " first now: {}" , build ( urgent.id) .await.waiting) ;
3328
let w = build ( first.id) .await.waiting;
3329
assert! ( w.contains ( " 2 ready jobs come first (after expedited run andy/urgent #1)" ) , " {w}" ) ;
b1860d60 Andy McCall 8 days ago ↶
3330
3331
3332
3333
let andy = db:: accounts:: account_by_name( & h.app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
3334
let ( token, hash) = oxygenhub:: auth:: new_token( ) ;
3335
db:: accounts:: create_token( & h.app.db, andy.id, " phone" , & hash, " repo:read,repo:write" , " " , None ) .await.unwrap ( ) ;
3336
let url = format! ( " /api/v1/repos/andy/first/flow/runs/{} /expedite" , first.number) ;
3337
let ( st, body) = call ( & h.app, " POST" , & url, Some ( & token) , Some ( r # "{"reason":" "}"# .into ( ) ) ) .await;
3338
assert_eq! ( st, StatusCode:: CONFLICT , " {body}" ) ;
3339
assert! ( body.contains ( " Say why the run is urgent." ) , " {body}" ) ;
3340
let ( st, body) = call ( & h.app, " POST" , & url, Some ( & token) , Some ( r # "{"reason":"the fix for the outage"}"# .into ( ) ) ) .await;
3341
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3342
let run = db:: flow:: run( & h.app.db, first.repo_id, first.number) .await.unwrap ( ) .unwrap ( ) ;
3343
assert! (
3344
run.priority = = " urgent" & & run.priority_note.contains ( " expedited by andy: the fix for the outage" ) ,
3345
" {} {}" ,
3346
run.priority,
3347
run.priority_note
3348
) ;
794ff94f Andy McCall 11 days ago ↶
3349
}
3350
92a797b2 Andy McCall 25 days ago ↶
3351
async fn put_bytes ( app : & Shared, path : & str , bearer : & str , bytes : Vec< u8 > ) -> ( StatusCode, String) {
3352
let req = Request:: put( path)
3353
.header ( header:: AUTHORIZATION , format! ( " Bearer {bearer} " ) )
3354
.header ( header:: CONTENT_TYPE , " application/octet-stream" )
3355
.body ( Body:: from( bytes) )
3356
.unwrap ( ) ;
3357
let res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
3358
let st = res.status ( ) ;
3359
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
3360
}
3361
3362
3363
3364
3365
951aed50 Andy McCall 9 days ago ↶
3366
3367
3368
92a797b2 Andy McCall 25 days ago ↶
3369
# [ tokio ::test ( flavor = " multi_thread" ) ]
951aed50 Andy McCall 9 days ago ↶
3370
async fn a_runner_offers_the_manifest_of_an_image_the_hub_lacks ( ) {
3371
let h = hub ( ) .await;
3372
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3373
let image: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i % 241 ) as u8 ) .collect ( ) ;
3374
let mut chunks = std:: collections:: HashMap:: new( ) ;
3375
let m = jet:: images:: scan( & image[ .. ] , Some ( " x86_64" .into ( ) ) , | c , b | {
3376
chunks.insert ( c.hash ( ) .to_string ( ) , b.to_vec ( ) ) ;
3377
Ok ( ( ) )
3378
} )
3379
.unwrap ( ) ;
3380
let hash = m.hash.clone ( ) ;
3381
3382
let held = serde_json:: json! ( { " images" : [ hash.clone ( ) , " not-a-hash" ] } ) .to_string ( ) ;
3383
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/images/held" , Some ( " runner-secret" ) , Some ( held.clone ( ) ) ) .await;
3384
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3385
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " wanted" ] , serde_json:: json! ( [ hash.clone ( ) ] ) ) ;
3386
3387
assert_ne! ( call ( & h.app, " POST" , " /api/v1/flow/images/held" , Some ( " site" ) , Some ( held.clone ( ) ) ) .await.0 , StatusCode:: OK ) ;
3388
3389
let manifest = serde_json:: to_string( & m) .unwrap ( ) ;
3390
let wrong = format! ( " /api/v1/flow/images/{} /held" , " ab" .repeat ( 32 ) ) ;
3391
assert_eq! ( call ( & h.app, " PUT" , & wrong, Some ( " runner-secret" ) , Some ( manifest.clone ( ) ) ) .await.0 , StatusCode:: BAD_REQUEST ) ;
3392
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} /held" ) , Some ( " runner-secret" ) , Some ( manifest.clone ( ) ) ) .await;
3393
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
3394
let kept = oxygenhub:: images:: kept( & h.app.db) .await.unwrap ( ) ;
3395
let row = kept.iter ( ) .find ( | k | k.hash = = hash ) .expect ( " recorded" ) ;
3396
assert_eq! ( row.source, " peer" ) ;
3397
let ( _ , body) = call ( & h.app, " POST" , " /api/v1/flow/images/held" , Some ( " runner-secret" ) , Some ( held) ) .await;
3398
assert! ( body.contains ( " \" wanted\" :[]" ) , " {body}" ) ;
3399
3400
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " runner-secret" ) , None ) .await;
3401
assert_eq! ( st, StatusCode:: OK ) ;
3402
let first = m.needed ( ) [ 0 ] .to_string ( ) ;
3403
assert_ne! ( call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/chunks/{first} " ) , Some ( " runner-secret" ) , None ) .await.0 , StatusCode:: OK ) ;
3404
let reach = oxygenhub:: images:: reach( & h.app) .await.unwrap ( ) ;
3405
assert! ( reach.peer_only ( & hash) ) ;
3406
3407
for c in m.needed ( ) {
3408
let ( st, body) = put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{c} " ) , " site" , chunks[ c] .clone ( ) ) .await;
3409
assert_eq! ( st, StatusCode:: NO_CONTENT , " {body}" ) ;
3410
}
3411
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " site" ) , Some ( manifest) ) .await;
3412
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
3413
let kept = oxygenhub:: images:: kept( & h.app.db) .await.unwrap ( ) ;
3414
assert_eq! ( kept.iter ( ) .find ( | k | k.hash = = hash ) .unwrap ( ) .source, " hub" ) ;
3415
assert! ( ! oxygenhub:: images:: reach( & h.app) .await.unwrap ( ) .peer_only ( & hash) ) ;
3416
}
3417
3418
# [ tokio ::test ( flavor = " multi_thread" ) ]
92a797b2 Andy McCall 25 days ago ↶
3419
async fn images_are_sent_by_chunk_and_offered_to_runners_that_fetch ( ) {
3420
let h = hub ( ) .await;
3421
3422
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3423
3424
let mut image: Vec< u8 > = ( 0 .. 400_000 u32 ) .map ( | i | ( i % 251 ) as u8 ) .collect ( ) ;
3425
image.extend ( std:: iter:: repeat_n( 0 u8 , 2 < < 20 ) ) ;
3426
image.extend ( ( 0 .. 100_000 u32 ) .map ( | i | ( i % 239 ) as u8 ) ) ;
3427
let mut chunks = std:: collections:: HashMap:: new( ) ;
3428
let m = jet:: images:: scan( & image[ .. ] , Some ( " x86_64" .into ( ) ) , | c , b | {
3429
chunks.insert ( c.hash ( ) .to_string ( ) , b.to_vec ( ) ) ;
3430
Ok ( ( ) )
3431
} )
3432
.unwrap ( ) ;
3433
let hash = m.hash.clone ( ) ;
3434
3435
3436
let ask = serde_json:: json! ( { " chunks" : m.needed ( ) } ) .to_string ( ) ;
3437
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/images/missing" , Some ( " site" ) , Some ( ask.clone ( ) ) ) .await;
3438
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3439
let missing: Vec< String> = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " missing" ]
3440
.as_array ( )
3441
.unwrap ( )
3442
.iter ( )
3443
.map ( | v | v.as_str ( ) .unwrap ( ) .to_string ( ) )
3444
.collect ( ) ;
3445
assert_eq! ( missing.len ( ) , m.needed ( ) .len ( ) ) ;
3446
let manifest = serde_json:: to_string( & m) .unwrap ( ) ;
3447
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " site" ) , Some ( manifest.clone ( ) ) ) .await;
3448
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " was not sent" ) , " {st} {body}" ) ;
3449
3450
let first = & missing[ 0 ] ;
3451
assert_eq! ( put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{first} " ) , " site" , vec! [ 1 , 2 , 3 ] ) .await.0 , StatusCode:: BAD_REQUEST ) ;
3452
assert_ne! (
3453
put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{first} " ) , " runner-secret" , chunks[ first] .clone ( ) ) .await.0 ,
3454
StatusCode:: NO_CONTENT
3455
) ;
3456
for c in & missing {
3457
let ( st, body) = put_bytes ( & h.app, & format! ( " /api/v1/flow/images/chunks/{c} " ) , " site" , chunks[ c] .clone ( ) ) .await;
3458
assert_eq! ( st, StatusCode:: NO_CONTENT , " {body}" ) ;
3459
}
3460
let ( _ , body) = call ( & h.app, " POST" , " /api/v1/flow/images/missing" , Some ( " site" ) , Some ( ask) ) .await;
3461
assert! ( body.contains ( " \" missing\" :[]" ) , " {body}" ) ;
750579ec Andy McCall 24 days ago ↶
3462
3463
let many: Vec< String> = ( 0 .. 60_000 u32 ) .map ( | i | blake3:: hash( & i.to_le_bytes ( ) ) .to_hex ( ) .to_string ( ) ) .collect ( ) ;
3464
let ( st, body) =
3465
call ( & h.app, " POST" , " /api/v1/flow/images/missing" , Some ( " site" ) , Some ( serde_json:: json! ( { " chunks" : many } ) .to_string ( ) ) ) .await;
3466
assert_eq! ( st, StatusCode:: OK , " {}" , & body[ .. body.len ( ) .min ( 200 ) ] ) ;
3467
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " missing" ] .as_array ( ) .unwrap ( ) .len ( ) , 60_000 ) ;
92a797b2 Andy McCall 25 days ago ↶
3468
3469
let mut wrong = m.clone ( ) ;
3470
wrong.hash = " ab" .repeat ( 32 ) ;
3471
let ( st, _ ) =
3472
call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{} " , wrong.hash) , Some ( " site" ) , Some ( serde_json:: to_string( & wrong) .unwrap ( ) ) )
3473
.await;
3474
assert_eq! ( st, StatusCode:: CONFLICT , " chunks that make another image" ) ;
d9d0d314 Andy McCall 9 days ago ↶
3475
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " site" ) , Some ( manifest.clone ( ) ) ) .await;
3476
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
3477
3478
3479
3480
sqlx:: query( " DELETE FROM jet_images" ) .execute ( & h.app.db) .await.unwrap ( ) ;
3481
let gone = h.app.config.data.join ( " images/chunks" ) .join ( & missing[ 0 ] [ .. 2 ] ) .join ( & missing[ 0 ] ) ;
3482
let kept = std:: fs:: read( & gone) .unwrap ( ) ;
3483
std:: fs:: remove_file( & gone) .unwrap ( ) ;
92a797b2 Andy McCall 25 days ago ↶
3484
let ( st, body) = call ( & h.app, " PUT" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " site" ) , Some ( manifest) ) .await;
3485
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
d9d0d314 Andy McCall 9 days ago ↶
3486
assert! ( oxygenhub:: images:: held( & h.app.db) .await.unwrap ( ) .contains ( & hash) ) ;
3487
std:: fs:: write( & gone, kept) .unwrap ( ) ;
92a797b2 Andy McCall 25 days ago ↶
3488
3489
3490
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{hash} " ) , Some ( " runner-secret" ) , None ) .await;
3491
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3492
let got: jet:: images:: Manifest = serde_json:: from_str( & body) .unwrap ( ) ;
3493
assert_eq! ( got, m) ;
3494
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
3495
let out = dir.path ( ) .join ( " img.ext4" ) ;
3496
let mut fetched = Vec :: new( ) ;
3497
for c in got.needed ( ) {
3498
let req = Request:: get( format! ( " /api/v1/flow/images/chunks/{c} " ) )
3499
.header ( header:: AUTHORIZATION , " Bearer runner-secret" )
3500
.body ( Body:: empty( ) )
3501
.unwrap ( ) ;
3502
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
3503
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
3504
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ;
3505
fetched.push ( ( c.to_string ( ) , bytes) ) ;
3506
}
3507
jet:: images:: assemble( & got, & out, | c | Ok ( fetched.iter ( ) .find ( | ( h , _) | h = = c.hash ( ) ) .unwrap ( ) .1. clone ( ) ) ) .unwrap ( ) ;
3508
assert_eq! ( std:: fs:: read( & out) .unwrap ( ) , image) ;
3509
assert_eq! (
3510
call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{} " , " cd" .repeat ( 32 ) ) , Some ( " runner-secret" ) , None ) .await.0 ,
3511
StatusCode:: NOT_FOUND
3512
) ;
3513
assert_ne! ( call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{hash} " ) , None , None ) .await.0 , StatusCode:: OK , " not anonymous" ) ;
3514
3515
3516
3517
create ( & h.app, " andy" , " demo" ) .await;
3518
let url = format! ( " {} /andy/demo" , h.base) ;
3519
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3520
let c = client ( & url, work.path ( ) ) ;
3521
write (
3522
work.path ( ) ,
3523
" .flow/pipeline.kdl" ,
3524
& format! ( " schema-version 1\n sandbox \" img\" {{ \n rootfs \" cas://blob/{hash} \" \n }} \n job \" build\" {{ \n sandbox \" img\" \n exec \" make\" \n }} \n " ) ,
3525
) ;
3526
c.commit ( " Build on the image" , & [ ] ) .unwrap ( ) ;
3527
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3528
runs_of ( & h.app, " andy" , " demo" ) .await;
1c8276b9 Andy McCall 24 days ago ↶
3529
3530
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
3531
let run1 = db:: flow:: run( & h.app.db, row.id, 1 ) .await.unwrap ( ) .unwrap ( ) ;
3532
let checks = checks_when ( & h.app, row.id, & run1.state, 1 ) .await;
3533
assert! ( checks.iter ( ) .all ( | c | c.name ! = " pipeline/images" ) , " {checks:?}" ) ;
92a797b2 Andy McCall 25 days ago ↶
3534
let ask = | fetches : bool | format! ( r # "{{ "runner":"test-1","runtimes":["microvm"],"images":[],"fetches_images":{fetches} }} "# ) ;
3535
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( false ) ) ) .await;
3536
assert_eq! ( st, StatusCode:: NO_CONTENT , " it lacks the image and does not fetch" ) ;
3537
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3538
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
3539
assert! ( body.contains ( " does not hold image" ) , " {body}" ) ;
3540
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( true ) ) ) .await;
3541
assert_eq! ( st, StatusCode:: OK , " it fetches, and the hub holds the image: {body}" ) ;
7829b7c7 Andy McCall 24 days ago ↶
3542
3543
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
3544
assert! ( ! body.contains ( " waiting" ) , " a running job keeps no waiting reason: {body}" ) ;
92a797b2 Andy McCall 25 days ago ↶
3545
}
40dd3f86 Andy McCall 25 days ago ↶
3546
3547
3548
3549
3550
# [ tokio ::test ( flavor = " multi_thread" ) ]
3551
async fn coverage_uploaded_from_a_workstation_says_so ( ) {
3552
let h = hub ( ) .await;
3553
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3554
create ( & h.app, " andy" , " vk" ) .await;
3555
let url = format! ( " {} /andy/vk" , h.base) ;
3556
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3557
let c = client ( & url, work.path ( ) ) ;
3558
write ( work.path ( ) , " src/game.c" , " int a;\n int b;\n int c;\n int d;\n " ) ;
3559
c.commit ( " Game" , & [ ] ) .unwrap ( ) ;
3560
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3561
3562
let lcov = " SF:src/game.c\n DA:1,3\n DA:2,1\n DA:3,0\n DA:4,5\n end_of_record\n SF:reference/rom.asm\n DA:1,1\n end_of_record\n " ;
3563
let body = serde_json:: json! ( { " bookmark" : " main" , " content" : lcov } ) .to_string ( ) ;
3564
let ( st, out) = call ( & h.app, " POST" , " /api/v1/repos/andy/vk/coverage" , Some ( " site" ) , Some ( body) ) .await;
3565
assert_eq! ( st, StatusCode:: CREATED , " {out}" ) ;
3566
let v: serde_json:: Value = serde_json:: from_str( & out) .unwrap ( ) ;
3567
assert_eq! ( ( v[ " uploaded_by" ] .as_str ( ) , v[ " lines_hit" ] .as_i64 ( ) , v[ " lines_found" ] .as_i64 ( ) ) , ( Some ( " andy" ) , Some ( 3 ) , Some ( 4 ) ) ) ;
3568
assert_eq! ( v[ " dropped" ] , 1 , " a file outside the state is left out" ) ;
3569
assert_eq! ( v[ " bookmark" ] , " main" ) ;
3570
3571
let ( _ , page) = call ( & h.app, " GET" , " /andy/vk/coverage" , None , None ) .await;
3572
assert! ( page.contains ( " uploaded by andy" ) & & page.contains ( " 75.0%" ) , " the Coverage page says where it came from" ) ;
3573
let ( _ , page) = call ( & h.app, " GET" , " /andy/vk/reports" , None , None ) .await;
3574
assert! ( page.contains ( " uploaded by andy" ) , " and the Reports card" ) ;
3575
let row = db:: repos:: get( & h.app.db, " andy" , " vk" ) .await.unwrap ( ) .unwrap ( ) ;
3576
let state = v[ " state" ] .as_str ( ) .unwrap ( ) ;
3577
let checks = db:: hooks:: checks_for_state( & h.app.db, row.id, state) .await.unwrap ( ) ;
3578
assert! ( checks.iter ( ) .all ( | c | c.name ! = " coverage" ) , " an upload sets no check: {checks:?}" ) ;
3579
3580
3581
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/vk/coverage" , None , Some ( r # "{"content":"x"}"# .into ( ) ) ) .await;
3582
assert_ne! ( st, StatusCode:: CREATED ) ;
3583
let ( st, _ ) = call (
3584
& h.app,
3585
" POST" ,
3586
" /api/v1/repos/andy/vk/coverage" ,
3587
Some ( " site" ) ,
3588
Some ( r # "{"bookmark":"nope","content":"SF:a\nend_of_record\n"}"# .into ( ) ) ,
3589
)
3590
.await;
3591
assert_eq! ( st, StatusCode:: BAD_REQUEST ) ;
3592
}
2fb70d01 Andy McCall 24 days ago ↶
3593
3594
3595
# [ tokio ::test ( flavor = " multi_thread" ) ]
3596
async fn a_runners_facts_are_kept_from_its_claim ( ) {
3597
let h = hub ( ) .await;
3598
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3599
let body = r # "{"runner":"test-1","runtimes":["microvm"],"facts":{"free_disk_mib":42000,"load_x100":112,"speed":100,"speed_class":3,"kernel":"7.0.0"}}"# ;
3600
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.into ( ) ) ) .await;
3601
assert_eq! ( st, StatusCode:: NO_CONTENT , " nothing to run, but the facts are kept" ) ;
3602
let ( st, out) = call ( & h.app, " GET" , " /api/v1/runners" , Some ( " site" ) , None ) .await;
3603
assert_eq! ( st, StatusCode:: OK , " {out}" ) ;
3604
let v: serde_json:: Value = serde_json:: from_str( & out) .unwrap ( ) ;
3605
let r = & v[ 0 ] ;
3606
assert_eq! (
3607
( r[ " speed" ] .as_i64 ( ) , r[ " speed_class" ] .as_i64 ( ) , r[ " free_disk_mib" ] .as_i64 ( ) , r[ " load_x100" ] .as_i64 ( ) ) ,
3608
( Some ( 100 ) , Some ( 3 ) , Some ( 42000 ) , Some ( 112 ) )
3609
) ;
3610
let row = db:: runners:: list( & h.app.db) .await.unwrap ( ) .remove ( 0 ) ;
3611
assert_eq! ( row.facts_line ( ) , " speed 100 (class 3) · load 1.12 · 41 GiB free" ) ;
3612
}
44a3d2eb Andy McCall 24 days ago ↶
3613
3614
3615
3616
# [ tokio ::test ( flavor = " multi_thread" ) ]
3617
async fn a_jobs_next_run_needs_the_disk_its_last_took ( ) {
3618
let h = hub ( ) .await;
3619
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3620
create ( & h.app, " andy" , " demo" ) .await;
3621
let url = format! ( " {} /andy/demo" , h.base) ;
3622
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3623
let c = client ( & url, work.path ( ) ) ;
3624
write (
3625
work.path ( ) ,
3626
" .flow/pipeline.kdl" ,
3627
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
3628
) ;
3629
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
3630
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3631
runs_of ( & h.app, " andy" , " demo" ) .await;
3632
let claim_with = | free : u64 | format! ( r # "{{ "runner":"test-1","runtimes":["microvm"],"facts":{{ "free_disk_mib":{free} }} }} "# ) ;
3633
3634
3635
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( claim_with ( 500 ) ) ) .await;
3636
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3637
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3638
let done = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " peak_disk_mib" : 8000 , " peak_memory_mib" : 900 } ) ;
3639
assert_eq! ( finish ( & h.app, id, done) .await, StatusCode:: NO_CONTENT ) ;
3640
let job = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
3641
assert_eq! ( ( job.peak_disk_mib, job.peak_memory_mib) , ( Some ( 8000 ) , Some ( 900 ) ) ) ;
3642
3643
3644
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
3645
assert_eq! ( st, StatusCode:: CREATED ) ;
3646
let repo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
3647
let run2 = db:: flow:: run( & h.app.db, repo.id, 2 ) .await.unwrap ( ) .unwrap ( ) ;
3648
let next = db:: flow:: jobs( & h.app.db, run2.id) .await.unwrap ( ) .remove ( 0 ) ;
3649
assert_eq! ( next.need_disk_mib, Some ( 8000 + 2000 + 64 ) ) ;
3650
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( claim_with ( 9000 ) ) ) .await;
3651
assert_eq! ( st, StatusCode:: NO_CONTENT , " 9000 MiB free is not enough" ) ;
3652
oxygenhub:: flow:: explain_waits( & h.app) .await.unwrap ( ) ;
3653
let waiting = db:: flow:: job( & h.app.db, next.id) .await.unwrap ( ) .unwrap ( ) .waiting;
3654
assert! ( waiting.contains ( " 8.8 GiB free; the job needs ~9.8 GiB" ) , " {waiting}" ) ;
3655
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( claim_with ( 20_000 ) ) ) .await;
3656
assert_eq! ( st, StatusCode:: OK , " a roomier runner takes it: {body}" ) ;
3657
}
501d8b26 Andy McCall 24 days ago ↶
3658
3659
3660
3661
3662
# [ tokio ::test ( flavor = " multi_thread" ) ]
3663
async fn a_job_waits_briefly_for_a_runner_that_holds_its_image ( ) {
3664
let h = hub ( ) .await;
3665
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3666
3667
db:: runners:: configured( & h.app.db, & blake3:: hash( b " other-secret" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
3668
let ask = | images : & str | {
3669
format! (
3670
r # "{{ "runner":"r","runtimes":["microvm"],"images":[{images} ],"fetches_images":true,"facts":{{ "speed":100,"speed_class":3,"free_disk_mib":50000}} }} "#
3671
)
3672
} ;
3673
let holder = ask ( & format! ( " \" {ROOTFS} \" " ) ) ;
3674
3675
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder.clone ( ) ) ) .await;
3676
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
3677
3b1bc27f Andy McCall 20 days ago
3678
sqlx:: query( " INSERT INTO jet_images (hash, size, arch, chunks, uploader, created_ms) VALUES ($1, 1, 'x86_64', 1, 'andy', 0)" )
501d8b26 Andy McCall 24 days ago ↶
3679
.bind ( ROOTFS )
3680
.execute ( & h.app.db)
3681
.await
3682
.unwrap ( ) ;
3683
3684
create ( & h.app, " andy" , " demo" ) .await;
3685
let url = format! ( " {} /andy/demo" , h.base) ;
3686
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3687
let c = client ( & url, work.path ( ) ) ;
3688
write (
3689
work.path ( ) ,
3690
" .flow/pipeline.kdl" ,
3691
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
3692
) ;
3693
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
3694
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3695
runs_of ( & h.app, " andy" , " demo" ) .await;
3696
3697
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( " " ) ) ) .await;
3698
assert_eq! ( st, StatusCode:: NO_CONTENT , " held for the runner that holds the image" ) ;
3699
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder) ) .await;
3700
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3701
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3702
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
3703
assert! ( placed.starts_with ( " holds the image, idle, speed 100 (class 3)" ) , " {placed}" ) ;
3704
assert! ( placed.contains ( " held for a better runner" ) , " {placed}" ) ;
3705
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/demo/flow/runs/1" , Some ( " site" ) , None ) .await;
3706
assert! ( body.contains ( " \" placed\" :\" holds the image" ) , " {body}" ) ;
3707
}
20b8fe85 Andy McCall 24 days ago ↶
3708
4fa0da12 Andy McCall 23 days ago ↶
3709
3710
3711
# [ tokio ::test ( flavor = " multi_thread" ) ]
3712
async fn a_job_waits_briefly_for_the_runner_holding_its_build_cache ( ) {
3713
let h = hub ( ) .await;
3714
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3715
db:: runners:: configured( & h.app.db, & blake3:: hash( b " other-secret" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
3716
let slot = flow_core:: api:: cache_slot( " andy/demo" , " build" ) ;
3717
let ask = | caches : & str | {
3718
format! (
3719
r # "{{ "runner":"r","runtimes":["microvm"],"images":["{ROOTFS} "],"fetches_images":true,"caches":[{caches} ],"facts":{{ "speed":100,"speed_class":3,"free_disk_mib":50000}} }} "#
3720
)
3721
} ;
3722
let holder = ask ( & format! ( " \" {slot} \" " ) ) ;
3723
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder.clone ( ) ) ) .await;
3724
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
3725
3726
create ( & h.app, " andy" , " demo" ) .await;
3727
let url = format! ( " {} /andy/demo" , h.base) ;
3728
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3729
let c = client ( & url, work.path ( ) ) ;
3730
write (
3731
work.path ( ) ,
3732
" .flow/pipeline.kdl" ,
3733
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
3734
) ;
3735
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
3736
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3737
runs_of ( & h.app, " andy" , " demo" ) .await;
3738
3739
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( " " ) ) ) .await;
3740
assert_eq! ( st, StatusCode:: NO_CONTENT , " held for the runner holding the build cache" ) ;
3741
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( holder) ) .await;
3742
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3743
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3744
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
3745
assert! ( placed.starts_with ( " holds its build cache, holds the image" ) , " {placed}" ) ;
3746
}
3747
c2be148c Andy McCall 22 days ago ↶
3748
3749
3750
# [ tokio ::test ( flavor = " multi_thread" ) ]
3751
async fn a_job_waits_briefly_for_the_fresher_build_cache ( ) {
3752
let h = hub ( ) .await;
3753
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3754
db:: runners:: configured( & h.app.db, & blake3:: hash( b " other-secret" ) .to_hex ( ) , " earth-9" ) .await.unwrap ( ) ;
3755
let slot = flow_core:: api:: cache_slot( " andy/demo" , " build" ) ;
3756
let now = db:: now_ms( ) ;
3757
let ask = | kept : i64 | {
3758
format! (
3759
r # "{{ "runner":"r","runtimes":["microvm"],"images":["{ROOTFS} "],"fetches_images":true,"caches":["{slot} "],"cache_times":{{ "{slot} ":{kept} }} ,"facts":{{ "speed":100,"speed_class":3,"free_disk_mib":50000}} }} "#
3760
)
3761
} ;
3762
3763
let fresh = ask ( now - 60_000 ) ;
3764
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( fresh.clone ( ) ) ) .await;
3765
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
3766
3767
create ( & h.app, " andy" , " demo" ) .await;
3768
let url = format! ( " {} /andy/demo" , h.base) ;
3769
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3770
let c = client ( & url, work.path ( ) ) ;
3771
write (
3772
work.path ( ) ,
3773
" .flow/pipeline.kdl" ,
3774
& format! ( " schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n }} \n " ) ,
3775
) ;
3776
c.commit ( " Build" , & [ ] ) .unwrap ( ) ;
3777
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3778
runs_of ( & h.app, " andy" , " demo" ) .await;
3779
3780
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( ask ( now - 86_400_000 ) ) ) .await;
3781
assert_eq! ( st, StatusCode:: NO_CONTENT , " held for the fresher copy" ) ;
3782
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " other-secret" ) , Some ( fresh) ) .await;
3783
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3784
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
3785
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
3786
assert! ( placed.starts_with ( " holds its build cache (kept 1 min ago), holds the image" ) , " {placed}" ) ;
3787
}
3788
20b8fe85 Andy McCall 24 days ago ↶
3789
3790
3791
3792
# [ tokio ::test ( flavor = " multi_thread" ) ]
3793
async fn report_files_expire_under_retention ( ) {
3794
let h = hub ( ) .await;
3795
create ( & h.app, " andy" , " demo" ) .await;
3796
let repo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
3797
let add = | bookmark : & 'static str , at : i64 | {
3798
let db = h.app.db.clone ( ) ;
3799
async move {
3800
let id: i64 = sqlx:: query_scalar(
3801
" INSERT INTO reports (repo_id, name, kind, title, summary, context, job, bookmark, state, files, bytes, created_ms) \
3b1bc27f Andy McCall 20 days ago
3802
VALUES ($1, 'perf', 'performance', 'Perf', '', '{}', 'perf', $2, 's', 1, 1, $3) RETURNING id" ,
20b8fe85 Andy McCall 24 days ago ↶
3803
)
3804
.bind ( repo.id)
3805
.bind ( bookmark)
3806
.bind ( at)
3807
.fetch_one ( & db)
3808
.await
3809
.unwrap ( ) ;
3b1bc27f Andy McCall 20 days ago
3810
sqlx:: query( " INSERT INTO report_files (report_id, path, blob, size) VALUES ($1, 'report.toml', $2, 1)" )
20b8fe85 Andy McCall 24 days ago ↶
3811
.bind ( id)
3812
.bind ( format! ( " {:064x}" , id) )
3813
.execute ( & db)
3814
.await
3815
.unwrap ( ) ;
3b1bc27f Andy McCall 20 days ago
3816
sqlx:: query( " INSERT INTO report_metrics (report_id, key, value, unit, better) VALUES ($1, 'ms', 1, '', '')" )
20b8fe85 Andy McCall 24 days ago ↶
3817
.bind ( id)
3818
.execute ( & db)
3819
.await
3820
.unwrap ( ) ;
3821
id
3822
}
3823
} ;
3824
let main_old = add ( " main" , 1 ) .await;
3825
add ( " main" , 2 ) .await;
3826
let mut feature = Vec :: new( ) ;
3827
for at in 0 .. 52 {
3828
feature.push ( add ( " feature" , 10 + at) .await) ;
3829
}
3830
assert_eq! ( oxygenhub:: reports:: expire( & h.app.db) .await.unwrap ( ) , 2 , " the two oldest feature runs" ) ;
3831
let files = | id : i64 | {
3832
let db = h.app.db.clone ( ) ;
3833
async move {
3b1bc27f Andy McCall 20 days ago
3834
sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM report_files WHERE report_id = $1" ) .bind ( id) .fetch_one ( & db) .await.unwrap ( )
20b8fe85 Andy McCall 24 days ago ↶
3835
}
3836
} ;
3837
assert_eq! ( ( files ( feature[ 0 ] ) .await, files ( feature[ 1 ] ) .await, files ( feature[ 2 ] ) .await) , ( 0 , 0 , 1 ) ) ;
3838
assert_eq! ( files ( main_old) .await, 1 , " the default bookmark keeps every run" ) ;
3839
assert_eq! ( oxygenhub:: reports:: metrics( & h.app.db, feature[ 0 ] ) .await.unwrap ( ) .len ( ) , 1 , " metrics are kept" ) ;
3840
assert_eq! ( oxygenhub:: reports:: expire( & h.app.db) .await.unwrap ( ) , 0 , " once" ) ;
3841
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/reports/perf?id={} " , feature[ 0 ] ) , None , None ) .await;
3842
assert! ( page.contains ( " files have expired" ) , " the page says so" ) ;
1c8276b9 Andy McCall 24 days ago ↶
3843
}
3844
3845
3846
3847
3848
# [ tokio ::test ( flavor = " multi_thread" ) ]
3849
async fn a_pipeline_pinning_an_image_nobody_holds_fails_a_check ( ) {
3850
let h = hub ( ) .await;
3851
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3852
3853
let none = r # "{"runner":"test-1","runtimes":["microvm"],"images":[]}"# ;
3854
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( none.into ( ) ) ) .await;
3855
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
3856
create ( & h.app, " andy" , " demo" ) .await;
3857
let url = format! ( " {} /andy/demo" , h.base) ;
3858
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3859
let c = client ( & url, work.path ( ) ) ;
3860
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3861
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3862
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3863
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3864
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
3865
let checks = checks_when ( & h.app, run.repo_id, & run.state, 4 ) .await;
3866
let check = checks.iter ( ) .find ( | c | c.name = = " pipeline/images" ) .expect ( " an images check" ) ;
3867
assert! ( check.status = = " failed" & & check.summary.contains ( & ROOTFS [ .. 12 ] ) & & check.summary.contains ( " flow rootfs push" ) , " {check:?}" ) ;
3868
3869
let holds = format! ( r # "{{ "runner":"test-1","runtimes":["microvm"],"images":["{ROOTFS} "]}} "# ) ;
3870
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( holds) ) .await;
3871
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
3872
assert_eq! ( st, StatusCode:: CREATED ) ;
3873
let checks = db:: hooks:: checks_for_state( & h.app.db, run.repo_id, & run.state) .await.unwrap ( ) ;
3874
let check = checks.iter ( ) .find ( | c | c.name = = " pipeline/images" ) .unwrap ( ) ;
3875
assert_eq! ( check.status, " passed" , " {check:?}" ) ;
20b8fe85 Andy McCall 24 days ago ↶
3876
}
a46d15e7 Andy McCall 24 days ago ↶
3877
3878
3879
3880
3881
# [ tokio ::test ( flavor = " multi_thread" ) ]
3882
async fn the_tracker_names_peers_in_the_same_site_and_signs_a_ticket ( ) {
3883
let h = hub ( ) .await;
3884
let app = & h.app;
3885
let image = " ab" .repeat ( 32 ) ;
3886
let claim_as = | token : & 'static str , name : & 'static str , body : serde_json:: Value| async move {
3887
let mut b = serde_json:: json! ( { " runner" : name, " runtimes" : [ " microvm" ] } ) ;
3888
b.as_object_mut ( ) .unwrap ( ) .extend ( body.as_object ( ) .unwrap ( ) .clone ( ) ) ;
3889
let ( st, out) = call ( app, " POST" , " /api/v1/flow/claim" , Some ( token) , Some ( b.to_string ( ) ) ) .await;
3890
assert_eq! ( st, StatusCode:: NO_CONTENT , " {out}" ) ;
3891
} ;
3892
let announce = | token : & 'static str , state : & 'static str | {
3893
let body = serde_json:: json! ( { " image" : image, " state" : state } ) .to_string ( ) ;
3894
async move { call ( app, " POST" , " /api/v1/flow/swarm/announce" , Some ( token) , Some ( body) ) .await }
3895
} ;
3896
6adcd68b Andy McCall 24 days ago ↶
3897
claim_as (
3898
" runner-secret" ,
3899
" a" ,
3900
serde_json:: json! ( { " images" : [ image] , " swarm" : { " addr" : " 10.0.0.1:7401" , " site" : " lab" , " cert" : " 11" .repeat ( 32 ) } } ) ,
3901
)
3902
.await;
3903
claim_as (
3904
" runner-two" ,
3905
" b" ,
3906
serde_json:: json! ( { " images" : [ ] , " swarm" : { " addr" : " 10.0.0.2:7401" , " site" : " lab" , " cert" : " 22" .repeat ( 32 ) } } ) ,
3907
)
3908
.await;
3909
claim_as (
3910
" runner-three" ,
3911
" c" ,
3912
serde_json:: json! ( { " images" : [ ] , " swarm" : { " addr" : " 10.0.0.3:7401" , " site" : " lab" , " cert" : " 33" .repeat ( 32 ) } } ) ,
3913
)
3914
.await;
a46d15e7 Andy McCall 24 days ago ↶
3915
3916
let ( st, body) = announce ( " runner-two" , " fetching" ) .await;
3917
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
3918
let a: flow_core:: api:: Announced = serde_json:: from_str( & body) .unwrap ( ) ;
3919
let names: Vec< ( & str , bool ) > = a.peers.iter ( ) .map ( | p | ( p.addr.as_str ( ) , p.complete) ) .collect ( ) ;
3920
assert_eq! ( names, vec! [ ( " 10.0.0.1:7401" , true ) ] , " only A holds it so far" ) ;
6adcd68b Andy McCall 24 days ago ↶
3921
assert_eq! ( a.peers[ 0 ] .cert, " 11" .repeat ( 32 ) , " with the fingerprint peers check it by" ) ;
a46d15e7 Andy McCall 24 days ago ↶
3922
let t = jet:: swarm:: Ticket:: open( & a.ticket, & app.hub_key.identity, oxygenhub:: db:: now_ms( ) as u64 ) .unwrap ( ) ;
3923
assert_eq! ( ( t.name.as_str ( ) , t.site.as_str ( ) ) , ( " b" , " lab" ) ) ;
3924
3925
3926
let ( _ , body) = announce ( " runner-three" , " fetching" ) .await;
3927
let a: flow_core:: api:: Announced = serde_json:: from_str( & body) .unwrap ( ) ;
3928
let mut peers: Vec< ( & str , bool ) > = a.peers.iter ( ) .map ( | p | ( p.addr.as_str ( ) , p.complete) ) .collect ( ) ;
3929
peers.sort ( ) ;
3930
assert_eq! ( peers, vec! [ ( " 10.0.0.1:7401" , true ) , ( " 10.0.0.2:7401" , false ) ] ) ;
3931
3932
3933
announce ( " runner-two" , " done" ) .await;
6adcd68b Andy McCall 24 days ago ↶
3934
claim_as (
3935
" runner-three" ,
3936
" c" ,
3937
serde_json:: json! ( { " images" : [ ] , " swarm" : { " addr" : " 10.0.0.3:7401" , " site" : " elsewhere" , " cert" : " 33" .repeat ( 32 ) } } ) ,
3938
)
3939
.await;
a46d15e7 Andy McCall 24 days ago ↶
3940
let ( _ , body) = announce ( " runner-three" , " fetching" ) .await;
3941
let a: flow_core:: api:: Announced = serde_json:: from_str( & body) .unwrap ( ) ;
3942
assert! ( a.peers.is_empty ( ) , " {:?}" , a.peers) ;
6adcd68b Andy McCall 24 days ago ↶
3943
3944
3945
claim_as ( " runner-secret" , " a" , serde_json:: json! ( { " images" : [ image] , " swarm" : { " addr" : " 10.0.0.1:7401" , " site" : " elsewhere" } } ) )
3946
.await;
3947
let ( st, _ ) = announce ( " runner-secret" , " fetching" ) .await;
3948
assert_eq! ( st, StatusCode:: CONFLICT ) ;
a46d15e7 Andy McCall 24 days ago ↶
3949
3950
3951
claim_as ( " runner-two" , " b" , serde_json:: json! ( { " images" : [ ] } ) ) .await;
3952
let ( st, body) = announce ( " runner-two" , " fetching" ) .await;
3953
assert_eq! ( st, StatusCode:: CONFLICT ) ;
3954
assert! ( body.contains ( " [runner.swarm]" ) , " {body}" ) ;
3955
let ( st, _ ) =
3956
call ( app, " POST" , " /api/v1/flow/swarm/announce" , Some ( " runner-secret" ) , Some ( r # "{"image":"nope","state":"fetching"}"# .into ( ) ) )
3957
.await;
3958
assert_eq! ( st, StatusCode:: BAD_REQUEST ) ;
3959
let ( st, _ ) = call (
3960
app,
3961
" POST" ,
3962
" /api/v1/flow/swarm/announce" ,
3963
None ,
3964
Some ( serde_json:: json! ( { " image" : image, " state" : " fetching" } ) .to_string ( ) ) ,
3965
)
3966
.await;
3967
assert_eq! ( st, StatusCode:: UNAUTHORIZED ) ;
3968
}
6ca0b971 Andy McCall 24 days ago ↶
3969
3970
3971
# [ tokio ::test ( flavor = " multi_thread" ) ]
3972
async fn the_images_page_says_where_each_image_is_and_what_uses_it ( ) {
3973
let h = hub ( ) .await;
3974
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
3975
create ( & h.app, " andy" , " demo" ) .await;
3976
let work = tempfile:: tempdir( ) .unwrap ( ) ;
3977
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
3978
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
3979
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
3980
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
3981
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3982
3983
let held = " cd" .repeat ( 32 ) ;
3984
let kept = " ef" .repeat ( 32 ) ;
3985
let body = serde_json:: json! ( { " runner" : " earth-1" , " runtimes" : [ " wasm" ] , " images" : [ held] , " swarm" : { " addr" : " 10.0.0.1:7401" , " site" : " home" , " cert" : " 11" .repeat ( 32 ) } } ) ;
3986
let ( st, out) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( body.to_string ( ) ) ) .await;
3987
assert! ( st = = StatusCode:: NO_CONTENT | | st = = StatusCode:: OK , " {st} {out}" ) ;
3b1bc27f Andy McCall 20 days ago
3988
sqlx:: query( " INSERT INTO jet_images (hash, size, arch, chunks, uploader, created_ms) VALUES ($1, 1048576, 'x86_64', 9, 'andy', 1)" )
6ca0b971 Andy McCall 24 days ago ↶
3989
.bind ( & kept)
3990
.execute ( & h.app.db)
3991
.await
3992
.unwrap ( ) ;
3993
3994
let res = oxygenhub:: router( h.app.clone ( ) )
3995
.oneshot (
3996
Request:: post( " /register" )
3997
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
c8684d44 Andy McCall 19 days ago
3998
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery&setup=test-setup" ) )
6ca0b971 Andy McCall 24 days ago ↶
3999
.unwrap ( ) ,
4000
)
4001
.await
4002
.unwrap ( ) ;
4003
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
4004
let carol = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
4005
db:: accounts:: set_admin( & h.app.db, carol.id, true ) .await.unwrap ( ) ;
5d66f27c Andy McCall 9 days ago ↶
4006
4007
let lost = jet:: images:: scan( & [ 7 u8 ; 4096 ] [ .. ] , Some ( " x86_64" .into ( ) ) , | _, _| Ok ( ( ) ) ) .unwrap ( ) ;
4008
oxygenhub:: images:: record_peer( & h.app.db, & lost, " mars-1" ) .await.unwrap ( ) ;
6ca0b971 Andy McCall 24 days ago ↶
4009
let res = oxygenhub:: router( h.app.clone ( ) )
4010
.oneshot ( Request:: get( " /admin/runners/images" ) .header ( header:: COOKIE , cookie) .body ( Body:: empty( ) ) .unwrap ( ) )
4011
.await
4012
.unwrap ( ) ;
4013
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
4014
let page = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
4015
let row = | hash : & str | {
4016
let at = page.find ( & format! ( " title=\" {hash} \" " ) ) .unwrap_or_else ( | | panic! ( " {hash} is not on the page" ) ) ;
4017
let end = page[ at.. ] .find ( " </tr>" ) .unwrap ( ) ;
4018
page[ at.. at + end] .to_string ( )
4019
} ;
4020
let pinned = row ( ROOTFS ) ;
4021
assert! ( pinned.contains ( " nowhere" ) & & pinned.contains ( " andy/demo" ) & & pinned.contains ( " 3 jobs" ) , " {pinned}" ) ;
4022
let on_runner = row ( & held) ;
4023
assert! ( on_runner.contains ( " img-holds" ) & & on_runner.contains ( " earth-1" ) & & ! on_runner.contains ( " nowhere" ) , " {on_runner}" ) ;
4024
let on_hub = row ( & kept) ;
4025
assert! ( on_hub.contains ( " img-hub" ) & & on_hub.contains ( " 1.0 MiB" ) & & on_hub.contains ( " x86_64" ) , " {on_hub}" ) ;
5d66f27c Andy McCall 9 days ago ↶
4026
let peer_only = row ( & lost.hash) ;
4027
assert! ( peer_only.contains ( " img-peer" ) & & peer_only.contains ( " sent by mars-1" ) & & peer_only.contains ( " nowhere" ) , " {peer_only}" ) ;
6ca0b971 Andy McCall 24 days ago ↶
4028
assert! ( page.contains ( " earth-1 (home)" ) , " the swarm is named" ) ;
6246d1e5 Andy McCall 22 days ago ↶
4029
4030
assert! ( page.contains ( r # "{"images":"# ) & & page.contains ( r # ""scene":"swarm""# ) & & page.contains ( r # ""earth-1""# ) , " the swarm's data" ) ;
6ca0b971 Andy McCall 24 days ago ↶
4031
assert_eq! ( call ( & h.app, " GET" , " /admin/runners/images" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
4032
}
f677d627 Andy McCall 23 days ago ↶
4033
4034
4035
4036
4037
# [ tokio ::test ( flavor = " multi_thread" ) ]
4038
async fn consumers_of_a_subrepo_are_indexed_with_how_far_behind_they_are ( ) {
4039
let h = hub ( ) .await;
e7158350 Andy McCall 23 days ago ↶
4040
4041
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
f677d627 Andy McCall 23 days ago ↶
4042
create ( & h.app, " andy" , " api" ) .await;
4043
create ( & h.app, " andy" , " game" ) .await;
4044
let api_work = tempfile:: tempdir( ) .unwrap ( ) ;
4045
let api = client ( & format! ( " {} /andy/api" , h.base) , api_work.path ( ) ) ;
4046
write ( api_work.path ( ) , " api.h" , " v1\n " ) ;
4047
api.snapshot ( " t" ) .unwrap ( ) ;
4048
api.commit ( " api v1" , & [ ] ) .unwrap ( ) ;
4049
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4050
4051
let game_work = tempfile:: tempdir( ) .unwrap ( ) ;
4052
let game = client ( & format! ( " {} /andy/game" , h.base) , game_work.path ( ) ) ;
4053
game.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
4054
write ( game_work.path ( ) , " game.c" , " int main;\n " ) ;
4055
game.snapshot ( " t" ) .unwrap ( ) ;
4056
game.commit ( " game on api v1" , & [ ] ) .unwrap ( ) ;
4057
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4058
4059
let consumers = | | async {
4060
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , Some ( " site" ) , None ) .await;
4061
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4062
serde_json:: from_str:: < Vec< serde_json:: Value> > ( & body) .unwrap ( )
4063
} ;
4064
4065
4066
let settle = | want : & 'static ( dyn Fn( & [serde_json:: Value]) -> bool + Sync) | {
4067
let consumers = & consumers;
4068
async move {
4069
for _ in 0 .. 100 {
4070
let list = consumers ( ) .await;
4071
if want ( & list) {
4072
return list;
4073
}
4074
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4075
}
4076
consumers ( ) .await
4077
}
4078
} ;
4079
let list = settle ( & | l| ! l.is_empty ( ) ) .await;
4080
assert_eq! ( list.len ( ) , 1 , " {list:?}" ) ;
4081
assert_eq! (
4082
( list[ 0 ] [ " name" ] .as_str ( ) , list[ 0 ] [ " path" ] .as_str ( ) , list[ 0 ] [ " bookmark" ] .as_str ( ) ) ,
4083
( Some ( " game" ) , Some ( " deps/api" ) , Some ( " main" ) )
4084
) ;
4085
assert_eq! ( list[ 0 ] [ " behind" ] , 0 ) ;
4086
4087
4088
write ( api_work.path ( ) , " api.c" , " v2\n " ) ;
4089
api.snapshot ( " t" ) .unwrap ( ) ;
4090
api.commit ( " api v2" , & [ ] ) .unwrap ( ) ;
4091
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4092
assert_eq! ( settle ( & | l| l.first ( ) .is_some_and ( | c | c[ " behind" ] = = 1 ) ) .await[ 0 ] [ " behind" ] , 1 ) ;
4093
let ( _ , page) = call ( & h.app, " GET" , " /andy/api" , None , None ) .await;
c9aacc0e Andy McCall 23 days ago ↶
4094
assert! ( page.contains ( " Diffuses to" ) & & page.contains ( " andy/game" ) & & page.contains ( " 1 behind" ) , " the page shows it" ) ;
f677d627 Andy McCall 23 days ago ↶
4095
4096
4097
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE name = 'game'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
4098
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , None , None ) .await;
4099
assert_eq! ( body.trim ( ) , " []" ) ;
4100
let ( _ , page) = call ( & h.app, " GET" , " /andy/api" , None , None ) .await;
4101
assert! ( ! page.contains ( " andy/game" ) ) ;
4102
sqlx:: query( " UPDATE repositories SET visibility = 'public' WHERE name = 'game'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
e7158350 Andy McCall 23 days ago ↶
4103
1d634f69 Andy McCall 23 days ago ↶
4104
4105
4106
let game_row = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
4107
let api_row = db:: repos:: get( & h.app.db, " andy" , " api" ) .await.unwrap ( ) .unwrap ( ) ;
4108
let api_heads = api.bookmark_heads ( ) .unwrap ( ) ;
4109
let bumped = oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, api_heads.clone ( ) ) .await.unwrap ( ) ;
4110
let oxygenhub:: diffusion:: Bumped:: Moved { bookmark, heads, .. } = bumped else { panic! ( " {bumped:?}" ) } ;
4111
assert_eq! ( bookmark, " diffusion/deps/api" ) ;
4112
game.fetch ( " origin" ) .unwrap ( ) ;
4113
let bump_state = game.load_state ( & heads) .unwrap ( ) .materialize ( ) ;
4114
let pin = match & bump_state.files[ " deps/api" ] .content {
4115
oxy_core:: state:: FileContent:: Opaque { blobs } => oxy_core:: subrepo:: Pin:: decode( & game.read_blob ( & blobs[ 0 ] ) .unwrap ( ) ) .unwrap ( ) ,
4116
other => panic! ( " {other:?}" ) ,
4117
} ;
4118
assert! (
4119
matches! ( & pin, oxy_core:: subrepo:: Pin:: Oxy { heads, .. } if { let mut a = heads.clone ( ) ; a.sort ( ) ; let mut b = api_heads.clone ( ) ; b.sort ( ) ; a = = b } ) ,
4120
" {pin:?}"
4121
) ;
4122
assert! ( bump_state.files.contains_key ( " game.c" ) , " the rest of the game is as it was" ) ;
9aaa88b2 Andy McCall 23 days ago ↶
4123
assert! ( matches! (
1d634f69 Andy McCall 23 days ago ↶
4124
oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, api_heads.clone ( ) ) .await.unwrap ( ) ,
9aaa88b2 Andy McCall 23 days ago ↶
4125
oxygenhub:: diffusion:: Bumped:: Already { .. }
4126
) ) ;
7a14bdae Andy McCall 23 days ago ↶
4127
4128
4129
1d634f69 Andy McCall 23 days ago ↶
4130
write ( api_work.path ( ) , " other.c" , " v3\n " ) ;
4131
api.snapshot ( " t" ) .unwrap ( ) ;
4132
let v3 = api.commit ( " api v3, on its own" , & [ ] ) .unwrap ( ) ;
4133
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4134
let only_v3 = api.load_state ( & [ v3] ) .unwrap ( ) .heads ( ) .to_vec ( ) ;
7a14bdae Andy McCall 23 days ago ↶
4135
let combined = oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, only_v3) .await.unwrap ( ) ;
4136
let oxygenhub:: diffusion:: Bumped:: Moved { heads: ch, .. } = combined else { panic! ( " {combined:?}" ) } ;
4137
game.fetch ( " origin" ) .unwrap ( ) ;
4138
let st = game.load_state ( & ch) .unwrap ( ) .materialize ( ) ;
4139
let pin = match & st.files[ " deps/api" ] .content {
4140
oxy_core:: state:: FileContent:: Opaque { blobs } => oxy_core:: subrepo:: Pin:: decode( & game.read_blob ( & blobs[ 0 ] ) .unwrap ( ) ) .unwrap ( ) ,
4141
other => panic! ( " {other:?}" ) ,
4142
} ;
4143
let oxy_core:: subrepo:: Pin:: Oxy { heads: pinned, .. } = pin else { panic! ( ) } ;
4144
let all = api.load_state ( & pinned) .unwrap ( ) ;
4145
4146
assert! ( all.contains ( & v3) & & all.len ( ) = = 2 , " v1 and v3, {} changes" , all.len ( ) ) ;
1d634f69 Andy McCall 23 days ago ↶
4147
4148
let newer = api.bookmark_heads ( ) .unwrap ( ) ;
4149
let again = oxygenhub:: diffusion:: bump( & h.app, & game_row, " main" , " deps/api" , & api_row, newer) .await.unwrap ( ) ;
4150
assert! ( matches! ( again, oxygenhub:: diffusion:: Bumped:: Moved { ref bookmark, .. } if bookmark = = " diffusion/deps/api" ) , " {again:?}" ) ;
4151
e7158350 Andy McCall 23 days ago ↶
4152
4153
let put =
4154
| body : & 'static str , who : Option< & 'static str > | call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , who, Some ( body.into ( ) ) ) ;
4155
let ( st, body) = put ( r # "{"follow":"green"}"# , Some ( " site" ) ) .await;
4156
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4157
assert! ( body.contains ( " \" follow\" :\" green\" " ) & & body.contains ( " \" from\" :\" setting\" " ) , " {body}" ) ;
4158
assert_eq! ( put ( r # "{"follow":"sometimes"}"# , Some ( " site" ) ) .await.0 , StatusCode:: BAD_REQUEST ) ;
4159
assert_ne! ( put ( r # "{"follow":"any"}"# , None ) .await.0 , StatusCode:: OK , " not without a token" ) ;
4160
let ( _ , body) = put ( r # "{"follow":"default"}"# , Some ( " site" ) ) .await;
4161
assert! ( body.contains ( " \" follow\" :\" off\" " ) & & body.contains ( " \" from\" :\" site\" " ) , " {body}" ) ;
f677d627 Andy McCall 23 days ago ↶
4162
4163
4164
game.submodule_remove ( " deps/api" ) .unwrap ( ) ;
4165
std:: fs:: remove_dir_all( game_work.path ( ) .join ( " deps/api" ) ) .unwrap ( ) ;
4166
game.snapshot ( " t" ) .unwrap ( ) ;
4167
game.commit ( " game without api" , & [ ] ) .unwrap ( ) ;
4168
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4169
let left = settle ( & | l| l.is_empty ( ) ) .await;
4170
assert! ( left.is_empty ( ) , " {left:?}" ) ;
4171
}
a7e6e8e6 Andy McCall 23 days ago ↶
4172
4173
4174
4175
4176
# [ tokio ::test ( flavor = " multi_thread" ) ]
4177
async fn diffusion_proposes_a_bump_when_the_upstream_moves ( ) {
4178
let h = hub ( ) .await;
4179
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4180
create ( & h.app, " andy" , " api" ) .await;
4181
create ( & h.app, " andy" , " game" ) .await;
180dc43a Andy McCall 23 days ago ↶
4182
4183
let seen: std:: sync:: Mutex< Vec< serde_json:: Value> > = std:: sync:: Mutex:: new( Vec :: new( ) ) ;
4184
let seen = & seen;
4185
let hub_app = & h.app;
4186
let pass_all = move | | async move {
4187
let h_app = hub_app;
a7e6e8e6 Andy McCall 23 days ago ↶
4188
for _ in 0 .. 20 {
180dc43a Andy McCall 23 days ago ↶
4189
let Some ( a) = claim ( h_app) .await else {
a7e6e8e6 Andy McCall 23 days ago ↶
4190
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
180dc43a Andy McCall 23 days ago ↶
4191
if ! jobs_left ( h_app) .await {
a7e6e8e6 Andy McCall 23 days ago ↶
4192
break ;
4193
}
4194
continue ;
4195
} ;
180dc43a Andy McCall 23 days ago ↶
4196
seen.lock ( ) .unwrap ( ) .push ( a.clone ( ) ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
4197
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4198
let ( name, ph) = ( a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
f7bcee49 Andy McCall 22 days ago ↶
4199
let st = finish ( h_app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, 0 ) } ) ) .await;
4200
4201
4202
4203
if st = = StatusCode:: CONFLICT {
4204
let job = db:: flow:: job( & h_app.db, id) .await.unwrap ( ) .unwrap ( ) ;
4205
assert_eq! ( job.status, " cancelled" , " refused only because a newer run cancelled it" ) ;
4206
continue ;
4207
}
4208
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
4209
}
4210
} ;
4211
let api_work = tempfile:: tempdir( ) .unwrap ( ) ;
4212
let api = client ( & format! ( " {} /andy/api" , h.base) , api_work.path ( ) ) ;
4213
write ( api_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4214
write ( api_work.path ( ) , " test.sh" , " echo ok\n " ) ;
4215
write ( api_work.path ( ) , " api.h" , " v1\n " ) ;
4216
api.snapshot ( " t" ) .unwrap ( ) ;
4217
api.commit ( " api v1" , & [ ] ) .unwrap ( ) ;
4218
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4219
pass_all ( ) .await;
4220
4221
let game_work = tempfile:: tempdir( ) .unwrap ( ) ;
4222
let game = client ( & format! ( " {} /andy/game" , h.base) , game_work.path ( ) ) ;
4223
game.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
4224
write ( game_work.path ( ) , " game.c" , " int main;\n " ) ;
f01cb865 Andy McCall 23 days ago ↶
4225
write ( game_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4226
write ( game_work.path ( ) , " test.sh" , " echo ok\n " ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
4227
game.snapshot ( " t" ) .unwrap ( ) ;
4228
game.commit ( " game on api v1" , & [ ] ) .unwrap ( ) ;
4229
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4230
let game_row = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
4231
let bump = | | async {
4232
for _ in 0 .. 100 {
4233
let open = db:: proposals:: list( & h.app.db, game_row.id, & [ " open" ] ) .await.unwrap ( ) ;
4234
if let Some ( p) = open.into_iter ( ) .find ( | p | p.source_bookmark = = " diffusion/deps/api" ) {
4235
return Some ( p) ;
4236
}
4237
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4238
}
4239
None
4240
} ;
4241
4242
4243
write ( api_work.path ( ) , " a2.c" , " v2\n " ) ;
4244
api.snapshot ( " t" ) .unwrap ( ) ;
4245
api.commit ( " api v2: faster sprites" , & [ ] ) .unwrap ( ) ;
4246
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4247
pass_all ( ) .await;
4248
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
4249
assert! ( db:: proposals:: list( & h.app.db, game_row.id, & [ ] ) .await.unwrap ( ) .is_empty ( ) , " off proposes nothing" ) ;
4250
3776b70d Andy McCall 23 days ago ↶
4251
4252
a7e6e8e6 Andy McCall 23 days ago ↶
4253
let put = | body : & 'static str | call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , Some ( body.into ( ) ) ) ;
4254
assert_eq! ( put ( r # "{"follow":"any"}"# ) .await.0 , StatusCode:: OK ) ;
3776b70d Andy McCall 23 days ago ↶
4255
let first = bump ( ) .await.expect ( " caught up when turned on" ) ;
4256
assert! ( first.title.contains ( " 1 change" ) & & first.body.contains ( " api v2: faster sprites" ) , " {:?}" , first.title) ;
f01cb865 Andy McCall 23 days ago ↶
4257
4258
let mut ran = false ;
4259
for _ in 0 .. 100 {
4260
if runs_of ( & h.app, " andy" , " game" ) .await.iter ( ) .any ( | r | r.bookmark = = " diffusion/deps/api" ) {
4261
ran = true ;
4262
break ;
4263
}
4264
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4265
}
4266
assert! ( ran, " a run of the bump" ) ;
4267
pass_all ( ) .await;
a7e6e8e6 Andy McCall 23 days ago ↶
4268
write ( api_work.path ( ) , " a3.c" , " v3\n " ) ;
4269
api.snapshot ( " t" ) .unwrap ( ) ;
4270
api.commit ( " api v3: save slots past ten" , & [ ] ) .unwrap ( ) ;
4271
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
3776b70d Andy McCall 23 days ago ↶
4272
let mut p = first.clone ( ) ;
4273
for _ in 0 .. 100 {
4274
p = bump ( ) .await.unwrap ( ) ;
4275
if p.body.contains ( " api v3" ) {
4276
break ;
4277
}
4278
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4279
}
4280
assert_eq! ( p.number, first.number, " the same proposal" ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
4281
assert_eq! ( ( p.author.as_str ( ) , p.target.as_str ( ) ) , ( " diffusion" , " main" ) ) ;
4282
assert! ( p.title.contains ( " deps/api" ) & & p.title.contains ( " andy/api" ) & & p.title.contains ( " 2 changes" ) , " {}" , p.title) ;
4283
assert! ( p.body.contains ( " api v2: faster sprites" ) & & p.body.contains ( " api v3: save slots past ten" ) , " {}" , p.body) ;
4284
let labels = db:: issues:: proposal_labels( & h.app.db, p.id) .await.unwrap ( ) ;
4285
assert! ( labels.iter ( ) .any ( | l | l.name = = " dependencies" ) ) ;
4286
pass_all ( ) .await;
4287
4288
4289
4290
assert_eq! ( put ( r # "{"follow":"green"}"# ) .await.0 , StatusCode:: OK ) ;
4291
write ( api_work.path ( ) , " a4.c" , " v4\n " ) ;
4292
api.snapshot ( " t" ) .unwrap ( ) ;
4293
api.commit ( " api v4: rsm_set cheaper" , & [ ] ) .unwrap ( ) ;
4294
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4295
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 300 ) ) .await;
4296
assert! ( ! bump ( ) .await.unwrap ( ) .body.contains ( " api v4" ) , " not before its run passes" ) ;
4297
pass_all ( ) .await;
4298
let mut updated = None ;
04683947 Andy McCall 9 days ago ↶
4299
4300
for _ in 0 .. 600 {
a7e6e8e6 Andy McCall 23 days ago ↶
4301
let p2 = bump ( ) .await.unwrap ( ) ;
4302
if p2.body.contains ( " api v4" ) {
4303
updated = Some ( p2) ;
4304
break ;
4305
}
4306
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4307
}
4308
let p2 = updated.expect ( " brought up to date once the run passed" ) ;
4309
assert_eq! ( p2.number, p.number, " the same proposal" ) ;
4310
assert! ( p2.title.contains ( " 3 changes" ) , " {}" , p2.title) ;
4311
assert_eq! ( db:: proposals:: list( & h.app.db, game_row.id, & [ ] ) .await.unwrap ( ) .len ( ) , 1 , " never a pile of bumps" ) ;
3776b70d Andy McCall 23 days ago ↶
4312
757195ad Andy McCall 23 days ago ↶
4313
4314
4315
pass_all ( ) .await;
4316
let ( st, page) = call ( & h.app, " GET" , " /andy/api/diffusion" , None , None ) .await;
4317
assert_eq! ( st, StatusCode:: OK ) ;
c9aacc0e Andy McCall 23 days ago ↶
4318
assert! ( page.contains ( " Diffuses to" ) & & page.contains ( " andy/game" ) & & page.contains ( & format! ( " #{} " , p2.number) ) , " the bump is listed" ) ;
757195ad Andy McCall 23 days ago ↶
4319
assert! ( page.contains ( " df-green" ) , " a passing run of a checked upstream is green" ) ;
6246d1e5 Andy McCall 22 days ago ↶
4320
4321
assert! (
4322
page.contains ( r # ""scene":"ripples""# ) & & page.contains ( r # "["andy/game","# ) & & page.contains ( r # ""repo":"andy/api""# ) ,
4323
" the ripples' data"
4324
) ;
757195ad Andy McCall 23 days ago ↶
4325
let ( _ , page) = call ( & h.app, " GET" , " /andy/game/diffusion" , None , None ) .await;
c9aacc0e Andy McCall 23 days ago ↶
4326
assert! ( page.contains ( " Takes from" ) & & page.contains ( " andy/api" ) ) ;
757195ad Andy McCall 23 days ago ↶
4327
let ( _ , overview) = call ( & h.app, " GET" , " /andy/game" , None , None ) .await;
c9aacc0e Andy McCall 23 days ago ↶
4328
assert! ( overview.contains ( " Takes from" ) & & overview.contains ( " >Diffusion</a>" ) , " the tab and the panel" ) ;
eb2a3502 Andy McCall 23 days ago ↶
4329
4330
let ( st, body) = call ( & h.app, " GET" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , None ) .await;
4331
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4332
let d: hub_api:: Diffusion = serde_json:: from_str( & body) .unwrap ( ) ;
4333
assert! ( d.diffuses_to.is_empty ( ) , " nothing pins the game" ) ;
4334
let pin = d.takes_from.first ( ) .expect ( " the game takes from the api" ) ;
4335
assert_eq! ( ( pin.upstream.as_str ( ) , pin.consumer.as_str ( ) ) , ( " andy/api" , " andy/game" ) ) ;
4336
assert_eq! ( pin.bump, Some ( p2.number) ) ;
4337
assert_eq! ( pin.upstream_state.tone, " green" , " {body}" ) ;
4338
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/diffusion" , Some ( " site" ) , None ) .await;
4339
let d: hub_api:: Diffusion = serde_json:: from_str( & body) .unwrap ( ) ;
4340
assert! ( d.takes_from.is_empty ( ) & & d.diffuses_to.iter ( ) .any ( | p | p.consumer = = " andy/game" ) , " {body}" ) ;
757195ad Andy McCall 23 days ago ↶
4341
let ( st, _ ) = call ( & h.app, " GET" , " /admin/diffusion" , None , None ) .await;
4342
assert_eq! ( st, StatusCode:: NOT_FOUND , " administrators only" ) ;
4343
4344
5d54cf74 Andy McCall 20 days ago ↶
4345
4346
4347
let game = oxygenhub:: db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
4348
oxygenhub:: diffusion:: fail_next_records( & h.app, game.id, 1 ) ;
757195ad Andy McCall 23 days ago ↶
4349
let ( st, body) =
4350
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/game/proposals/{} /land" , p2.number) , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
4351
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4352
let mut behind = serde_json:: Value:: Null;
4353
for _ in 0 .. 100 {
4354
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , Some ( " site" ) , None ) .await;
4355
let list: Vec< serde_json:: Value> = serde_json:: from_str( & list) .unwrap ( ) ;
4356
behind = list.iter ( ) .find ( | c | c[ " name" ] = = " game" ) .map ( | c | c[ " behind" ] .clone ( ) ) .unwrap_or_default ( ) ;
4357
if behind = = 0 {
4358
break ;
4359
}
4360
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4361
}
4362
assert_eq! ( behind, 0 , " the index followed the landing" ) ;
5d54cf74 Andy McCall 20 days ago ↶
4363
assert_eq! ( oxygenhub:: diffusion:: forced_left( & h.app, game.id) , 0 , " the first write did fail" ) ;
0e04dfc2 Andy McCall 20 days ago ↶
4364
4365
4366
4367
let pins_of = | | async {
3b1bc27f Andy McCall 20 days ago
4368
sqlx:: query_scalar:: < _ , String> ( " SELECT heads FROM diffusion_pins WHERE consumer_id = $1" )
0e04dfc2 Andy McCall 20 days ago ↶
4369
.bind ( game.id)
4370
.fetch_one ( & h.app.db)
4371
.await
4372
.unwrap ( )
4373
} ;
4374
let right = pins_of ( ) .await;
3b1bc27f Andy McCall 20 days ago
4375
sqlx:: query( " UPDATE diffusion_pins SET heads = 'stale' WHERE consumer_id = $1" ) .bind ( game.id) .execute ( & h.app.db) .await.unwrap ( ) ;
4376
sqlx:: query( " DELETE FROM diffusion_indexed WHERE consumer_id = $1" ) .bind ( game.id) .execute ( & h.app.db) .await.unwrap ( ) ;
0e04dfc2 Andy McCall 20 days ago ↶
4377
let r = oxygenhub:: diffusion:: reconcile( & h.app) .await;
4378
assert! ( r.reindexed > = 1 , " {r:?}" ) ;
4379
assert_eq! ( pins_of ( ) .await, right, " the pin read again from the game's bookmark" ) ;
4380
assert_eq! ( oxygenhub:: diffusion:: reconcile( & h.app) .await.reindexed, 0 , " nothing left to mend" ) ;
757195ad Andy McCall 23 days ago ↶
4381
180dc43a Andy McCall 23 days ago ↶
4382
4383
4384
sqlx:: query( " UPDATE repositories SET visibility = 'private' WHERE name = 'api'" ) .execute ( & h.app.db) .await.unwrap ( ) ;
4385
seen.lock ( ) .unwrap ( ) .clear ( ) ;
4386
write ( api_work.path ( ) , " a6.c" , " v6\n " ) ;
4387
api.snapshot ( " t" ) .unwrap ( ) ;
4388
api.commit ( " api v6" , & [ ] ) .unwrap ( ) ;
4389
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4fa0da12 Andy McCall 23 days ago ↶
4390
4391
4392
let mut bump_job = None ;
4393
for _ in 0 .. 100 {
4394
pass_all ( ) .await;
4395
bump_job = seen.lock ( ) .unwrap ( ) .iter ( ) .find ( | c | c[ " repo" ] = = " andy/game" & & c[ " bookmark" ] = = " diffusion/deps/api" ) .cloned ( ) ;
4396
if bump_job.is_some ( ) {
4397
break ;
4398
}
4399
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 100 ) ) .await;
4400
}
4401
let bump_job = bump_job.expect ( " the bump's job ran" ) ;
180dc43a Andy McCall 23 days ago ↶
4402
let subs: Vec< & str > = bump_job[ " subrepos" ] .as_array ( ) .unwrap ( ) .iter ( ) .filter_map ( | s | s[ " path" ] .as_str ( ) ) .collect ( ) ;
4403
assert_eq! ( subs, vec! [ " deps/api" ] , " {bump_job}" ) ;
4404
3776b70d Andy McCall 23 days ago ↶
4405
4406
4407
create ( & h.app, " andy" , " game2" ) .await;
4408
let g2_work = tempfile:: tempdir( ) .unwrap ( ) ;
4409
let g2 = client ( & format! ( " {} /andy/game2" , h.base) , g2_work.path ( ) ) ;
4410
g2.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
4411
g2.snapshot ( " t" ) .unwrap ( ) ;
4412
g2.commit ( " game2 on api" , & [ ] ) .unwrap ( ) ;
4413
assert! ( g2.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4414
4415
write ( api_work.path ( ) , " a5.c" , " v5\n " ) ;
4416
api.snapshot ( " t" ) .unwrap ( ) ;
4417
api.commit ( " api v5" , & [ ] ) .unwrap ( ) ;
7e01a766 Andy McCall 20 days ago ↶
4418
let api_row = db:: repos:: get( & h.app.db, " andy" , " api" ) .await.unwrap ( ) .unwrap ( ) ;
4419
let runs_before = db:: flow:: runs( & h.app.db, api_row.id, 100 , None ) .await.unwrap ( ) .len ( ) ;
3776b70d Andy McCall 23 days ago ↶
4420
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
7e01a766 Andy McCall 20 days ago ↶
4421
4422
4423
for _ in 0 .. 300 {
4424
if db:: flow:: runs( & h.app.db, api_row.id, 100 , None ) .await.unwrap ( ) .len ( ) > runs_before {
4425
break ;
4426
}
4427
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 20 ) ) .await;
4428
}
3776b70d Andy McCall 23 days ago ↶
4429
pass_all ( ) .await;
4430
let g2_row = db:: repos:: get( & h.app.db, " andy" , " game2" ) .await.unwrap ( ) .unwrap ( ) ;
4431
for _ in 0 .. 100 {
3b1bc27f Andy McCall 20 days ago
4432
if sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
3776b70d Andy McCall 23 days ago ↶
4433
.bind ( g2_row.id)
4434
.fetch_one ( & h.app.db)
4435
.await
4436
.unwrap ( )
4437
> 0
4438
{
4439
break ;
4440
}
4441
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4442
}
4443
assert! ( db:: proposals:: list( & h.app.db, g2_row.id, & [ ] ) .await.unwrap ( ) .is_empty ( ) , " off until turned on" ) ;
4444
let ( st, _ ) = call ( & h.app, " PUT" , " /api/v1/repos/andy/game2/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green"}"# .into ( ) ) ) .await;
4445
assert_eq! ( st, StatusCode:: OK ) ;
4446
let mut caught = None ;
5d54cf74 Andy McCall 20 days ago ↶
4447
4448
for _ in 0 .. 300 {
3776b70d Andy McCall 23 days ago ↶
4449
if let Some ( p) = db:: proposals:: list( & h.app.db, g2_row.id, & [ " open" ] ) .await.unwrap ( ) .into_iter ( ) .next ( ) {
4450
caught = Some ( p) ;
4451
break ;
4452
}
4453
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4454
}
4455
let caught = caught.expect ( " bumped as soon as it was turned on" ) ;
4456
assert! ( caught.body.contains ( " api v5" ) & & caught.title.contains ( " 1 change" ) , " {:?} {:?}" , caught.title, caught.body) ;
0e04dfc2 Andy McCall 20 days ago ↶
4457
3b1bc27f Andy McCall 20 days ago
4458
sqlx:: query( " UPDATE proposals SET state = 'closed' WHERE id = $1" ) .bind ( caught.id) .execute ( & h.app.db) .await.unwrap ( ) ;
62cdf34d Andy McCall 20 days ago ↶
4459
4460
0e04dfc2 Andy McCall 20 days ago ↶
4461
let r = oxygenhub:: diffusion:: reconcile( & h.app) .await;
62cdf34d Andy McCall 20 days ago ↶
4462
assert_eq! ( r.caught_up, 0 , " the reconciler respects the closed bump: {r:?}" ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
4463
}
4464
4465
4466
async fn jobs_left ( app : & Shared) -> bool {
4467
sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM flow_jobs WHERE status IN ('queued', 'running')" )
4468
.fetch_one ( & app.db)
4469
.await
4470
.unwrap_or ( 0 )
4471
> 0
9aaa88b2 Andy McCall 23 days ago ↶
4472
}
4473
4474
4475
4476
# [ tokio ::test ( flavor = " multi_thread" ) ]
4477
async fn bumps_from_an_upstream_without_checks_are_marked_unverified ( ) {
4478
let h = hub ( ) .await;
4479
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4480
create ( & h.app, " andy" , " lib" ) .await;
4481
create ( & h.app, " andy" , " app" ) .await;
4482
let lib_work = tempfile:: tempdir( ) .unwrap ( ) ;
4483
let lib = client ( & format! ( " {} /andy/lib" , h.base) , lib_work.path ( ) ) ;
4484
write ( lib_work.path ( ) , " lib.h" , " v1\n " ) ;
4485
lib.snapshot ( " t" ) .unwrap ( ) ;
4486
lib.commit ( " lib v1" , & [ ] ) .unwrap ( ) ;
4487
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4488
let app_work = tempfile:: tempdir( ) .unwrap ( ) ;
4489
let app = client ( & format! ( " {} /andy/app" , h.base) , app_work.path ( ) ) ;
4490
app.submodule_add ( & format! ( " {} /andy/lib" , h.base) , " deps/lib" , Some ( " main" ) ) .unwrap ( ) ;
c4e432da Andy McCall 23 days ago ↶
4491
4492
let modules = std:: fs:: read_to_string( app_work.path ( ) .join ( " .oxymodules" ) ) .unwrap ( ) ;
4493
write ( app_work.path ( ) , " .oxymodules" , & format! ( " {modules} watch = lib.c\n " ) ) ;
9aaa88b2 Andy McCall 23 days ago ↶
4494
app.snapshot ( " t" ) .unwrap ( ) ;
4495
app.commit ( " app on lib" , & [ ] ) .unwrap ( ) ;
4496
assert! ( app.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4497
let app_row = db:: repos:: get( & h.app.db, " andy" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
4498
for _ in 0 .. 100 {
3b1bc27f Andy McCall 20 days ago
4499
if sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
9aaa88b2 Andy McCall 23 days ago ↶
4500
.bind ( app_row.id)
4501
.fetch_one ( & h.app.db)
4502
.await
4503
.unwrap ( )
4504
> 0
4505
{
4506
break ;
4507
}
4508
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4509
}
4510
let ( st, _ ) = call ( & h.app, " PUT" , " /api/v1/repos/andy/app/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green"}"# .into ( ) ) ) .await;
4511
assert_eq! ( st, StatusCode:: OK ) ;
4512
write ( lib_work.path ( ) , " lib.c" , " v2\n " ) ;
4513
lib.snapshot ( " t" ) .unwrap ( ) ;
4514
lib.commit ( " lib v2" , & [ ] ) .unwrap ( ) ;
4515
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4516
let mut found = None ;
4517
for _ in 0 .. 100 {
4518
if let Some ( p) = db:: proposals:: list( & h.app.db, app_row.id, & [ " open" ] ) .await.unwrap ( ) .into_iter ( ) .next ( ) {
4519
found = Some ( p) ;
4520
break ;
4521
}
4522
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4523
}
4524
let p = found.expect ( " a bump although the upstream never goes green" ) ;
4525
assert! ( p.body.contains ( " Upstream unverified" ) , " {}" , p.body) ;
c4e432da Andy McCall 23 days ago ↶
4526
assert! ( p.body.contains ( " Touches what you watch" ) & & p.body.contains ( " touches `lib.c`" ) & & p.body.contains ( " ⚑" ) , " {}" , p.body) ;
9aaa88b2 Andy McCall 23 days ago ↶
4527
let mut labels = Vec :: new( ) ;
4528
for _ in 0 .. 100 {
4529
labels = db:: issues:: proposal_labels( & h.app.db, p.id) .await.unwrap ( ) .into_iter ( ) .map ( | l | l.name ) .collect:: < Vec< String> > ( ) ;
4530
if labels.len ( ) = = 2 {
4531
break ;
4532
}
4533
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4534
}
4535
assert! ( labels.contains ( & " upstream unverified" .to_string ( ) ) & & labels.contains ( & " dependencies" .to_string ( ) ) , " {labels:?}" ) ;
a7e6e8e6 Andy McCall 23 days ago ↶
4536
}
d943160d Andy McCall 23 days ago ↶
4537
4538
4539
4540
# [ tokio ::test ( flavor = " multi_thread" ) ]
4541
async fn a_moved_target_runs_its_open_proposals_again ( ) {
4542
let h = hub ( ) .await;
4543
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4544
create ( & h.app, " andy" , " demo" ) .await;
4545
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4546
let mut c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
4547
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4548
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
4549
c.snapshot ( " t" ) .unwrap ( ) ;
4550
let base = c.commit ( " base" , & [ ] ) .unwrap ( ) ;
4551
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4552
4553
for ( b, file) in [ ( " fix-a" , " a.txt" ) , ( " fix-b" , " b.txt" ) ] {
4554
c.bookmark_create ( b, Some ( & [ base] ) ) .unwrap ( ) ;
4555
c.bookmark_switch ( b, true ) .unwrap ( ) ;
4556
write ( work.path ( ) , file, " x\n " ) ;
4557
c.snapshot ( " t" ) .unwrap ( ) ;
4558
c.commit ( & format! ( " add {file} " ) , & [ ] ) .unwrap ( ) ;
4559
assert! ( c.push_bookmark ( " origin" , b, false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4560
let body = serde_json:: json! ( { " title" : b, " target" : " main" , " source" : b } ) .to_string ( ) ;
4561
let ( st, out) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/proposals" , Some ( " site" ) , Some ( body) ) .await;
4562
assert_eq! ( st, StatusCode:: CREATED , " {out}" ) ;
4563
}
4564
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
4565
let props = db:: proposals:: list( & h.app.db, row.id, & [ " open" ] ) .await.unwrap ( ) ;
4566
let a = props.iter ( ) .find ( | p | p.source_bookmark = = " fix-a" ) .unwrap ( ) .number;
4567
let b = props.iter ( ) .find ( | p | p.source_bookmark = = " fix-b" ) .unwrap ( ) .number;
4568
4569
let ( st, out) = call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/demo/proposals/{a} /land" ) , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
4570
assert_eq! ( st, StatusCode:: OK , " {out}" ) ;
4571
let label = format! ( " proposal/{b} " ) ;
4572
let mut found = None ;
4573
for _ in 0 .. 100 {
4574
if let Some ( r) = runs_of ( & h.app, " andy" , " demo" ) .await.into_iter ( ) .find ( | r | r.bookmark = = label ) {
4575
found = Some ( r) ;
4576
break ;
4577
}
4578
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4579
}
4580
let run = found.expect ( " a run of fix-b's new projected state" ) ;
4581
let projected = db:: proposals:: get( & h.app.db, row.id, b) .await.unwrap ( ) .unwrap ( ) ;
4582
assert_eq! ( ( run.trigger.as_str ( ) , run.state.as_str ( ) ) , ( " proposal" , projected.projected_state.as_str ( ) ) , " on its projected state" ) ;
4583
4584
let before = runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) ;
4585
oxygenhub:: web:: proposals:: settle_after_push( & h.app, & row, " main" , " andy" ) .await;
4586
assert_eq! ( runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) , before, " not again for the same state" ) ;
4587
}
93501da2 Andy McCall 22 days ago ↶
4588
4589
4590
4591
async fn send_image ( app : & Shared, image : & [u8 ]) -> ( String, serde_json:: Value) {
4592
let mut chunks = std:: collections:: HashMap:: new( ) ;
4593
let m = jet:: images:: scan( image, None , | c , b | {
4594
chunks.insert ( c.hash ( ) .to_string ( ) , b.to_vec ( ) ) ;
4595
Ok ( ( ) )
4596
} )
4597
.unwrap ( ) ;
4598
for c in m.needed ( ) {
4599
let ( st, body) = put_bytes ( app, & format! ( " /api/v1/flow/images/chunks/{c} " ) , " site" , chunks[ c] .clone ( ) ) .await;
4600
assert_eq! ( st, StatusCode:: NO_CONTENT , " {body}" ) ;
4601
}
4602
let ( st, body) =
4603
call ( app, " PUT" , & format! ( " /api/v1/flow/images/{} " , m.hash) , Some ( " site" ) , Some ( serde_json:: to_string( & m) .unwrap ( ) ) ) .await;
4604
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
4605
( m.hash.clone ( ) , serde_json:: from_str( & body) .unwrap ( ) )
4606
}
4607
4608
4609
4610
4611
4612
# [ tokio ::test ( flavor = " multi_thread" ) ]
4613
async fn an_image_names_its_own_architecture_and_a_pass_teaches_the_rest ( ) {
4614
use oxy_core:: identity:: SecretKey;
4615
let h = hub ( ) .await;
4616
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4617
4618
let dir = tempfile:: tempdir( ) .unwrap ( ) ;
4619
let tree = dir.path ( ) .join ( " tree" ) ;
4620
std:: fs:: create_dir_all( tree.join ( " bin" ) ) .unwrap ( ) ;
4621
let mut elf = vec! [ 0 u8 ; 64 ] ;
4622
elf[ .. 7 ] .copy_from_slice ( b " \x7f ELF\x02 \x01 \x01 " ) ;
4623
elf[ 18 .. 20 ] .copy_from_slice ( & 0x3E u16 .to_le_bytes ( ) ) ;
4624
std:: fs:: write( tree.join ( " bin/sh" ) , & elf) .unwrap ( ) ;
4625
let img = dir.path ( ) .join ( " rootfs.ext4" ) ;
4626
let made = std:: process:: Command:: new( " mkfs.ext4" ) .args ( [ " -q" , " -F" , " -d" ] ) .arg ( & tree) .arg ( & img) .arg ( " 8M" ) .status ( ) ;
4627
if ! made.is_ok_and ( | s | s.success ( ) ) {
4628
eprintln! ( " mkfs.ext4 not found: skipped" ) ;
4629
return ;
4630
}
4631
let ( x86, answer) = send_image ( & h.app, & std:: fs:: read( & img) .unwrap ( ) ) .await;
4632
assert_eq! ( answer[ " arch" ] , " x86_64" , " read from its shell: {answer}" ) ;
4633
let ( _ , body) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/images/{x86} " ) , Some ( " runner-secret" ) , None ) .await;
4634
assert_eq! ( serde_json:: from_str:: < jet:: images:: Manifest> ( & body) .unwrap ( ) .arch.as_deref ( ) , Some ( " x86_64" ) , " runners fetch it named" ) ;
4635
4636
let blob: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i % 241 ) as u8 ) .collect ( ) ;
4637
let ( unread, answer) = send_image ( & h.app, & blob) .await;
4638
assert! ( answer[ " arch" ] .is_null ( ) , " {answer}" ) ;
4639
let archs = db:: flow:: image_archs( & h.app.db) .await.unwrap ( ) ;
4640
assert_eq! ( ( archs.get ( & x86) .map ( String :: as_str) , archs.get ( & unread) ) , ( Some ( " x86_64" ) , None ) ) ;
4641
4642
sqlx:: query( " UPDATE jet_images SET arch = ''" ) .execute ( & h.app.db) .await.unwrap ( ) ;
4643
sqlx:: query( " DELETE FROM flow_images" ) .execute ( & h.app.db) .await.unwrap ( ) ;
4644
assert_eq! ( oxygenhub:: images:: learn_archs( & h.app) .await.unwrap ( ) , 1 ) ;
4645
assert_eq! ( db:: flow:: image_archs( & h.app.db) .await.unwrap ( ) .get ( & x86) .map ( String :: as_str) , Some ( " x86_64" ) ) ;
4646
4647
4648
create ( & h.app, " andy" , " arch" ) .await;
4649
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4650
let c = client ( & format! ( " {} /andy/arch" , h.base) , work.path ( ) ) ;
4651
let kdl = format! (
4652
" schema-version 1\n sandbox \" unread\" {{ \n rootfs \" cas://blob/{unread} \" \n }} \n sandbox \" x86\" {{ \n rootfs \" cas://blob/{x86} \" \n }} \n \
4653
job \" first\" {{ \n sandbox \" unread\" \n exec \" make\" \n }} \n job \" second\" {{ \n sandbox \" x86\" \n exec \" make\" \n depends-on \" first\" \n }} \n \
4654
job \" third\" {{ \n sandbox \" unread\" \n exec \" make\" \n depends-on \" first\" \n }} \n "
4655
) ;
4656
write ( work.path ( ) , " .flow/pipeline.kdl" , & kdl) ;
4657
c.commit ( " Jobs on two images" , & [ ] ) .unwrap ( ) ;
4658
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4659
runs_of ( & h.app, " andy" , " arch" ) .await;
4660
4661
4662
let x86_info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
4663
let x86_claim =
4664
serde_json:: json! ( { " runner" : " earth" , " runtimes" : [ " microvm" ] , " images" : [ ] , " fetches_images" : true , " info" : x86_info } ) ;
4665
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( x86_claim.to_string ( ) ) ) .await;
4666
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4667
let first: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
4668
assert_eq! ( first[ " job" ] [ " name" ] , " first" ) ;
4669
let ph = first[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) ;
4670
let id = first[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4671
assert_eq! (
4672
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( " first" , ph, 0 ) } ) ) .await,
4673
StatusCode:: NO_CONTENT
4674
) ;
4675
assert_eq! ( db:: flow:: image_archs( & h.app.db) .await.unwrap ( ) .get ( & unread) .map ( String :: as_str) , Some ( " x86_64" ) , " learned" ) ;
4676
4677
4678
let token = " flowreg_narvi" ;
4679
db:: runners:: add_registration( & h.app.db, & blake3:: hash( token.as_bytes ( ) ) .to_hex ( ) , " admin" , & Default :: default( ) ) .await.unwrap ( ) ;
4680
let identity = SecretKey:: generate( ) .identity ( " narvi" , " " , 1 ) ;
4681
let info = serde_json:: json! ( { " arch" : " riscv64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
4682
let reg = serde_json:: json! ( { " token" : token, " name" : " narvi" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
4683
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
4684
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4685
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4686
let narvi =
4687
serde_json:: json! ( { " runner" : " narvi" , " runtimes" : [ " microvm" ] , " images" : [ ] , " fetches_images" : true , " info" : info } ) .to_string ( ) ;
4688
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( narvi) ) .await;
4689
assert_eq! ( st, StatusCode:: NO_CONTENT , " the second job's image is x86_64 and the third's learned so: {body}" ) ;
4690
for _ in 0 .. 2 {
4691
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( x86_claim.to_string ( ) ) ) .await;
4692
assert_eq! ( st, StatusCode:: OK , " both go to x86_64: {body}" ) ;
4693
}
4694
}
f4d6ef41 Andy McCall 22 days ago ↶
4695
4696
4697
4698
4699
4700
# [ tokio ::test ( flavor = " multi_thread" ) ]
4701
async fn a_consumer_that_chose_auto_lands_green_bumps_itself ( ) {
4702
let h = hub ( ) .await;
4703
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4704
create ( & h.app, " andy" , " api" ) .await;
4705
create ( & h.app, " andy" , " game" ) .await;
4706
let hub_app = & h.app;
4707
4708
let pass_all = move | | async move {
4709
for _ in 0 .. 20 {
4710
let Some ( a) = claim ( hub_app) .await else {
4711
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4712
if ! jobs_left ( hub_app) .await {
4713
break ;
4714
}
4715
continue ;
4716
} ;
4717
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4718
let ( name, ph) = ( a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4719
assert_eq! (
4720
finish ( hub_app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, 0 ) } ) ) .await,
4721
StatusCode:: NO_CONTENT
4722
) ;
4723
}
4724
} ;
4725
let api_work = tempfile:: tempdir( ) .unwrap ( ) ;
4726
let api = client ( & format! ( " {} /andy/api" , h.base) , api_work.path ( ) ) ;
4727
write ( api_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4728
write ( api_work.path ( ) , " test.sh" , " echo ok\n " ) ;
4729
api.snapshot ( " t" ) .unwrap ( ) ;
4730
api.commit ( " api v1" , & [ ] ) .unwrap ( ) ;
4731
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4732
pass_all ( ) .await;
4733
let game_work = tempfile:: tempdir( ) .unwrap ( ) ;
4734
let game = client ( & format! ( " {} /andy/game" , h.base) , game_work.path ( ) ) ;
4735
game.submodule_add ( & format! ( " {} /andy/api" , h.base) , " deps/api" , Some ( " main" ) ) .unwrap ( ) ;
4736
write ( game_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4737
write ( game_work.path ( ) , " test.sh" , " echo ok\n " ) ;
4738
game.snapshot ( " t" ) .unwrap ( ) ;
4739
game.commit ( " game on api v1" , & [ ] ) .unwrap ( ) ;
4740
assert! ( game.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4741
pass_all ( ) .await;
4742
let game_row = db:: repos:: get( & h.app.db, " andy" , " game" ) .await.unwrap ( ) .unwrap ( ) ;
4743
for _ in 0 .. 100 {
3b1bc27f Andy McCall 20 days ago
4744
let n: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
f4d6ef41 Andy McCall 22 days ago ↶
4745
.bind ( game_row.id)
4746
.fetch_one ( & h.app.db)
4747
.await
4748
.unwrap ( ) ;
4749
if n > 0 {
4750
break ;
4751
}
4752
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4753
}
4754
let ( st, body) =
4755
call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green","land":"auto"}"# .into ( ) ) ) .await;
4756
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4757
assert! ( body.contains ( r # ""land":"auto""# ) , " {body}" ) ;
4758
let ( st, _ ) =
4759
call ( & h.app, " PUT" , " /api/v1/repos/andy/game/diffusion" , Some ( " site" ) , Some ( r # "{"follow":"green","land":"soon"}"# .into ( ) ) ) .await;
4760
assert_eq! ( st, StatusCode:: BAD_REQUEST ) ;
4761
4762
4763
4764
write ( api_work.path ( ) , " a2.c" , " v2\n " ) ;
4765
api.snapshot ( " t" ) .unwrap ( ) ;
4766
api.commit ( " api v2: faster sprites" , & [ ] ) .unwrap ( ) ;
4767
assert! ( api.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4768
let mut landed = None ;
4769
for _ in 0 .. 100 {
4770
pass_all ( ) .await;
4771
let all = db:: proposals:: list( & h.app.db, game_row.id, & [ ] ) .await.unwrap ( ) ;
4772
if let Some ( p) = all.into_iter ( ) .find ( | p | p.source_bookmark = = " diffusion/deps/api" & & p.state = = " landed" ) {
4773
landed = Some ( p) ;
4774
break ;
4775
}
4776
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4777
}
4778
let p = landed.expect ( " landed by itself once both halves were green" ) ;
4779
assert! ( p.body.contains ( " lands its bumps itself" ) , " {}" , p.body) ;
a2ef53c5 Andy McCall 20 days ago ↶
4780
4781
let mut events = vec! [ ] ;
4782
for _ in 0 .. 100 {
4783
events = db:: proposals:: events( & h.app.db, p.id) .await.unwrap ( ) ;
4784
if events.iter ( ) .any ( | e | e.event = = " landed" ) {
4785
break ;
4786
}
4787
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 20 ) ) .await;
4788
}
f4d6ef41 Andy McCall 22 days ago ↶
4789
let landing = events.iter ( ) .find ( | e | e.event = = " landed" ) .expect ( " a landing on the timeline" ) ;
4790
assert_eq! ( landing.actor, " diffusion" ) ;
4791
assert! ( landing.detail.contains ( " Diffusion, both halves green" ) , " {}" , landing.detail) ;
4792
4793
let mut behind = serde_json:: Value:: Null;
4794
for _ in 0 .. 100 {
4795
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/api/consumers" , Some ( " site" ) , None ) .await;
4796
let list: Vec< serde_json:: Value> = serde_json:: from_str( & list) .unwrap ( ) ;
4797
behind = list.iter ( ) .find ( | c | c[ " name" ] = = " game" ) .map ( | c | c[ " behind" ] .clone ( ) ) .unwrap_or_default ( ) ;
4798
if behind = = 0 {
4799
break ;
4800
}
4801
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4802
}
4803
assert_eq! ( behind, 0 , " main moved to the bump" ) ;
4804
4805
4806
4807
create ( & h.app, " andy" , " lib" ) .await;
4808
create ( & h.app, " andy" , " app" ) .await;
4809
let lib_work = tempfile:: tempdir( ) .unwrap ( ) ;
4810
let lib = client ( & format! ( " {} /andy/lib" , h.base) , lib_work.path ( ) ) ;
4811
write ( lib_work.path ( ) , " lib.h" , " v1\n " ) ;
4812
lib.snapshot ( " t" ) .unwrap ( ) ;
4813
lib.commit ( " lib v1" , & [ ] ) .unwrap ( ) ;
4814
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4815
let app_work = tempfile:: tempdir( ) .unwrap ( ) ;
4816
let app = client ( & format! ( " {} /andy/app" , h.base) , app_work.path ( ) ) ;
4817
app.submodule_add ( & format! ( " {} /andy/lib" , h.base) , " deps/lib" , Some ( " main" ) ) .unwrap ( ) ;
4818
let modules = std:: fs:: read_to_string( app_work.path ( ) .join ( " .oxymodules" ) ) .unwrap ( ) ;
4819
write ( app_work.path ( ) , " .oxymodules" , & format! ( " {modules} follow = green\n land = auto\n " ) ) ;
4820
write ( app_work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4821
write ( app_work.path ( ) , " test.sh" , " echo ok\n " ) ;
4822
app.snapshot ( " t" ) .unwrap ( ) ;
4823
app.commit ( " app on lib" , & [ ] ) .unwrap ( ) ;
4824
assert! ( app.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4825
pass_all ( ) .await;
4826
let app_row = db:: repos:: get( & h.app.db, " andy" , " app" ) .await.unwrap ( ) .unwrap ( ) ;
4827
for _ in 0 .. 100 {
3b1bc27f Andy McCall 20 days ago
4828
let n: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1" )
f4d6ef41 Andy McCall 22 days ago ↶
4829
.bind ( app_row.id)
4830
.fetch_one ( & h.app.db)
4831
.await
4832
.unwrap ( ) ;
4833
if n > 0 {
4834
break ;
4835
}
4836
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
4837
}
4838
write ( lib_work.path ( ) , " lib.c" , " v2\n " ) ;
4839
lib.snapshot ( " t" ) .unwrap ( ) ;
4840
lib.commit ( " lib v2" , & [ ] ) .unwrap ( ) ;
4841
assert! ( lib.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4842
let mut passed = false ;
4843
for _ in 0 .. 100 {
4844
pass_all ( ) .await;
4845
let s: Option< String> = sqlx:: query_scalar(
3b1bc27f Andy McCall 20 days ago
4846
" SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = 'diffusion/deps/lib' ORDER BY id DESC LIMIT 1" ,
f4d6ef41 Andy McCall 22 days ago ↶
4847
)
4848
.bind ( app_row.id)
4849
.fetch_optional ( & h.app.db)
4850
.await
4851
.unwrap ( ) ;
4852
if s.as_deref ( ) = = Some ( " passed" ) {
4853
passed = true ;
4854
break ;
4855
}
4856
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
4857
}
4858
assert! ( passed, " the app's run on the bump passed" ) ;
4859
assert_eq! ( oxygenhub:: diffusion:: auto_land( & h.app, & app_row, " deps/lib" ) .await, Err ( oxygenhub:: diffusion:: Held:: Unverified) ) ;
4860
let open = db:: proposals:: list( & h.app.db, app_row.id, & [ " open" ] ) .await.unwrap ( ) ;
4861
assert! ( open.iter ( ) .any ( | p | p.source_bookmark = = " diffusion/deps/lib" ) , " amber waits for a person" ) ;
4862
}
e8fa5369 Andy McCall 22 days ago ↶
4863
4864
4865
4866
4867
4868
# [ tokio ::test ( flavor = " multi_thread" ) ]
4869
async fn one_job_reruns_with_what_depends_on_it ( ) {
4870
let h = hub ( ) .await;
4871
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4872
create ( & h.app, " andy" , " flaky" ) .await;
4873
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4874
let c = client ( & format! ( " {} /andy/flaky" , h.base) , work.path ( ) ) ;
4875
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4876
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
4877
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
4878
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4879
let run = runs_of ( & h.app, " andy" , " flaky" ) .await[ 0 ] .clone ( ) ;
4880
4881
let ( st, _ ) =
4882
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/test/rerun" , run.number) , Some ( " site" ) , None ) .await;
4883
assert_eq! ( st, StatusCode:: CONFLICT , " not while the run is live" ) ;
4884
while let Some ( j) = claim ( & h.app) .await {
4885
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4886
let ( name, ph) = ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4887
let exit = if name = = " test" { 1 } else { 0 } ;
d9b3b846 Andy McCall 22 days ago ↶
4888
let log = serde_json:: json! ( { " records" : [
4889
{ " t" : 1 , " stream" : " out" , " text" : format! ( " {name} ran" ) } ,
4890
{ " t" : 2 , " stream" : " out" , " text" : " ::group::compile" } ,
4891
{ " t" : 5 , " stream" : " out" , " text" : " cc a.c" } ,
4892
{ " t" : 1502 , " stream" : " out" , " text" : " ::endgroup::" } ,
4893
] } ) ;
e8fa5369 Andy McCall 22 days ago ↶
4894
call ( & h.app, " POST" , & format! ( " /api/v1/flow/jobs/{id} /log" ) , Some ( " runner-secret" ) , Some ( log.to_string ( ) ) ) .await;
4895
assert_eq! (
4896
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : exit, " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, exit as u32 ) } ) )
4897
.await,
4898
StatusCode:: NO_CONTENT
4899
) ;
4900
}
d9b3b846 Andy McCall 22 days ago ↶
4901
4902
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/flaky/flow/{} " , run.number) , None , None ) .await;
4903
assert! ( page.contains ( r # "<details class="log-group" data-from="2">"# ) , " {page}" ) ;
4904
assert! ( page.contains ( r # "compile <span class="took">1.5s</span></summary>"# ) & & ! page.contains ( " ::endgroup::" ) , " {page}" ) ;
e8fa5369 Andy McCall 22 days ago ↶
4905
let status = | n : & 'static str | {
4906
let ( app, run_id) = ( h.app.clone ( ) , run.id) ;
4907
async move { db:: flow:: jobs( & app.db, run_id) .await.unwrap ( ) .into_iter ( ) .find ( | j | j.name = = n ) .unwrap ( ) }
4908
} ;
4909
assert_eq! ( db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) .status, " failed" ) ;
4910
let failed_test = status ( " test" ) .await;
4911
4912
4913
let ( st, body) =
4914
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/test/rerun" , run.number) , Some ( " site" ) , None ) .await;
4915
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
4916
assert! ( body.contains ( r # ""queued":1"# ) , " {body}" ) ;
4917
assert_eq! (
4918
( status ( " build" ) .await.status, status ( " lint" ) .await.status, status ( " test" ) .await.status) ,
4919
( " passed" .into ( ) , " passed" .into ( ) , " queued" .into ( ) )
4920
) ;
4921
assert! ( db:: flow:: log( & h.app.db, failed_test.id, 0 , 100 ) .await.unwrap ( ) .is_empty ( ) , " its old log is gone" ) ;
4922
assert! ( db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) .live ( ) ) ;
4923
let checks = checks_when ( & h.app, run.repo_id, & run.state, 3 ) .await;
4924
assert_eq! ( checks.iter ( ) .find ( | c | c.name = = " flow/test" ) .map ( | c | c.status.as_str ( ) ) , Some ( " queued" ) ) ;
4925
let j = claim ( & h.app) .await.expect ( " the test, queued again" ) ;
4926
assert_eq! ( j[ " job" ] [ " name" ] , " test" ) ;
4927
let ph = j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
4928
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4929
assert_eq! (
4930
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( " test" , & ph, 0 ) } ) ) .await,
4931
StatusCode:: NO_CONTENT
4932
) ;
4933
assert_eq! ( db:: flow:: run_by_id( & h.app.db, run.id) .await.unwrap ( ) .unwrap ( ) .status, " passed" , " the run is whole again" ) ;
bff9106e Andy McCall 22 days ago ↶
4934
4935
let andy_id = db:: accounts:: account_by_name( & h.app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) .id;
4936
let kinds: Vec< String> = db:: notify:: inbox( & h.app.db, andy_id, 20 ) .await.unwrap ( ) .into_iter ( ) .map ( | n | n.kind ) .collect ( ) ;
4937
assert! ( kinds.contains ( & " flow.failed" .to_string ( ) ) & & kinds.contains ( & " flow.fixed" .to_string ( ) ) , " {kinds:?}" ) ;
e8fa5369 Andy McCall 22 days ago ↶
4938
4939
4940
let ( _ , body) =
4941
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/build/rerun" , run.number) , Some ( " site" ) , None ) .await;
4942
assert! ( body.contains ( r # ""queued":2"# ) , " {body}" ) ;
4943
assert_eq! (
4944
( status ( " build" ) .await.status, status ( " lint" ) .await.status, status ( " test" ) .await.status) ,
4945
( " queued" .into ( ) , " passed" .into ( ) , " queued" .into ( ) )
4946
) ;
4947
let ( st, _ ) =
4948
call ( & h.app, " POST" , & format! ( " /api/v1/repos/andy/flaky/flow/runs/{} /jobs/deploy/rerun" , run.number) , Some ( " site" ) , None ) .await;
4949
assert_eq! ( st, StatusCode:: CONFLICT , " live again, and no such job either way" ) ;
4950
}
4a4400c5 Andy McCall 22 days ago ↶
4951
4952
4953
4954
4955
4956
# [ tokio ::test ( flavor = " multi_thread" ) ]
4957
async fn failed_runs_are_told_and_an_erroring_runner_is_drained ( ) {
4958
let h = hub ( ) .await;
4959
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
4960
let bob = db:: accounts:: create_account( & h.app.db, " bob" , " [email protected] " , " x" , " salt" , false ) .await.unwrap ( ) ;
4961
create ( & h.app, " andy" , " alerts" ) .await;
4962
let row = db:: repos:: get( & h.app.db, " andy" , " alerts" ) .await.unwrap ( ) .unwrap ( ) ;
4963
db:: notify:: set_watch( & h.app.db, bob.id, row.id, true ) .await.unwrap ( ) ;
4964
let work = tempfile:: tempdir( ) .unwrap ( ) ;
4965
let c = client ( & format! ( " {} /andy/alerts" , h.base) , work.path ( ) ) ;
4966
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
4967
write ( work.path ( ) , " test.sh" , " exit 1\n " ) ;
4968
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
4969
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
4970
let run = runs_of ( & h.app, " andy" , " alerts" ) .await[ 0 ] .clone ( ) ;
3b1bc27f Andy McCall 20 days ago
4971
sqlx:: query( " UPDATE flow_runs SET actor = 'andy' WHERE id = $1" ) .bind ( run.id) .execute ( & h.app.db) .await.unwrap ( ) ;
4a4400c5 Andy McCall 22 days ago ↶
4972
while let Some ( j) = claim ( & h.app) .await {
4973
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4974
let ( name, ph) = ( j[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
4975
let exit = if name = = " test" { 1 } else { 0 } ;
4976
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : exit, " duration_ms" : 10 , " receipt" : receipt ( & name, & ph, exit as u32 ) } ) ) .await;
4977
}
4978
let told = | id : i64 | {
4979
let db = h.app.db.clone ( ) ;
4980
async move { db:: notify:: inbox( & db, id, 20 ) .await.unwrap ( ) .into_iter ( ) .filter ( | n | n.kind = = " flow.failed" ) .collect:: < Vec< _ > > ( ) }
4981
} ;
4982
let ( for_andy, for_bob) = ( told ( andy.id) .await, told ( bob.id) .await) ;
4983
assert_eq! ( ( for_andy.len ( ) , for_bob.len ( ) ) , ( 1 , 1 ) , " the pusher and a watcher" ) ;
4984
assert! ( for_andy[ 0 ] .title.contains ( & format! ( " run #{} on main failed (test)" , run.number) ) , " {}" , for_andy[ 0 ] .title) ;
4985
assert_eq! ( for_andy[ 0 ] .url, format! ( " /andy/alerts/flow/{} " , run.number) ) ;
4986
4987
4988
let runner_id = | | {
4989
let db = h.app.db.clone ( ) ;
4990
async move { sqlx:: query_scalar:: < _ , i64 > ( " SELECT id FROM flow_runners WHERE name = 'test-1'" ) .fetch_one ( & db) .await.unwrap ( ) }
4991
} ;
4992
let mut errors = 0 ;
4993
while errors < 5 {
4994
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/repos/andy/alerts/flow/runs" , Some ( " site" ) , Some ( " {}" .into ( ) ) ) .await;
4995
assert_eq! ( st, StatusCode:: CREATED ) ;
4996
while let Some ( j) = claim ( & h.app) .await {
4997
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
4998
let st = finish (
4999
& h.app,
5000
id,
5001
serde_json:: json! ( { " exit_code" : null, " duration_ms" : 10 , " error" : " the microVM did not start: /dev/kvm busy" } ) ,
5002
)
5003
.await;
5004
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
5005
errors + = 1 ;
5006
if errors = = 5 {
5007
break ;
5008
}
5009
}
5010
}
5011
let state = | id : i64 | {
5012
let db = h.app.db.clone ( ) ;
5013
async move { db:: runners:: get( & db, id) .await.unwrap ( ) .unwrap ( ) .state }
5014
} ;
5015
let id = runner_id ( ) .await;
5016
assert_eq! ( state ( id) .await, " draining" ) ;
5017
let drained: Vec< _ > =
5018
db:: notify:: inbox( & h.app.db, andy.id, 50 ) .await.unwrap ( ) .into_iter ( ) .filter ( | n | n.kind = = " flow.runner_drained" ) .collect ( ) ;
5019
assert_eq! ( drained.len ( ) , 1 , " the administrator is told" ) ;
5020
assert! ( drained[ 0 ] .title.contains ( " /dev/kvm busy" ) , " {}" , drained[ 0 ] .title) ;
5021
assert! (
5022
! told ( bob.id) .await.is_empty ( )
5023
& & db:: notify:: inbox( & h.app.db, bob.id, 50 ) .await.unwrap ( ) .iter ( ) .all ( | n | n.kind ! = " flow.runner_drained" )
5024
) ;
5025
5026
db:: runners:: set_state( & h.app.db, id, " active" ) .await.unwrap ( ) ;
5027
assert_eq! ( db:: runners:: erroring( & h.app.db, id, 5 ) .await.unwrap ( ) , None ) ;
f7bcee49 Andy McCall 22 days ago ↶
5028
}
5029
5030
5031
5032
# [ tokio ::test ( flavor = " multi_thread" ) ]
5033
async fn a_landed_proposal_runs_its_targets_pipeline ( ) {
5034
let h = hub ( ) .await;
5035
create ( & h.app, " andy" , " demo" ) .await;
5036
let url = format! ( " {} /andy/demo" , h.base) ;
5037
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5038
let mut c = client ( & url, work.path ( ) ) ;
5039
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
5040
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
5041
c.snapshot ( " test" ) .unwrap ( ) ;
5042
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
5043
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5044
assert_eq! ( runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) , 1 ) ;
5045
c.bookmark_create ( " feature" , None ) .unwrap ( ) ;
5046
c.bookmark_switch ( " feature" , false ) .unwrap ( ) ;
5047
write ( work.path ( ) , " b.txt" , " more\n " ) ;
5048
c.snapshot ( " test" ) .unwrap ( ) ;
5049
c.commit ( " More" , & [ ] ) .unwrap ( ) ;
5050
assert! ( c.push_bookmark ( " origin" , " feature" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5051
let runs_before =
5052
db:: flow:: runs( & h.app.db, db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) .id, 10 , None ) .await.unwrap ( ) .len ( ) ;
5053
5054
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
5055
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
5056
db:: proposals:: create(
5057
& h.app.db,
5058
db:: proposals:: NewProposal {
5059
repo_id: row.id,
5060
title: " More" ,
5061
body: " " ,
5062
author_id: Some ( andy.id) ,
5063
author: " andy" ,
5064
draft: false ,
5065
target: " main" ,
5066
source_kind: " bookmark" ,
5067
source_repo_id: Some ( row.id) ,
5068
source_bookmark: " feature" ,
5069
source_changes: " " ,
5070
} ,
5071
)
5072
.await
5073
.unwrap ( ) ;
5074
let repo = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
2a8b6216 Andy McCall 18 days ago ↶
5075
let state = oxygenhub:: web:: proposals:: land_as( & h.app, & repo, & row, & andy, 1 , " web" , false , None ) .await.ok ( ) .expect ( " landed" ) ;
f7bcee49 Andy McCall 22 days ago ↶
5076
let mut runs = vec! [ ] ;
5077
for _ in 0 .. 100 {
5078
runs = db:: flow:: runs( & h.app.db, row.id, 10 , None ) .await.unwrap ( ) ;
5079
if runs.len ( ) > runs_before {
5080
break ;
5081
}
5082
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
5083
}
5084
let landed = runs.iter ( ) .find ( | r | r.bookmark = = " main" & & r.state = = state ) .expect ( " a run of the landed state on main" ) ;
5085
assert_eq! ( ( landed.trigger.as_str ( ) , landed.actor.as_str ( ) ) , ( " push" , " andy" ) ) ;
4a4400c5 Andy McCall 22 days ago ↶
5086
}
1870e4fa Andy McCall 20 days ago ↶
5087
5088
5089
5090
# [ tokio ::test ( flavor = " multi_thread" ) ]
5091
async fn owners_run_flow_on_their_own_runners_only ( ) {
5092
use oxy_core:: identity:: SecretKey;
5093
let h = hub ( ) .await;
5094
let register = | name : & 'static str | {
5095
let app = h.app.clone ( ) ;
5096
async move {
c8684d44 Andy McCall 19 days ago
5097
let body = format! ( " username={name} &email={name} %40example.com&password=correct-horse-battery&setup=test-setup" ) ;
1870e4fa Andy McCall 20 days ago ↶
5098
let res = oxygenhub:: router( app)
5099
.oneshot (
5100
Request:: post( " /register" )
5101
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
5102
.body ( Body:: from( body) )
5103
.unwrap ( ) ,
5104
)
5105
.await
5106
.unwrap ( ) ;
5107
res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( )
5108
}
5109
} ;
5110
register ( " andy" ) .await;
5111
let bob = register ( " bob" ) .await;
5112
create ( & h.app, " bob" , " game" ) .await;
5113
let web = | method : & 'static str , path : & str , body : Option< String> | {
5114
let ( app, cookie, path) = ( h.app.clone ( ) , bob.clone ( ) , path.to_string ( ) ) ;
5115
async move {
c7067c7b Andy McCall 16 days ago
5116
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie.clone ( ) ) ;
1870e4fa Andy McCall 20 days ago ↶
5117
if body.is_some ( ) {
5118
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
5119
}
c7067c7b Andy McCall 16 days ago
5120
let req = req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ;
5121
let mut res = oxygenhub:: router( app.clone ( ) ) .oneshot ( req) .await.unwrap ( ) ;
5122
5123
let back =
5124
res.headers ( ) .get ( header:: LOCATION ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .filter ( | l | l.contains ( " /settings/" ) ) .map ( String :: from) ;
5125
if let Some ( to) = back {
5126
let req = Request:: get( to) .header ( header:: COOKIE , cookie) .body ( Body:: empty( ) ) .unwrap ( ) ;
5127
res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
5128
}
1870e4fa Andy McCall 20 days ago ↶
5129
String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( )
5130
}
5131
} ;
5132
let csrf_of = | page : & str | {
5133
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
5134
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
5135
} ;
5136
c7067c7b Andy McCall 16 days ago
5137
let page = web ( " GET" , " /bob/game/settings/flow" , None ) .await;
1870e4fa Andy McCall 20 days ago ↶
5138
assert! ( page.contains ( " register a runner for this repository" ) & & ! page.contains ( " value=\" own\" " ) , " {page}" ) ;
5139
let page = web ( " POST" , " /bob/game/settings/flow" , Some ( format! ( " _csrf={} &flow=own" , csrf_of ( & page) ) ) ) .await;
5140
assert! ( page.contains ( " Register a runner for this repository" ) , " {page}" ) ;
5141
5142
let page = web ( " GET" , " /bob/game/settings" , None ) .await;
5143
let page = web ( " POST" , " /bob/game/settings/runners/token" , Some ( format! ( " _csrf={} " , csrf_of ( & page) ) ) ) .await;
5144
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
5145
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
5146
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " labels" : [ ] , " max_jobs" : 1 } ) ;
5147
let identity = SecretKey:: generate( ) .identity ( " bobs" , " " , 1 ) ;
5148
let reg = serde_json:: json! ( { " token" : token, " name" : " bobs" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
5149
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
5150
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
5151
let credential = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5152
c7067c7b Andy McCall 16 days ago
5153
let page = web ( " GET" , " /bob/game/settings/flow" , None ) .await;
1870e4fa Andy McCall 20 days ago ↶
5154
assert! ( page.contains ( " value=\" own\" " ) & & ! page.contains ( " value=\" on\" " ) , " {page}" ) ;
5155
let page = web ( " POST" , " /bob/game/settings/flow" , Some ( format! ( " _csrf={} &flow=on" , csrf_of ( & page) ) ) ) .await;
5156
assert! ( page.contains ( " Only site administrators let the site's runners" ) , " {page}" ) ;
5157
let page = web ( " POST" , " /bob/game/settings/flow" , Some ( format! ( " _csrf={} &flow=own" , csrf_of ( & page) ) ) ) .await;
5158
assert! ( page.contains ( " value=\" own\" checked" ) & & page.contains ( " start runs" ) , " {page}" ) ;
5159
assert_eq! ( db:: repos:: get( & h.app.db, " bob" , " game" ) .await.unwrap ( ) .unwrap ( ) .flow, " own" ) ;
5160
5161
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5162
let c = client ( & format! ( " {} /bob/game" , h.base) , work.path ( ) ) ;
5163
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
5164
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
5165
c.snapshot ( " t" ) .unwrap ( ) ;
5166
c.commit ( " A pipeline" , & [ ] ) .unwrap ( ) ;
5167
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5168
assert_eq! ( runs_of ( & h.app, " bob" , " game" ) .await.len ( ) , 1 , " a run started" ) ;
5169
assert! ( claim ( & h.app) .await.is_none ( ) , " the site's runner is not offered it" ) ;
5170
let body = serde_json:: json! ( { " runner" : " ignored" , " runtimes" : [ " microvm" ] , " info" : info } ) .to_string ( ) ;
5171
let ( st, job) = call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( & credential) , Some ( body) ) .await;
5172
assert_eq! ( st, StatusCode:: OK , " {job}" ) ;
5173
assert_eq! ( serde_json:: from_str:: < serde_json:: Value> ( & job) .unwrap ( ) [ " repo" ] , " bob/game" ) ;
5174
}
cd437c4f Andy McCall 20 days ago ↶
5175
5176
# [ tokio ::test ( flavor = " multi_thread" ) ]
5177
async fn a_job_takes_the_artifact_its_producer_kept ( ) {
5178
let h = hub ( ) .await;
5179
create ( & h.app, " andy" , " demo" ) .await;
5180
let url = format! ( " {} /andy/demo" , h.base) ;
5181
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5182
let c = client ( & url, work.path ( ) ) ;
5183
write (
5184
work.path ( ) ,
5185
" .flow/pipeline.kdl" ,
5186
& format! (
5187
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" plan\" {{ \n sandbox \" alpine\" \n exec \" plan\" \n output {{ \n artifact \" tfplan\" {{ \n path \" /src/out/tfplan\" \n }} \n }} \n }} \n job \" apply\" {{ \n sandbox \" alpine\" \n depends-on \" plan\" \n input {{ \n artifact \" tfplan\" from \" plan\" \n path \" /src/tfplan\" \n }} \n exec \" apply\" \n }} \n "
5188
) ,
5189
) ;
5190
c.snapshot ( " test" ) .unwrap ( ) ;
5191
c.commit ( " Plan, then apply that plan" , & [ ] ) .unwrap ( ) ;
5192
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5193
5194
5195
let runs = runs_of ( & h.app, " andy" , " demo" ) .await;
5196
assert_eq! ( runs.len ( ) , 1 , " a run" ) ;
5197
let jobs = db:: flow:: jobs( & h.app.db, runs[ 0 ] .id) .await.unwrap ( ) ;
5198
assert! ( ! jobs.is_empty ( ) , " jobs: {:?}" , runs[ 0 ] ) ;
5199
let p = claim ( & h.app)
5200
.await
5201
.unwrap_or_else ( | | panic! ( " nothing to claim: {:?}" , jobs.iter ( ) .map ( | j | ( & j.name, & j.status, & j.error) ) .collect:: < Vec< _ > > ( ) ) ) ;
5202
assert_eq! ( p[ " job" ] [ " name" ] , " plan" ) ;
5203
let plan_id = p[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
5204
let plan = b " the reviewed plan" .to_vec ( ) ;
5205
let ( st, text) = upload ( & h.app, plan_id, " tfplan" , & plan) .await;
5206
assert_eq! ( st, StatusCode:: OK , " {text}" ) ;
5207
let blob = oxy_core:: repo:: Repo:: blob_ref( & plan) .to_hex ( ) ;
5208
let mut r = receipt ( " plan" , p[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
5209
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
5210
assert_eq! (
5211
finish ( & h.app, plan_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : r } ) ) .await,
5212
StatusCode:: NO_CONTENT
5213
) ;
5214
5215
5216
5217
let a = claim ( & h.app) .await.unwrap ( ) ;
5218
assert_eq! ( a[ " job" ] [ " name" ] , " apply" ) ;
5219
let apply_id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
5220
assert_eq! (
5221
a[ " inputs" ] ,
5222
serde_json:: json! ( [ { " artifact" : " tfplan" , " from" : " plan" , " path" : " /src/tfplan" , " blob" : blob } ] ) ,
5223
" the claim names the producer's blob"
5224
) ;
5225
let req = Request:: get( format! ( " /api/v1/flow/jobs/{apply_id} /inputs/0" ) ) .header ( header:: AUTHORIZATION , " Bearer runner-secret" ) ;
5226
let res = oxygenhub:: router( h.app.clone ( ) ) .oneshot ( req.body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
5227
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
5228
assert_eq! ( res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) , plan) ;
5229
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{apply_id} /inputs/1" ) , Some ( " runner-secret" ) , None ) .await;
5230
assert_eq! ( st, StatusCode:: NOT_FOUND , " no second input" ) ;
5231
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{apply_id} /inputs/0" ) , None , None ) .await;
5232
assert_eq! ( st, StatusCode:: UNAUTHORIZED , " runners only" ) ;
5233
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /api/v1/flow/jobs/{plan_id} /inputs/0" ) , Some ( " runner-secret" ) , None ) .await;
5234
assert_eq! ( st, StatusCode:: CONFLICT , " only a running job's" ) ;
5235
}
417184c7 Andy McCall 20 days ago ↶
5236
5237
# [ tokio ::test ( flavor = " multi_thread" ) ]
5238
async fn a_job_waits_for_approval_and_is_approved_rejected_or_expires ( ) {
5239
let h = hub ( ) .await;
5240
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
5241
let bob = db:: accounts:: create_account( & h.app.db, " bob" , " [email protected] " , " x" , " salt" , false ) .await.unwrap ( ) ;
5242
let carol = db:: accounts:: create_account( & h.app.db, " carol" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
5243
create ( & h.app, " andy" , " gated" ) .await;
5244
let row = db:: repos:: get( & h.app.db, " andy" , " gated" ) .await.unwrap ( ) .unwrap ( ) ;
5245
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5246
let c = client ( & format! ( " {} /andy/gated" , h.base) , work.path ( ) ) ;
5247
write (
5248
work.path ( ) ,
5249
" .flow/pipeline.kdl" ,
5250
& format! (
47e37402 Andy McCall 20 days ago ↶
5251
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" plan\" {{ \n sandbox \" alpine\" \n exec \" plan\" \n }} \n job \" apply\" {{ \n sandbox \" alpine\" \n depends-on \" plan\" \n approval {{ \n by \" maintain\" \n others-only #true\n sealed #false\n }} \n environment \" prod\" \n identity \" https://sts.example\" {{ \n mount-path \" /run/secrets/cloud\" \n }} \n exec \" apply\" \n }} \n "
417184c7 Andy McCall 20 days ago ↶
5252
) ,
5253
) ;
5254
let mut n = 0 ;
5255
let mut push = | | {
5256
n + = 1 ;
5257
write ( work.path ( ) , " n.txt" , & format! ( " {n} \n " ) ) ;
5258
c.snapshot ( " test" ) .unwrap ( ) ;
5259
c.commit ( & format! ( " Push {n} " ) , & [ ] ) .unwrap ( ) ;
5260
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5261
} ;
5262
5263
let plan_passes = | number : i64 | {
5264
let app = h.app.clone ( ) ;
5265
let row = row.clone ( ) ;
5266
async move {
5267
let mut run = None ;
5268
for _ in 0 .. 100 {
5269
if let Some ( r) = db:: flow:: run( & app.db, row.id, number) .await.unwrap ( ) {
5270
run = Some ( r) ;
5271
break ;
5272
}
5273
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 30 ) ) .await;
5274
}
5275
let run = run.expect ( " the run" ) ;
3b1bc27f Andy McCall 20 days ago
5276
sqlx:: query( " UPDATE flow_runs SET actor = 'andy' WHERE id = $1" ) .bind ( run.id) .execute ( & app.db) .await.unwrap ( ) ;
417184c7 Andy McCall 20 days ago ↶
5277
let j = claim ( & app) .await.expect ( " plan is claimed" ) ;
5278
assert_eq! ( j[ " job" ] [ " name" ] , " plan" ) ;
5279
let ( id, ph) = ( j[ " job_id" ] .as_i64 ( ) .unwrap ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
5280
finish ( & app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( " plan" , & ph, 0 ) } ) ) .await;
5281
db:: flow:: run( & app.db, row.id, number) .await.unwrap ( ) .unwrap ( )
5282
}
5283
} ;
5284
5285
5286
push ( ) ;
5287
let run = runs_of ( & h.app, " andy" , " gated" ) .await[ 0 ] .clone ( ) ;
7e01a766 Andy McCall 20 days ago ↶
5288
33d98147 Andy McCall 16 days ago
5289
let heard = | topic : String| match h.app.bus.live.since ( 0 , & [ topic] ) {
7e01a766 Andy McCall 20 days ago ↶
5290
oxygenhub:: events:: Replay:: Events( e) => e.into_iter ( ) .map ( | e | e.kind ) .collect:: < Vec< _ > > ( ) ,
5291
oxygenhub:: events:: Replay:: Reload => vec! [ " reload" .into ( ) ] ,
5292
} ;
5293
assert! ( heard ( oxygenhub:: events:: topic:: repo( row.id) ) .contains ( & " push" .to_string ( ) ) , " the push is on the repository's topic" ) ;
3b1bc27f Andy McCall 20 days ago
5294
sqlx:: query( " UPDATE flow_runs SET actor = 'andy' WHERE id = $1" ) .bind ( run.id) .execute ( & h.app.db) .await.unwrap ( ) ;
05f1a85a Andy McCall 18 days ago ↶
5295
let early = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, true , None ) .await.unwrap_err ( ) ;
417184c7 Andy McCall 20 days ago ↶
5296
assert! ( early.contains ( " waits for plan first" ) , " {early}" ) ;
5297
let run = plan_passes ( run.number) .await;
5298
assert! ( claim ( & h.app) .await.is_none ( ) , " apply waits for a person, so no runner gets it" ) ;
7e01a766 Andy McCall 20 days ago ↶
5299
assert! ( heard ( oxygenhub:: events:: topic:: run( run.id) ) .contains ( & " jobs" .to_string ( ) ) , " the run's topic hears its jobs" ) ;
dc59d1b5 Andy McCall 20 days ago ↶
5300
5301
5302
let told = | id : i64 | {
5303
let db = h.app.db.clone ( ) ;
5304
async move { db:: notify:: inbox( & db, id, 20 ) .await.unwrap ( ) .into_iter ( ) .filter ( | n | n.kind = = " flow.approval" ) .count ( ) }
5305
} ;
5306
assert_eq! ( ( told ( carol.id) .await, told ( andy.id) .await, told ( bob.id) .await) , ( 1 , 0 , 0 ) ) ;
417184c7 Andy McCall 20 days ago ↶
5307
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/gated/flow/{} " , run.number) , None , None ) .await;
5308
assert! ( page.contains ( " waits for approval by someone with the <em>maintain</em> role, other than andy" ) , " the run page says so" ) ;
b1860d60 Andy McCall 8 days ago ↶
5309
5310
let ( _ , v) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/gated/flow/runs/{} " , run.number) , None , None ) .await;
5311
let v: serde_json:: Value = serde_json:: from_str( & v) .unwrap ( ) ;
5312
let apply = v[ " jobs" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | j | j[ " name" ] = = " apply" ) .unwrap ( ) .clone ( ) ;
5313
assert_eq! ( apply[ " approval" ] , " waiting" , " {v}" ) ;
5314
assert! ( v[ " jobs" ] .as_array ( ) .unwrap ( ) .iter ( ) .filter ( | j | j[ " name" ] ! = " apply" ) .all ( | j | j.get ( " approval" ) .is_none ( ) ) , " {v}" ) ;
05f1a85a Andy McCall 18 days ago ↶
5315
let own = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & andy, true , None ) .await.unwrap_err ( ) ;
417184c7 Andy McCall 20 days ago ↶
5316
assert! ( own.contains ( " someone other than andy" ) , " {own}" ) ;
05f1a85a Andy McCall 18 days ago ↶
5317
let no_role = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & bob, true , None ) .await.unwrap_err ( ) ;
417184c7 Andy McCall 20 days ago ↶
5318
assert! ( no_role.contains ( " takes the maintain role" ) , " {no_role}" ) ;
05f1a85a Andy McCall 18 days ago ↶
5319
assert_eq! ( oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, true , None ) .await.unwrap ( ) , " apply: approved by carol" ) ;
dc59d1b5 Andy McCall 20 days ago ↶
5320
assert_eq! ( told ( carol.id) .await, 1 , " told once" ) ;
417184c7 Andy McCall 20 days ago ↶
5321
let j = claim ( & h.app) .await.expect ( " approved, it runs" ) ;
5322
assert_eq! ( j[ " job" ] [ " name" ] , " apply" ) ;
b644e175 Andy McCall 20 days ago ↶
5323
5324
let token =
5325
j[ " secrets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | s | s[ " path" ] = = " /run/secrets/cloud" ) .unwrap ( ) [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5326
let claims = h.app.oidc ( ) .await.unwrap ( ) .verify ( & token, " https://sts.example" , db:: now_ms( ) ) .expect ( " a valid token" ) ;
5327
assert_eq! ( claims[ " environment" ] , " prod" ) ;
417184c7 Andy McCall 20 days ago ↶
5328
let ( id, ph) = ( j[ " job_id" ] .as_i64 ( ) .unwrap ( ) , j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
5329
finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : receipt ( " apply" , & ph, 0 ) } ) ) .await;
5330
let audit = db:: accounts:: audit_rows( & h.app.db, 50 ) .await.unwrap ( ) ;
5331
assert! ( audit.iter ( ) .any ( | r | r.action = = " flow.approved" & & r.actor = = " carol" & & r.target.ends_with ( " #1 apply" ) ) , " audited" ) ;
5332
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/gated/flow/{} " , run.number) , None , None ) .await;
5333
assert! ( page.contains ( " approved by carol" ) , " the run says who" ) ;
b644e175 Andy McCall 20 days ago ↶
5334
let ( _ , page) = call ( & h.app, " GET" , " /andy/gated/deployments" , None , None ) .await;
5335
assert! ( page.contains ( " <strong>prod</strong>" ) & & page.contains ( " >#1</a>" ) & & page.contains ( " <td>carol</td>" ) , " the Deployments page" ) ;
5336
let ( _ , list) = call ( & h.app, " GET" , " /api/v1/repos/andy/gated/deployments" , Some ( " site" ) , None ) .await;
5337
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
5338
assert_eq! ( ( list[ 0 ] [ " environment" ] .as_str ( ) , list[ 0 ] [ " approved_by" ] .as_str ( ) ) , ( Some ( " prod" ) , Some ( " carol" ) ) ) ;
417184c7 Andy McCall 20 days ago ↶
5339
05f1a85a Andy McCall 18 days ago ↶
5340
5341
5342
417184c7 Andy McCall 20 days ago ↶
5343
push ( ) ;
5344
let run = plan_passes ( 2 ) .await;
05f1a85a Andy McCall 18 days ago ↶
5345
let ( secret, hash) = oxygenhub:: auth:: new_token( ) ;
5346
let tok = db:: accounts:: create_token( & h.app.db, carol.id, " claude-code" , & hash, " repo:write" , " " , None ) .await.unwrap ( ) ;
5347
db:: accounts:: set_token_agent( & h.app.db, carol.id, tok.id, true ) .await.unwrap ( ) ;
4a68f106 Andy McCall 17 days ago ↶
5348
5349
05f1a85a Andy McCall 18 days ago ↶
5350
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/gated/flow/runs/2/jobs/apply/reject" , Some ( & secret) , Some ( " {}" .into ( ) ) ) .await;
4a68f106 Andy McCall 17 days ago ↶
5351
assert_eq! ( st, StatusCode:: CONFLICT , " {body}" ) ;
5352
assert! ( body.contains ( " decided by a person here" ) & & body.contains ( " Settings › OxygenFlow" ) , " {body}" ) ;
5353
let gated = db:: repos:: get( & h.app.db, " andy" , " gated" ) .await.unwrap ( ) .unwrap ( ) ;
5354
db:: repos:: set_agents_decide_gates( & h.app.db, gated.id, true ) .await.unwrap ( ) ;
5355
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/gated/flow/runs/2/jobs/apply/reject" , Some ( & secret) , Some ( " {}" .into ( ) ) ) .await;
05f1a85a Andy McCall 18 days ago ↶
5356
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
5357
assert! ( body.contains ( " rejected by carol via claude-code" ) , " {body}" ) ;
417184c7 Andy McCall 20 days ago ↶
5358
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
5359
let apply = jobs.iter ( ) .find ( | j | j.name = = " apply" ) .unwrap ( ) ;
05f1a85a Andy McCall 18 days ago ↶
5360
assert_eq! ( ( apply.status.as_str ( ) , apply.error.as_str ( ) ) , ( " cancelled" , " rejected by carol via claude-code" ) ) ;
5361
assert_eq! ( ( apply.approved_by.as_str ( ) , apply.approved_via.as_str ( ) ) , ( " carol" , " claude-code" ) ) ;
5362
let audit = db:: accounts:: audit_rows( & h.app.db, 50 ) .await.unwrap ( ) ;
5363
assert! (
5364
audit.iter ( ) .any ( | r | r.action = = " flow.rejected" & & r.actor = = " carol" & & r.detail.contains ( " via claude-code" ) ) ,
5365
" audited with the agent"
5366
) ;
417184c7 Andy McCall 20 days ago ↶
5367
assert! ( claim ( & h.app) .await.is_none ( ) ) ;
5368
5369
5370
push ( ) ;
5371
let run = plan_passes ( 3 ) .await;
5372
assert_eq! ( oxygenhub:: flow:: expire_approvals( & h.app, db:: now_ms( ) + 3_600_000 ) .await.unwrap ( ) , 0 , " not yet" ) ;
5373
assert_eq! ( oxygenhub:: flow:: expire_approvals( & h.app, db:: now_ms( ) + 25 * 3_600_000 ) .await.unwrap ( ) , 1 ) ;
5374
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
5375
let apply = jobs.iter ( ) .find ( | j | j.name = = " apply" ) .unwrap ( ) ;
5376
assert_eq! ( ( apply.status.as_str ( ) , apply.error.as_str ( ) ) , ( " cancelled" , " no approval within 24h" ) ) ;
5377
}
67be8ac0 Andy McCall 20 days ago ↶
5378
5379
# [ tokio ::test ( flavor = " multi_thread" ) ]
5380
async fn live_streams_only_topics_the_viewer_may_hear_and_replays ( ) {
5381
let h = hub ( ) .await;
5382
create ( & h.app, " andy" , " open" ) .await;
5383
h.app.repos.create ( & RepoName:: parse( " andy/shut" ) .unwrap ( ) , None ) .unwrap ( ) ;
5384
db:: repos:: create( & h.app.db, " andy" , " shut" , " " , hub_api:: Visibility:: Private) .await.unwrap ( ) ;
5385
let open = db:: repos:: get( & h.app.db, " andy" , " open" ) .await.unwrap ( ) .unwrap ( ) ;
5386
let shut = db:: repos:: get( & h.app.db, " andy" , " shut" ) .await.unwrap ( ) .unwrap ( ) ;
33d98147 Andy McCall 16 days ago
5387
let live = & h.app.bus.live;
67be8ac0 Andy McCall 20 days ago ↶
5388
let first = live.publish ( & oxygenhub:: events:: topic:: repo( open.id) , " push" ) ;
5389
live.publish ( & oxygenhub:: events:: topic:: repo( shut.id) , " push" ) ;
5390
live.publish ( & oxygenhub:: events:: topic:: proposal( open.id, 3 ) , " review" ) ;
5391
5392
5393
let read = | path : String, last : Option< u64 > | {
5394
let app = h.app.clone ( ) ;
5395
async move {
5396
let mut req = Request:: get( & path) ;
5397
if let Some ( l) = last {
5398
req = req.header ( " last-event-id" , l.to_string ( ) ) ;
5399
}
5400
let res = oxygenhub:: router( app) .oneshot ( req.body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
5401
let status = res.status ( ) ;
5402
let mut body = res.into_body ( ) ;
5403
let mut text = String :: new( ) ;
5404
while let Ok ( Some ( Ok ( frame) ) ) = tokio:: time:: timeout( std:: time:: Duration:: from_millis( 300 ) , body.frame ( ) ) .await {
5405
if let Some ( d) = frame.data_ref ( ) {
5406
text.push_str ( & String :: from_utf8_lossy( d) ) ;
5407
}
5408
}
5409
( status, text)
5410
}
5411
} ;
5412
let topics = format! ( " /live?topics=repo:{} ,repo:{} /proposal:3" , open.id, open.id) ;
5413
5414
let ( st, text) = read ( topics.clone ( ) , Some ( first.id) ) .await;
5415
assert_eq! ( st, StatusCode:: OK ) ;
5416
assert! ( text.contains ( " event: change" ) & & text.contains ( & format! ( " \" topic\" :\" repo:{} /proposal:3\" " , open.id) ) , " {text}" ) ;
5417
assert! ( ! text.contains ( & format! ( " \" topic\" :\" repo:{} \" " , shut.id) ) , " never another topic: {text}" ) ;
5418
assert! ( ! text.contains ( & format! ( " id: {} \n " , first.id) ) , " not what was already seen: {text}" ) ;
5419
5420
let ( st, _ ) = read ( format! ( " /live?topics=repo:{} ,repo:{} " , open.id, shut.id) , None ) .await;
5421
assert_eq! ( st, StatusCode:: FORBIDDEN ) ;
5422
assert_eq! ( read ( " /live?topics=user:1" .into ( ) , None ) .await.0 , StatusCode:: FORBIDDEN ) ;
5423
assert_eq! ( read ( " /live?topics=nonsense" .into ( ) , None ) .await.0 , StatusCode:: BAD_REQUEST ) ;
5424
assert_eq! ( read ( " /live?topics=" .into ( ) , None ) .await.0 , StatusCode:: BAD_REQUEST ) ;
5425
5426
for _ in 0 .. oxygenhub:: events:: REPLAY {
5427
live.publish ( " repo:999999" , " push" ) ;
5428
}
5429
let ( _ , text) = read ( topics, Some ( first.id) ) .await;
5430
assert! ( text.contains ( " event: reload" ) , " {text}" ) ;
5431
}
47e37402 Andy McCall 20 days ago ↶
5432
5433
# [ tokio ::test ( flavor = " multi_thread" ) ]
5434
async fn a_sealed_approval_releases_exactly_the_reviewed_plan ( ) {
5435
use oxy_core:: approval:: { SignedApproval, Statement} ;
5436
use oxy_core:: hash:: ObjectRef;
5437
use oxy_core:: identity:: SecretKey;
5438
let h = hub ( ) .await;
5439
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
5440
let carol = db:: accounts:: create_account( & h.app.db, " carol" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
5441
let sk = SecretKey:: generate( ) ;
5442
let carol_id = sk.identity ( " Carol" , " [email protected] " , 1 ) ;
5443
db:: accounts:: link_identity( & h.app.db, carol.id, & carol_id.hash ( ) .to_hex ( ) , " Carol" , " [email protected] " , & carol_id.encode ( ) )
5444
.await
5445
.unwrap ( ) ;
5446
create ( & h.app, " andy" , " infra" ) .await;
5447
let row = db:: repos:: get( & h.app.db, " andy" , " infra" ) .await.unwrap ( ) .unwrap ( ) ;
5448
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5449
let c = client ( & format! ( " {} /andy/infra" , h.base) , work.path ( ) ) ;
5450
write (
5451
work.path ( ) ,
5452
" .flow/pipeline.kdl" ,
5453
& format! (
5454
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" plan\" {{ \n sandbox \" alpine\" \n exec \" plan\" \n output {{ \n artifact \" tfplan\" {{ \n path \" /src/out/tfplan\" \n }} \n }} \n }} \n job \" apply\" {{ \n sandbox \" alpine\" \n depends-on \" plan\" \n environment \" prod\" \n approval\n input {{ \n artifact \" tfplan\" from \" plan\" \n path \" /src/tfplan\" \n }} \n identity \" https://sts.example\" {{ \n mount-path \" /run/secrets/cloud\" \n }} \n exec \" apply\" \n }} \n "
5455
) ,
5456
) ;
5457
c.snapshot ( " test" ) .unwrap ( ) ;
5458
c.commit ( " Plan, then apply" , & [ ] ) .unwrap ( ) ;
5459
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5460
let run = runs_of ( & h.app, " andy" , " infra" ) .await[ 0 ] .clone ( ) ;
5461
let p = claim ( & h.app) .await.unwrap ( ) ;
5462
let plan_id = p[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
5463
let plan = b " the reviewed plan" .to_vec ( ) ;
5464
upload ( & h.app, plan_id, " tfplan" , & plan) .await;
5465
let mut r = receipt ( " plan" , p[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
5466
r[ " outputs" ] = serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( & plan) .to_hex ( ) ] ) ;
5467
finish ( & h.app, plan_id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 10 , " receipt" : r } ) ) .await;
5468
let run = db:: flow:: run( & h.app.db, row.id, run.number) .await.unwrap ( ) .unwrap ( ) ;
5469
5470
05f1a85a Andy McCall 18 days ago ↶
5471
let plain = oxygenhub:: flow:: decide( & h.app, & row, & run, " apply" , & carol, true , None ) .await.unwrap_err ( ) ;
47e37402 Andy McCall 20 days ago ↶
5472
assert! ( plain.contains ( & format! ( " oxy seal --flow {} /andy/infra/flow/{} --job apply" , h.base, run.number) ) , " {plain}" ) ;
5473
5474
let ( st, body) =
5475
call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/infra/flow/runs/{} /jobs/apply/approval" , run.number) , Some ( " site" ) , None ) .await;
5476
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
5477
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
5478
assert_eq! ( v[ " environment" ] , " prod" ) ;
5479
assert_eq! ( v[ " inputs" ] , serde_json:: json! ( [ oxy_core:: repo:: Repo:: blob_ref( & plan) .to_hex ( ) ] ) , " the reviewed plan" ) ;
5480
let mut repo = [ 0 u8 ; 16 ] ;
5481
repo.copy_from_slice ( & hex:: decode( v[ " repo" ] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ) ;
5482
let oref = | k : & str | ObjectRef:: from_hex( v[ k] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ;
5483
let statement = Statement {
5484
repo,
5485
run: v[ " run" ] .as_str ( ) .unwrap ( ) .into ( ) ,
5486
job: " apply" .into ( ) ,
5487
job_hash: oref ( " job_hash" ) ,
5488
state: oref ( " state" ) ,
5489
inputs: v[ " inputs" ] .as_array ( ) .unwrap ( ) .iter ( ) .map ( | i | ObjectRef:: from_hex( i.as_str ( ) .unwrap ( ) ) .unwrap ( ) ) .collect ( ) ,
5490
environment: " prod" .into ( ) ,
5491
approve: true ,
5492
} ;
5493
let seal = | s : & Statement, key : & SecretKey, id : & oxy_core:: identity:: Identity| {
5494
let body = serde_json:: json! ( { " signed" : hex:: encode( SignedApproval:: sign( s, id, key) .encode ( ) ) } ) .to_string ( ) ;
5495
let app = h.app.clone ( ) ;
5496
let path = format! ( " /api/v1/repos/andy/infra/flow/runs/{} /jobs/apply/seal" , run.number) ;
5497
async move { call ( & app, " POST" , & path, Some ( " site" ) , Some ( body) ) .await }
5498
} ;
5499
5500
let other_plan = Statement { inputs: vec! [ ObjectRef( [ 9 ; 32 ] ) ] , .. statement.clone ( ) } ;
5501
let ( st, body) = seal ( & other_plan, & sk, & carol_id) .await;
5502
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " approves something else" ) , " {st} {body}" ) ;
5503
let agent = SecretKey:: generate( ) ;
5504
let agent_id = agent.identity ( " Claude Code" , " [email protected] " , 1 ) ;
5505
let ( st, body) = seal ( & statement, & agent, & agent_id) .await;
5506
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not linked to an account" ) , " {st} {body}" ) ;
5507
assert! ( claim ( & h.app) .await.is_none ( ) , " still waiting" ) ;
5508
317d776c Andy McCall 18 days ago ↶
5509
5510
let forged = oxy_core:: identity:: Identity { previous: Some ( carol_id.hash ( ) ) , .. agent_id.clone ( ) } ;
5511
let ( st, body) = seal ( & statement, & agent, & forged) .await;
5512
assert! ( st = = StatusCode:: CONFLICT & & body.contains ( " not linked" ) , " {st} {body}" ) ;
5513
5514
5515
5516
5517
let renewed = carol_id.successor ( vec! [ ] , None , 2 ) ;
5518
let ( st, body) = seal ( & statement, & sk, & renewed) .await;
47e37402 Andy McCall 20 days ago ↶
5519
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
317d776c Andy McCall 18 days ago ↶
5520
assert! (
5521
db:: accounts:: identity_by_key( & h.app.db, & renewed.hash ( ) .to_hex ( ) ) .await.unwrap ( ) .is_some_and ( | l | l.account_id = = carol.id ) ,
5522
" linked now"
5523
) ;
47e37402 Andy McCall 20 days ago ↶
5524
assert! ( body.contains ( " approved by carol, sealed" ) , " {body}" ) ;
5525
let name = oxygenhub:: repos:: RepoName:: parse( " andy/infra" ) .unwrap ( ) ;
5526
let state = statement.state;
5527
let kept = h.app.repos.read ( & name, move | r, _ | Ok ( r.store ( ) .approvals_for ( & state) ? ) ) .await.unwrap ( ) ;
5528
assert_eq! ( kept.len ( ) , 1 , " the approval is in the repository" ) ;
5529
let j = claim ( & h.app) .await.expect ( " released" ) ;
5530
assert_eq! ( j[ " job" ] [ " name" ] , " apply" ) ;
5531
let token =
5532
j[ " secrets" ] .as_array ( ) .unwrap ( ) .iter ( ) .find ( | s | s[ " path" ] = = " /run/secrets/cloud" ) .unwrap ( ) [ " value" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5533
let claims = h.app.oidc ( ) .await.unwrap ( ) .verify ( & token, " https://sts.example" , db:: now_ms( ) ) .unwrap ( ) ;
5534
assert_eq! ( ( claims[ " environment" ] .as_str ( ) , claims[ " approved_by" ] .as_str ( ) ) , ( Some ( " prod" ) , Some ( " carol" ) ) ) ;
5535
assert_eq! ( claims[ " approval" ] .as_str ( ) , Some ( kept[ 0 ] .to_hex ( ) .as_str ( ) ) ) ;
5536
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/infra/flow/{} " , run.number) , None , None ) .await;
5537
assert! ( page.contains ( " sealed with their key" ) , " the run page says so" ) ;
5538
}
55673c65 Andy McCall 19 days ago ↶
5539
5540
5541
5542
# [ tokio ::test ( flavor = " multi_thread" ) ]
5543
async fn repositories_take_turns_on_the_runners ( ) {
5544
let h = hub ( ) .await;
5545
for name in [ " busy" , " quiet" ] {
5546
create ( & h.app, " andy" , name) .await;
5547
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5548
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
5549
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
5550
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
5551
c.snapshot ( " test" ) .unwrap ( ) ;
5552
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
5553
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5554
assert_eq! ( runs_of ( & h.app, " andy" , name) .await.len ( ) , 1 ) ;
5555
}
5556
5557
5558
let order: Vec< String> = {
5559
let mut out = Vec :: new( ) ;
5560
for _ in 0 .. 3 {
5561
let c = claim ( & h.app) .await.expect ( " a job to claim" ) ;
5562
out.push ( c[ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
5563
}
5564
out
5565
} ;
5566
assert_eq! ( order, [ " andy/busy" , " andy/quiet" , " andy/busy" ] ) ;
5567
}
107eaba5 Andy McCall 19 days ago ↶
5568
53a0fb2e Andy McCall 19 days ago ↶
5569
5570
5571
5572
5573
# [ tokio ::test ( flavor = " multi_thread" ) ]
5574
async fn a_pipeline_lowers_its_jobs_and_expediting_still_wins ( ) {
5575
let h = hub ( ) .await;
5576
for ( name, extra) in [ ( " nightly" , " priority \" low\" \n " ) , ( " everyday" , " " ) ] {
5577
create ( & h.app, " andy" , name) .await;
5578
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5579
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
5580
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( extra) ) ;
5581
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
5582
c.snapshot ( " test" ) .unwrap ( ) ;
5583
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
5584
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5585
assert_eq! ( runs_of ( & h.app, " andy" , name) .await.len ( ) , 1 ) ;
5586
}
5587
let nightly = db:: repos:: get( & h.app.db, " andy" , " nightly" ) .await.unwrap ( ) .unwrap ( ) ;
5588
db:: repos:: set_flow_priority( & h.app.db, nightly.id, " high" ) .await.unwrap ( ) ;
5589
let q = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
5590
assert! (
5591
q.iter ( ) .filter ( | e | e.name = = " nightly" ) .all ( | e | e.priority ( ) = = " low" & & e.priority_note = = " pipeline: low" ) ,
5592
" {:?}" ,
5593
q.iter ( ) .map ( | e | ( & e.name, e.priority ( ) , & e.priority_note) ) .collect:: < Vec< _ > > ( )
5594
) ;
5595
let repo_of = | c : serde_json:: Value| c[ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5596
5597
5598
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/everyday" ) ;
5599
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/everyday" ) ;
5600
let run = & runs_of ( & h.app, " andy" , " nightly" ) .await[ 0 ] ;
5601
assert! ( db:: flow:: expedite( & h.app.db, run.id, " expedited by andy: needed now" ) .await.unwrap ( ) ) ;
5602
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/nightly" , " an expedited run is urgent whatever its jobs say" ) ;
5603
}
5604
107eaba5 Andy McCall 19 days ago ↶
5605
5606
5607
# [ tokio ::test ( flavor = " multi_thread" ) ]
5608
async fn priority_then_turns_and_an_expedited_run_first ( ) {
5609
let h = hub ( ) .await;
5610
for name in [ " early" , " later" ] {
5611
create ( & h.app, " andy" , name) .await;
5612
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5613
let c = client ( & format! ( " {} /andy/{name} " , h.base) , work.path ( ) ) ;
5614
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
5615
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
5616
c.snapshot ( " test" ) .unwrap ( ) ;
5617
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
5618
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5619
assert_eq! ( runs_of ( & h.app, " andy" , name) .await.len ( ) , 1 ) ;
5620
}
5621
let early = db:: repos:: get( & h.app.db, " andy" , " early" ) .await.unwrap ( ) .unwrap ( ) ;
5622
db:: repos:: set_flow_priority( & h.app.db, early.id, " low" ) .await.unwrap ( ) ;
5623
5624
let repo_of = | c : serde_json:: Value| c[ " repo" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5625
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/later" ) ;
5626
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/later" ) ;
5627
5628
let run = & runs_of ( & h.app, " andy" , " early" ) .await[ 0 ] ;
5629
assert! ( db:: flow:: expedite( & h.app.db, run.id, " expedited by andy: an incident" ) .await.unwrap ( ) ) ;
76f93eae Andy McCall 19 days ago ↶
5630
5631
assert_eq! ( db:: flow:: expedited_live( & h.app.db, " andy" ) .await.unwrap ( ) , 1 ) ;
5632
assert_eq! ( db:: flow:: expedited_live( & h.app.db, " someone-else" ) .await.unwrap ( ) , 0 ) ;
107eaba5 Andy McCall 19 days ago ↶
5633
assert_eq! ( repo_of ( claim ( & h.app) .await.unwrap ( ) ) , " andy/early" ) ;
5634
let q = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
5635
assert! (
5636
q.iter ( ) .any ( | e | e.name = = " early" & & e.priority ( ) = = " urgent" & & e.priority_note.contains ( " an incident" ) ) ,
5637
" {:?}" ,
5638
q.iter ( ) .map ( | e | ( & e.name, e.priority ( ) ) ) .collect:: < Vec< _ > > ( )
5639
) ;
5640
5641
db:: accounts:: set_setting( & h.app.db, " flow_priority" , " high" ) .await.unwrap ( ) ;
5642
let q = db:: flow:: queue( & h.app.db) .await.unwrap ( ) ;
5643
assert! ( q.iter ( ) .filter ( | e | e.name = = " later" ) .all ( | e | e.priority ( ) = = " high" ) ) ;
5644
}
026a76d2 Andy McCall 19 days ago ↶
5645
5646
5647
5648
5649
5650
5651
# [ tokio ::test ( flavor = " multi_thread" ) ]
5652
async fn a_running_job_gets_a_short_certificate_for_a_fresh_key ( ) {
5653
use oxy_core:: cert:: { certify_challenge, SignedCertificate} ;
5654
use oxy_core:: identity:: { Scope, SecretKey} ;
5655
let h = hub ( ) .await;
5656
create ( & h.app, " andy" , " demo" ) .await;
5657
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5658
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
5659
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
5660
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
5661
c.snapshot ( " test" ) .unwrap ( ) ;
5662
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
5663
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5664
assert_eq! ( runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) , 1 ) ;
5665
let job = claim ( & h.app) .await.expect ( " a job" ) ;
5666
let token = job[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5667
let job_id = job[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
5668
5669
let sk = SecretKey:: generate( ) ;
5670
let id = sk.identity ( " job" , " [email protected] " , 1 ) ;
5671
let ask = | credential : & str , signed_for : & str | {
5672
let proof = sk.sign_challenge ( & id.hash ( ) , & certify_challenge ( signed_for) ) .unwrap ( ) ;
5673
let body = serde_json:: json! ( {
5674
" identity" : oxy_core:: dsse:: base64( & id.encode ( ) ) ,
5675
" proof" : oxy_core:: dsse:: base64( & oxy_core:: cbor:: encode( & proof.to_value ( ) ) ) ,
5676
" scope" : " changes,refs" ,
5677
} ) ;
5678
let ( credential, app) = ( credential.to_string ( ) , h.app.clone ( ) ) ;
5679
async move { call ( & app, " POST" , " /api/v1/keyless/certify" , Some ( & credential) , Some ( body.to_string ( ) ) ) .await }
5680
} ;
5681
let ( st, body) = ask ( & token, & token) .await;
5682
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
5683
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
5684
let sc = SignedCertificate:: decode( & oxy_core:: dsse:: base64_decode( v[ " certificate" ] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ) .unwrap ( ) ;
5685
assert! ( sc.verify ( & h.app.hub_key.identity) .is_ok ( ) , " signed with the hub's key" ) ;
5686
assert_eq! ( sc.cert.key_id ( ) , id.hash ( ) ) ;
5687
assert_eq! ( sc.cert.subject, " repo:andy/demo:bookmark:main" ) ;
5688
assert_eq! ( sc.cert.claim ( " repository" ) , Some ( " andy/demo" ) ) ;
5689
assert! ( sc.cert.claim ( " job" ) .is_some ( ) & & sc.cert.claim ( " run" ) = = Some ( " 1" ) ) ;
5690
assert_eq! ( sc.cert.scope, Scope:: CHANGES | Scope:: REFS ) ;
5691
assert! ( sc.cert.expires - sc.cert.not_before < = 10 * 60_000 + 30_000 , " ten minutes" ) ;
5692
5693
5694
let ( st, body) = ask ( & token, " flowjob_someone_else" ) .await;
5695
assert_eq! ( st, StatusCode:: UNAUTHORIZED , " {body}" ) ;
5696
assert! ( body.contains ( " the proof does not show" ) , " {body}" ) ;
5697
5698
5699
5700
let issuer = h.app.oidc ( ) .await.unwrap ( ) ;
5701
let now = oxygenhub:: db:: now_ms( ) ;
5702
let aud = oxygenhub:: keyless:: audience( h.app.config.base_url ( ) ) ;
5703
let good = issuer.mint_for ( h.app.config.base_url ( ) , " agent:claude@andy" , & aud, now, 600 ) ;
5704
let ( st, body) = ask ( & good, & good) .await;
5705
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
5706
assert! ( body.contains ( " \" subject\" :\" agent:claude@andy\" " ) , " {body}" ) ;
5707
let other = issuer.mint_for ( h.app.config.base_url ( ) , " agent:claude@andy" , " sts.amazonaws.com" , now, 600 ) ;
5708
let ( st, _ ) = ask ( & other, & other) .await;
5709
assert_eq! ( st, StatusCode:: UNAUTHORIZED ) ;
5710
5711
5712
sqlx:: query( " UPDATE flow_jobs SET status = 'passed' WHERE id = $1" ) .bind ( job_id) .execute ( & h.app.db) .await.unwrap ( ) ;
5713
let ( st, body) = ask ( & token, & token) .await;
5714
assert_eq! ( st, StatusCode:: UNAUTHORIZED , " {body}" ) ;
5715
}
2c31915f Andy McCall 19 days ago ↶
5716
5717
5718
5719
5720
5721
# [ tokio ::test ( flavor = " multi_thread" ) ]
5722
async fn a_receipt_signed_by_a_certified_key_is_kept ( ) {
5723
use oxy_core:: cert:: { certify_challenge, SignedCertificate} ;
5724
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectType} ;
5725
use oxy_core:: identity:: SecretKey;
5726
use oxy_core:: objects:: StateObject;
5727
use oxy_core:: receipt:: SignedReceipt;
5728
let h = hub ( ) .await;
5729
create ( & h.app, " andy" , " demo" ) .await;
5730
let url = format! ( " {} /andy/demo" , h.base) ;
5731
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5732
let c = client ( & url, work.path ( ) ) ;
5733
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
5734
c.snapshot ( " test" ) .unwrap ( ) ;
5735
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
5736
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5737
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
5738
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
5739
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads.clone ( ) ) .encode ( ) ) ;
5740
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
5741
5742
5743
5744
let certify = | token : String| {
5745
let app = h.app.clone ( ) ;
5746
async move {
5747
let sk = SecretKey:: generate( ) ;
5748
let id = sk.identity ( " test-1 job" , " " , 1 ) ;
5749
let proof = sk.sign_challenge ( & id.hash ( ) , & certify_challenge ( & token) ) .unwrap ( ) ;
5750
let body = serde_json:: json! ( {
5751
" identity" : oxy_core:: dsse:: base64( & id.encode ( ) ) ,
5752
" proof" : oxy_core:: dsse:: base64( & oxy_core:: cbor:: encode( & proof.to_value ( ) ) ) ,
5753
" scope" : " seal" ,
5754
} ) ;
5755
let ( st, body) = call ( & app, " POST" , " /api/v1/keyless/certify" , Some ( & token) , Some ( body.to_string ( ) ) ) .await;
5756
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
5757
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
5758
let cert = SignedCertificate:: decode( & oxy_core:: dsse:: base64_decode( v[ " certificate" ] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ) .unwrap ( ) ;
5759
( sk, id, cert)
5760
}
5761
} ;
5762
let sign = | r : & serde_json:: Value,
5763
sk: & SecretKey,
5764
id: & oxy_core:: identity:: Identity,
5765
cert: SignedCertificate,
5766
issuer: oxy_core:: identity:: Identity| {
5767
let r: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
5768
let run_url = format! ( " {url} /flow/{} " , run.number) ;
5769
hex:: encode( SignedReceipt:: sign( r.to_oxy ( ) , repo_id, state, & run_url, id, sk) .with_certificate ( cert, issuer) .encode ( ) )
5770
} ;
5771
let hub_id = h.app.hub_key.identity.clone ( ) ;
5772
5773
let a = claim ( & h.app) .await.unwrap ( ) ;
5774
let b = claim ( & h.app) .await.unwrap ( ) ;
5775
let ph = a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5776
let ( a_name, b_name) = ( a[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) , b[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
5777
let ( a_id, b_id) = ( a[ " job_id" ] .as_i64 ( ) .unwrap ( ) , b[ " job_id" ] .as_i64 ( ) .unwrap ( ) ) ;
5778
let ( sk_a, id_a, cert_a) = certify ( a[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) .await;
5779
5780
5781
let rb = receipt ( & b_name, & ph, 0 ) ;
5782
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 300 , " receipt" : rb, " signed_receipt" : sign ( & rb, & sk_a, & id_a, cert_a.clone ( ) , hub_id.clone ( ) ) } ) ;
5783
assert_eq! ( finish ( & h.app, b_id, body) .await, StatusCode:: NO_CONTENT ) ;
5784
5785
let ra = receipt ( & a_name, & ph, 0 ) ;
5786
let body = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : ra, " signed_receipt" : sign ( & ra, & sk_a, & id_a, cert_a, hub_id.clone ( ) ) } ) ;
5787
assert_eq! ( finish ( & h.app, a_id, body) .await, StatusCode:: NO_CONTENT ) ;
5788
5789
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
5790
let job = | n : & str | jobs.iter ( ) .find ( | j | j.name = = n ) .unwrap ( ) .clone ( ) ;
5791
assert_eq! ( job ( & a_name) .receipt_attest.len ( ) , 64 , " {}" , job ( & a_name) .receipt_note) ;
5792
assert! ( job ( & a_name) .receipt_signer.ends_with ( " (short-lived key)" ) , " {}" , job ( & a_name) .receipt_signer) ;
5793
assert! ( job ( & b_name) .receipt_attest.is_empty ( ) ) ;
5794
assert! ( job ( & b_name) .receipt_note.contains ( " another run or job" ) , " {}" , job ( & b_name) .receipt_note) ;
5795
5796
let kept = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .receipts ( & heads) .unwrap ( ) ;
5797
assert_eq! ( kept.len ( ) , 1 ) ;
5798
assert! ( kept[ 0 ] .problem.is_none ( ) & & kept[ 0 ] .signed.certificate.is_some ( ) ) ;
5799
5800
5801
let t = claim ( & h.app) .await.unwrap ( ) ;
5802
let t_name = t[ " job" ] [ " name" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5803
let fake_sk = SecretKey:: generate( ) ;
5804
let fake = fake_sk.identity ( " not the hub" , " " , 1 ) ;
5805
let ( sk_t , id_t , mut cert_t ) = certify ( t[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) .await;
5806
cert_t .cert.issuer = fake.hash ( ) ;
5807
let cert_t = SignedCertificate:: sign( cert_t .cert, & fake, & fake_sk) ;
5808
let rt = receipt ( & t_name, & ph, 0 ) ;
5809
let body =
5810
serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 500 , " receipt" : rt, " signed_receipt" : sign ( & rt, & sk_t , & id_t , cert_t , fake) } ) ;
5811
assert_eq! ( finish ( & h.app, t[ " job_id" ] .as_i64 ( ) .unwrap ( ) , body) .await, StatusCode:: NO_CONTENT ) ;
5812
let jobs = db:: flow:: jobs( & h.app.db, run.id) .await.unwrap ( ) ;
5813
let t_job = jobs.iter ( ) .find ( | j | j.name = = t_name ) .unwrap ( ) ;
5814
assert! ( t_job.receipt_note.contains ( " not issued by this hub" ) , " {}" , t_job.receipt_note) ;
5815
}
3eb491e9 Andy McCall 19 days ago ↶
5816
24472413 Andy McCall 18 days ago ↶
5817
5818
5819
5820
5821
# [ tokio ::test ( flavor = " multi_thread" ) ]
5822
async fn a_release_built_by_a_certified_key_is_attested ( ) {
5823
use oxy_core:: cert:: { certify_challenge, SignedCertificate} ;
5824
use oxy_core:: hash:: { hash_object, ChangeHash, ObjectType} ;
5825
use oxy_core:: identity:: SecretKey;
5826
use oxy_core:: objects:: StateObject;
5827
use oxy_core:: receipt:: SignedReceipt;
5828
let h = hub ( ) .await;
5829
create ( & h.app, " andy" , " demo" ) .await;
5830
let url = format! ( " {} /andy/demo" , h.base) ;
5831
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5832
let mut c = client ( & url, work.path ( ) ) ;
5833
c.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
5834
write (
5835
work.path ( ) ,
5836
" .flow/pipeline.kdl" ,
5837
& format! (
5838
" schema-version 1\n sandbox \" alpine\" {{ \n rootfs \" cas://blob/{ROOTFS} \" \n }} \n job \" build\" {{ \n sandbox \" alpine\" \n exec \" make\" \n output {{ \n artifact \" tool\" {{ \n path \" /src/out/tool\" \n }} \n }} \n }} \n "
5839
) ,
5840
) ;
5841
c.snapshot ( " test" ) .unwrap ( ) ;
5842
c.commit ( " Build a tool" , & [ ] ) .unwrap ( ) ;
5843
let heads = c.bookmark_heads ( ) .unwrap ( ) ;
5844
c.tag_create ( " v1" , & heads, " first" ) .unwrap ( ) ;
5845
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5846
5847
let run = runs_of ( & h.app, " andy" , " demo" ) .await.remove ( 0 ) ;
5848
let a = claim ( & h.app) .await.unwrap ( ) ;
5849
let id = a[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
5850
let token = a[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5851
let body: Vec< u8 > = b " #!/bin/sh\n echo tool\n " .to_vec ( ) ;
5852
let ( _ , text) = upload ( & h.app, id, " tool" , & body) .await;
5853
let blob = serde_json:: from_str:: < serde_json:: Value> ( & text) .unwrap ( ) [ " blob" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
5854
let mut r = receipt ( " build" , a[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) , 0 ) ;
5855
r[ " outputs" ] = serde_json:: json! ( [ blob] ) ;
5856
let heads_run: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
5857
let state = hash_object ( ObjectType:: State, & StateObject:: new( heads_run) .encode ( ) ) ;
5858
let repo_id = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) .id ( ) .unwrap ( ) ;
5859
5860
let sk = SecretKey:: generate( ) ;
5861
let key = sk.identity ( " test-1 job" , " " , 1 ) ;
5862
let proof = sk.sign_challenge ( & key.hash ( ) , & certify_challenge ( & token) ) .unwrap ( ) ;
5863
let ask = serde_json:: json! ( {
5864
" identity" : oxy_core:: dsse:: base64( & key.encode ( ) ) ,
5865
" proof" : oxy_core:: dsse:: base64( & oxy_core:: cbor:: encode( & proof.to_value ( ) ) ) ,
5866
" scope" : " seal" ,
5867
} ) ;
5868
let ( st, answer) = call ( & h.app, " POST" , " /api/v1/keyless/certify" , Some ( & token) , Some ( ask.to_string ( ) ) ) .await;
5869
assert_eq! ( st, StatusCode:: OK , " {answer}" ) ;
5870
let v: serde_json:: Value = serde_json:: from_str( & answer) .unwrap ( ) ;
5871
let cert = SignedCertificate:: decode( & oxy_core:: dsse:: base64_decode( v[ " certificate" ] .as_str ( ) .unwrap ( ) ) .unwrap ( ) ) .unwrap ( ) ;
5872
let fr: flow_core:: Receipt = serde_json:: from_value( r.clone ( ) ) .unwrap ( ) ;
5873
let signed = hex:: encode(
5874
SignedReceipt:: sign( fr.to_oxy ( ) , repo_id, state, & format! ( " {url} /flow/{} " , run.number) , & key, & sk)
5875
.with_certificate ( cert, h.app.hub_key.identity.clone ( ) )
5876
.encode ( ) ,
5877
) ;
5878
let done = serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 900 , " receipt" : r, " signed_receipt" : signed } ) ;
5879
assert_eq! ( finish ( & h.app, id, done) .await, StatusCode:: NO_CONTENT ) ;
5880
5881
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
5882
let rel = db:: releases:: create( & h.app.db, row.id, " v1" , " One" , " " , false , None ) .await.unwrap ( ) ;
5883
oxygenhub:: web:: releases:: store_asset( & h.app, rel.id, " tool" , " " , & body) .await.unwrap ( ) ;
5884
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/releases/trust/v1" , None , None ) .await;
5885
assert_eq! ( st, StatusCode:: OK ) ;
5886
for want in [
5887
" tool: signed receipt from run #1 build" ,
5888
" a short-lived key this hub certified for repo:andy/demo:bookmark:main" ,
5889
" 1 of 1 assets built and attested by OxygenFlow" ,
5890
] {
5891
assert! ( page.contains ( want) , " missing {want:?}" ) ;
5892
}
5893
assert! ( ! page.contains ( " not one this hub" ) , " not an unknown key" ) ;
5894
}
5895
965b950f Andy McCall 18 days ago ↶
5896
5897
5898
5899
5900
# [ tokio ::test ( flavor = " multi_thread" ) ]
5901
async fn a_push_is_logged_and_the_log_checks_out ( ) {
5902
use oxy_core:: tlog:: { self , Checkpoint, Entry, Hash, Note, Tile} ;
5903
let h = hub ( ) .await;
5904
create ( & h.app, " andy" , " demo" ) .await;
5905
let url = format! ( " {} /andy/demo" , h.base) ;
5906
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5907
let mut c = client ( & url, work.path ( ) ) ;
5908
c.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
5909
write ( work.path ( ) , " a.txt" , " a\n " ) ;
5910
c.commit ( " a" , & [ ] ) .unwrap ( ) ;
5911
c.policy_init ( ) .unwrap ( ) ;
5912
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5913
let host = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
5914
let ( attest, _ ) = host.latest_ref_attest ( " main" ) .unwrap ( ) .unwrap ( ) ;
5915
5916
5917
let ( _ , sk) = oxygenhub:: keys:: load_secret( & h.app.config.keys_dir ( ) ) .unwrap ( ) ;
5918
h.app.tlog.refresh_checkpoint ( & h.app.hub_key.identity, & sk) .unwrap ( ) ;
5919
let ( st, note) = call ( & h.app, " GET" , " /log/checkpoint" , None , None ) .await;
5920
assert_eq! ( st, StatusCode:: OK , " {note}" ) ;
5921
let origin = oxygenhub:: tlog:: origin_of( h.app.config.base_url ( ) ) ;
5922
let cp = Checkpoint:: verify( & Note:: parse( & note) .unwrap ( ) , & origin, & h.app.hub_key.identity) .unwrap ( ) ;
5923
assert! ( cp.size > = 3 , " ref, seal and policy: {}" , cp.size) ;
5924
5925
5926
let bundle_path = Tile { level: 0 , n: 0 , width: cp.size as u32 } .entries_path ( ) ;
5927
let ( st, _ ) = call ( & h.app, " GET" , & format! ( " /log/{bundle_path} " ) , None , None ) .await;
5928
assert_eq! ( st, StatusCode:: OK ) ;
5929
let res =
5930
oxygenhub:: router( h.app.clone ( ) ) .oneshot ( Request:: get( format! ( " /log/{bundle_path} " ) ) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
5931
assert_eq! ( res.headers ( ) [ header:: CACHE_CONTROL ] , " public, max-age=5" , " a partial tile changes" ) ;
5932
let bytes = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
5933
let entries: Vec< Entry> = tlog:: decode_bundle( & bytes) .unwrap ( ) .iter ( ) .map ( | b | Entry:: decode( b) .unwrap ( ) ) .collect ( ) ;
5934
let kinds: Vec< & str > = entries.iter ( ) .map ( | e | e.kind.as_str ( ) ) .collect ( ) ;
5935
assert! ( kinds.contains ( & " ref" ) & & kinds.contains ( & " seal" ) & & kinds.contains ( & " policy" ) , " {kinds:?}" ) ;
5936
let r = entries.iter ( ) .find ( | e | e.kind = = " ref" ) .unwrap ( ) ;
5937
assert_eq! ( r.object, attest) ;
5938
assert_eq! ( r.repo, c.id ( ) .unwrap ( ) ) ;
5939
5940
5941
5942
let mut stored: Vec< Hash> = Vec :: new( ) ;
5943
for ( n, e) in entries.iter ( ) .enumerate ( ) {
5944
stored.extend ( tlog:: stored_hashes_for_leaf( n as u64 , e.leaf_hash ( ) , & stored) .unwrap ( ) ) ;
5945
}
5946
assert_eq! ( tlog:: tree_hash( cp.size, & stored) .unwrap ( ) , cp.root) ;
5947
let t = Tile { level: 0 , n: 0 , width: cp.size as u32 } ;
5948
let res =
5949
oxygenhub:: router( h.app.clone ( ) ) .oneshot ( Request:: get( format! ( " /log/{} " , t.path ( ) ) ) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
5950
assert_eq! ( res.status ( ) , StatusCode:: OK ) ;
5951
let data = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
5952
assert_eq! ( data.to_vec ( ) , t.read_data ( & stored) .unwrap ( ) ) ;
5953
let ( st, _ ) = call ( & h.app, " GET" , " /log/tile/0/000.p/200" , None , None ) .await;
5954
assert_eq! ( st, StatusCode:: NOT_FOUND , " not held yet" ) ;
5955
5956
5957
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5958
assert_eq! ( h.app.tlog.size ( ) , cp.size) ;
5959
let ( st, page) = call ( & h.app, " GET" , " /log" , None , None ) .await;
5960
assert_eq! ( st, StatusCode:: OK ) ;
5961
assert! ( page.contains ( " Transparency log" ) & & page.contains ( " andy/demo" ) & & page.contains ( & origin) , " {page}" ) ;
5962
}
5963
e5b97ac3 Andy McCall 18 days ago ↶
5964
5965
5966
5967
5968
5969
# [ tokio ::test ( flavor = " multi_thread" ) ]
5970
async fn a_fetch_checks_the_hub_s_log_and_refuses_a_fork ( ) {
5971
use oxy_core:: tlog:: { Checkpoint, Note} ;
5972
let h = hub ( ) .await;
5973
create ( & h.app, " andy" , " demo" ) .await;
5974
let url = format! ( " {} /andy/demo" , h.base) ;
5975
let work = tempfile:: tempdir( ) .unwrap ( ) ;
5976
let mut a = client ( & url, & work.path ( ) .join ( " a" ) ) ;
5977
a.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
5978
let ( _ , sk) = oxygenhub:: keys:: load_secret( & h.app.config.keys_dir ( ) ) .unwrap ( ) ;
5979
let hub_id = h.app.hub_key.identity.clone ( ) ;
5980
let checkpoint = | | h.app.tlog.refresh_checkpoint ( & hub_id, & sk) .unwrap ( ) ;
5981
let mut n = 0 ;
5982
let mut push = | a : & Repo| {
5983
n + = 1 ;
5984
write ( & work.path ( ) .join ( " a" ) , " a.txt" , & format! ( " {n} \n " ) ) ;
5985
a.commit ( & format! ( " change {n} " ) , & [ ] ) .unwrap ( ) ;
5986
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
5987
} ;
5988
push ( & a) ;
5989
checkpoint ( ) ;
5990
5991
let home = work.path ( ) .join ( " home-b" ) ;
5992
let b = Repo:: clone( & url, & work.path ( ) .join ( " b" ) ) .unwrap ( ) .with_home ( home.clone ( ) ) ;
5993
let report = b.fetch ( " origin" ) .unwrap ( ) ;
5994
assert! ( report.log_notes.is_empty ( ) , " {:?}" , report.log_notes) ;
5995
let pin = std:: fs:: read_dir( home.join ( " tlog" ) ) .unwrap ( ) .next ( ) .unwrap ( ) .unwrap ( ) .path ( ) ;
5996
let seen = | p : & std:: path:: Path| {
5997
let t = std:: fs:: read_to_string( p) .unwrap ( ) ;
5998
Checkpoint:: parse( & Note:: parse( t.split_once ( ' \n ' ) .unwrap ( ) .1 ) .unwrap ( ) .text) .unwrap ( )
5999
} ;
6000
let first = seen ( & pin) ;
6001
6002
6003
push ( & a) ;
6004
checkpoint ( ) ;
6005
let report = b.fetch ( " origin" ) .unwrap ( ) ;
6006
assert! ( report.log_notes.is_empty ( ) , " {:?}" , report.log_notes) ;
6007
assert! ( seen ( & pin) .size > first.size) ;
6008
6009
6010
push ( & a) ;
6011
let report = b.fetch ( " origin" ) .unwrap ( ) ;
6012
assert! ( report.log_notes.iter ( ) .any ( | n | n.contains ( " not in its transparency log yet" ) ) , " {:?}" , report.log_notes) ;
6013
7dbb41b0 Andy McCall 18 days ago ↶
6014
6015
6016
let text = std:: fs:: read_to_string( & pin) .unwrap ( ) ;
6017
let other_sk = oxy_core:: identity:: SecretKey:: generate( ) ;
6018
let other = other_sk.identity ( " another hub" , " [email protected] " , 1 ) ;
6019
let note = text.split_once ( ' \n ' ) .unwrap ( ) .1 ;
6020
std:: fs:: write( & pin, format! ( " {} \n {note} " , oxy_core:: dsse:: base64( & other.encode ( ) ) ) ) .unwrap ( ) ;
6021
let report = b.fetch ( " origin" ) .unwrap ( ) ;
6022
assert! ( report.log_notes.iter ( ) .any ( | n | n.contains ( " signed by another key" ) ) , " {:?}" , report.log_notes) ;
6023
let report = b.fetch ( " origin" ) .unwrap ( ) ;
6024
assert! ( ! report.log_notes.iter ( ) .any ( | n | n.contains ( " another key" ) ) , " pinned now: {:?}" , report.log_notes) ;
6025
e5b97ac3 Andy McCall 18 days ago ↶
6026
6027
6028
let size = seen ( & pin) .size;
6029
let forged = Checkpoint { origin: oxygenhub:: tlog:: origin_of( h.app.config.base_url ( ) ) , size, root: [ 7 ; 32 ] , extensions: vec! [ ] } ;
6030
let text = std:: fs:: read_to_string( & pin) .unwrap ( ) ;
6031
let key = text.split_once ( ' \n ' ) .unwrap ( ) .0 ;
6032
std:: fs:: write( & pin, format! ( " {key} \n {} " , forged.sign ( & hub_id, & sk) .unwrap ( ) .encode ( ) ) ) .unwrap ( ) ;
f87a74ff Andy McCall 4 days ago ↶
6033
6034
6035
push ( & a) ;
e5b97ac3 Andy McCall 18 days ago ↶
6036
checkpoint ( ) ;
6037
let err = b.fetch ( " origin" ) .unwrap_err ( ) .to_string ( ) ;
6038
assert! ( err.contains ( " showed two histories" ) & & err.contains ( " does not extend" ) , " {err}" ) ;
6039
}
6040
4f40eed0 Andy McCall 18 days ago ↶
6041
6042
6043
6044
6045
6046
# [ tokio ::test ( flavor = " multi_thread" ) ]
6047
async fn a_hub_witnesses_another_hub_s_log ( ) {
6048
use oxy_core:: tlog:: { AddCheckpoint, Checkpoint, Note} ;
6049
let log = hub ( ) .await;
6050
let witness = hub ( ) .await;
6051
let origin = oxygenhub:: tlog:: origin_of( log.app.config.base_url ( ) ) ;
6052
witness.app.tlog.add_witness_for ( & origin, log.app.hub_key.identity.clone ( ) ) ;
6053
let name = witness.app.tlog.witness_name ( ) .to_string ( ) ;
6054
log.app.tlog.set_witnesses ( vec! [ oxygenhub:: tlog:: Peer {
6055
name: name.clone ( ) ,
6056
url: format! ( " {} /log/witness/add-checkpoint" , witness.base) ,
6057
identity: witness.app.hub_key.identity.clone ( ) ,
6058
} ] ) ;
6059
create ( & log.app, " andy" , " demo" ) .await;
6060
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6061
let mut c = client ( & format! ( " {} /andy/demo" , log.base) , work.path ( ) ) ;
6062
c.create_identity ( " Andy" , " [email protected] " , oxy_core:: repo:: IdentityScope:: Repo) .unwrap ( ) ;
6063
let ( _ , sk) = oxygenhub:: keys:: load_secret( & log.app.config.keys_dir ( ) ) .unwrap ( ) ;
6064
let log_id = log.app.hub_key.identity.clone ( ) ;
6065
let wid = witness.app.hub_key.identity.clone ( ) ;
6066
for n in 0 .. 2 {
6067
write ( work.path ( ) , " a.txt" , & format! ( " {n} \n " ) ) ;
6068
c.commit ( & format! ( " change {n} " ) , & [ ] ) .unwrap ( ) ;
6069
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6070
log.app.tlog.refresh_checkpoint ( & log_id, & sk) .unwrap ( ) ;
6071
oxygenhub:: tlog:: collect( & log.app) .await;
6072
let note = Note:: parse( & log.app.tlog.checkpoint ( ) .unwrap ( ) ) .unwrap ( ) ;
6073
assert! ( note.cosigned_by ( & name, & wid) .is_ok ( ) , " round {n}: {}" , note.encode ( ) ) ;
6074
6075
let ( _ , served) = call ( & log.app, " GET" , " /log/checkpoint" , None , None ) .await;
6076
assert! ( Note:: parse( & served) .unwrap ( ) .cosigned_by ( & name, & wid) .is_ok ( ) ) ;
6077
}
6078
let cp = Checkpoint:: parse( & Note:: parse( & log.app.tlog.checkpoint ( ) .unwrap ( ) ) .unwrap ( ) .text) .unwrap ( ) ;
13b27cb2 Andy McCall 17 days ago ↶
6079
6080
6081
6082
6083
let ( _ , listed) = call ( & log.app, " GET" , " /log/witnesses" , None , None ) .await;
6084
assert! ( listed.starts_with ( & format! ( " {name} " ) ) , " {listed}" ) ;
6085
let url = format! ( " {} /andy/demo" , log.base) ;
6086
let refs = tokio:: task:: spawn_blocking( move | | {
6087
let opts = RemoteOptions { token: Some ( " site" .into ( ) ) , .. Default :: default( ) } ;
6088
open_remote ( & url, & opts) .unwrap ( ) .refs ( ) .unwrap ( )
6089
} )
6090
.await
6091
.unwrap ( ) ;
6092
assert_eq! ( refs.witnesses.len ( ) , 1 ) ;
6093
assert_eq! ( refs.witnesses[ 0 ] .0 , name) ;
6094
let main = refs.refs.iter ( ) .find ( | r | r.name = = " main" ) .expect ( " main" ) ;
6095
let logged = main.logged.as_ref ( ) .expect ( " logged" ) ;
6096
assert! ( ! main.integration.is_empty ( ) , " a witnessed checkpoint holds it" ) ;
6097
let cosigned: Vec< ( & str , & [[u8 ; 32]]) > = main.integration.iter ( ) .map ( | i | ( i.note.as_str ( ) , i.proof.as_slice ( ) ) ) .collect ( ) ;
6098
let at = oxy_core:: tlog:: integrated_time( & logged.entry, logged.index, & cosigned, & origin, & log_id, & [ ( name.clone ( ) , wid.clone ( ) ) ] )
6099
.expect ( " an integrated time" ) ;
6100
assert_eq! ( at.witness, name) ;
6101
assert! ( at.size > logged.index) ;
4f40eed0 Andy McCall 18 days ago ↶
6102
6103
let ask = | req : AddCheckpoint| {
6104
let app = witness.app.clone ( ) ;
6105
async move { call ( & app, " POST" , " /log/witness/add-checkpoint" , None , Some ( req.encode ( ) ) ) .await }
6106
} ;
6107
6108
let forked = Checkpoint { root: [ 7 ; 32 ] , .. cp.clone ( ) } .sign ( & log_id, & sk) .unwrap ( ) .encode ( ) ;
6109
let ( st, body) = ask ( AddCheckpoint { old_size: cp.size, proof: vec! [ ] , note: forked.clone ( ) } ) .await;
6110
assert_eq! ( st, StatusCode:: UNPROCESSABLE_ENTITY , " {body}" ) ;
6111
6112
let ( st, body) = ask ( AddCheckpoint { old_size: 1 , proof: vec! [ ] , note: forked } ) .await;
6113
assert_eq! ( ( st, body.trim ( ) ) , ( StatusCode:: CONFLICT , cp.size.to_string ( ) .as_str ( ) ) ) ;
6114
6115
let stranger = oxy_core:: identity:: SecretKey:: generate( ) ;
6116
let sid = stranger.identity ( " elsewhere" , " [email protected] " , 1 ) ;
6117
let other = Checkpoint { origin: " elsewhere.example/log" .into ( ) , .. cp.clone ( ) } .sign ( & sid, & stranger) .unwrap ( ) .encode ( ) ;
6118
let ( st, _ ) = ask ( AddCheckpoint { old_size: 0 , proof: vec! [ ] , note: other } ) .await;
6119
assert_eq! ( st, StatusCode:: NOT_FOUND ) ;
6120
}
6121
8dcaeeb8 Andy McCall 18 days ago ↶
6122
6123
6124
6125
6126
# [ tokio ::test ( flavor = " multi_thread" ) ]
6127
async fn a_job_keeps_its_terraform_state_on_the_hub ( ) {
6128
let h = hub ( ) .await;
6129
create ( & h.app, " andy" , " infra" ) .await;
6130
create ( & h.app, " andy" , " other" ) .await;
6131
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6132
let c = client ( & format! ( " {} /andy/infra" , h.base) , work.path ( ) ) ;
6133
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
6134
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
6135
c.snapshot ( " test" ) .unwrap ( ) ;
6136
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
6137
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6138
assert_eq! ( runs_of ( & h.app, " andy" , " infra" ) .await.len ( ) , 1 ) ;
6139
let job = claim ( & h.app) .await.expect ( " a job" ) ;
6140
let token = job[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
6141
let basic = format! ( " Basic {} " , oxy_core:: dsse:: base64( format! ( " flow:{token} " ) .as_bytes ( ) ) ) ;
6142
let path = " /api/v1/repos/andy/infra/terraform/state/prod" ;
6143
let send = | method : & str , uri : String, auth : Option< String> , body : & str | {
6144
let mut req = Request:: builder( ) .method ( method) .uri ( uri) ;
6145
if let Some ( a) = auth {
6146
req = req.header ( header:: AUTHORIZATION , a) ;
6147
}
6148
let req = req.body ( Body:: from( body.to_string ( ) ) ) .unwrap ( ) ;
6149
let app = h.app.clone ( ) ;
6150
async move {
6151
let res = oxygenhub:: router( app) .oneshot ( req) .await.unwrap ( ) ;
6152
let st = res.status ( ) ;
6153
let b = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
6154
( st, String :: from_utf8_lossy( & b) .into_owned ( ) )
6155
}
6156
} ;
6157
let lock = r # "{"ID":"lock-1","Operation":"OperationTypeApply","Who":"flow@runner","Version":"1.8.0"}"# ;
6158
6159
assert_eq! ( send ( " GET" , path.into ( ) , None , " " ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
6160
assert_eq! ( send ( " GET" , path.into ( ) , Some ( basic.clone ( ) ) , " " ) .await.0 , StatusCode:: NO_CONTENT ) ;
6161
6162
let ( st, body) = send ( " LOCK" , path.into ( ) , Some ( basic.clone ( ) ) , lock) .await;
6163
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
6164
let other_lock = r # "{"ID":"lock-2","Operation":"OperationTypePlan"}"# ;
6165
let ( st, held) = send ( " LOCK" , path.into ( ) , Some ( basic.clone ( ) ) , other_lock) .await;
6166
assert_eq! ( st, StatusCode:: LOCKED ) ;
6167
assert! ( held.contains ( " lock-1" ) , " the holder's lock info: {held}" ) ;
6168
let state1 = r # "{"version":4,"serial":1,"lineage":"l","resources":[{"secret":"hunter2"}]}"# ;
6169
assert_eq! ( send ( " POST" , path.into ( ) , Some ( basic.clone ( ) ) , state1) .await.0 , StatusCode:: LOCKED , " not under its lock" ) ;
6170
assert_eq! ( send ( " POST" , format! ( " {path} ?ID=lock-1" ) , Some ( basic.clone ( ) ) , state1) .await.0 , StatusCode:: OK ) ;
6171
assert_eq! ( send ( " UNLOCK" , path.into ( ) , Some ( basic.clone ( ) ) , other_lock) .await.0 , StatusCode:: CONFLICT , " not its lock" ) ;
6172
assert_eq! ( send ( " UNLOCK" , path.into ( ) , Some ( basic.clone ( ) ) , lock) .await.0 , StatusCode:: OK ) ;
6173
let ( st, got) = send ( " GET" , path.into ( ) , Some ( basic.clone ( ) ) , " " ) .await;
6174
assert_eq! ( ( st, got.as_str ( ) ) , ( StatusCode:: OK , state1) ) ;
6175
6176
let state2 = state1.replace ( " \" serial\" :1" , " \" serial\" :2" ) ;
6177
assert_eq! ( send ( " POST" , path.into ( ) , Some ( basic.clone ( ) ) , & state2) .await.0 , StatusCode:: OK ) ;
6178
let row = db:: repos:: get( & h.app.db, " andy" , " infra" ) .await.unwrap ( ) .unwrap ( ) ;
6179
assert_eq! ( db:: terraform:: versions( & h.app.db, row.id, " prod" ) .await.unwrap ( ) , 1 ) ;
6180
let kept = db:: terraform:: get( & h.app.db, row.id, " prod" ) .await.unwrap ( ) .unwrap ( ) ;
6181
assert_eq! ( kept.serial, 2 ) ;
6182
assert! ( ! String :: from_utf8_lossy( & kept.body) .contains ( " hunter2" ) , " sealed at rest" ) ;
6183
6184
let ( st, _ ) = send ( " GET" , " /api/v1/repos/andy/other/terraform/state/prod" .into ( ) , Some ( basic.clone ( ) ) , " " ) .await;
6185
assert_eq! ( st, StatusCode:: FORBIDDEN ) ;
6186
assert_eq! (
6187
send ( " GET" , " /api/v1/repos/andy/infra/terraform/state/..x" .into ( ) , Some ( basic.clone ( ) ) , " " ) .await.0 ,
6188
StatusCode:: BAD_REQUEST
6189
) ;
bb018142 Andy McCall 18 days ago ↶
6190
6191
6192
let mallory = db:: accounts:: create_account( & h.app.db, " mallory" , " [email protected] " , " x" , " salt" , false ) .await.unwrap ( ) ;
6193
let ( secret, hash) = oxygenhub:: auth:: new_token( ) ;
6194
db:: accounts:: create_token( & h.app.db, mallory.id, " cli" , & hash, " repo:write" , " " , None ) .await.unwrap ( ) ;
6195
let ( st, got) = send ( " GET" , path.into ( ) , Some ( format! ( " Bearer {secret} " ) ) , " " ) .await;
6196
assert_eq! ( st, StatusCode:: FORBIDDEN , " {got}" ) ;
6197
assert! ( ! got.contains ( " hunter2" ) ) ;
6198
let ( _ , page) = call ( & h.app, " GET" , " /andy/infra/terraform" , Some ( & secret) , None ) .await;
6199
assert! ( ! page.contains ( " <strong>prod</strong>" ) & & page.contains ( " shown to people who can write" ) , " {page}" ) ;
8dcaeeb8 Andy McCall 18 days ago ↶
6200
6201
let ( st, _ ) = send ( " GET" , path.into ( ) , Some ( " Bearer site" .into ( ) ) , " " ) .await;
6202
assert_eq! ( st, StatusCode:: UNAUTHORIZED , " the site token is not a person's token" ) ;
6203
6204
6205
let issuer = h.app.oidc ( ) .await.unwrap ( ) ;
6206
let now = db:: now_ms( ) ;
6207
let aud = oxygenhub:: terraform:: audience( h.app.config.base_url ( ) ) ;
6208
let own = issuer.mint_for ( h.app.config.base_url ( ) , " repo:andy/infra:bookmark:main" , & aud, now, 600 ) ;
6209
let ( st, got) = send ( " GET" , path.into ( ) , Some ( format! ( " Bearer {own} " ) ) , " " ) .await;
6210
assert_eq! ( ( st, got.contains ( " \" serial\" :2" ) ) , ( StatusCode:: OK , true ) ) ;
6211
let other = issuer.mint_for ( h.app.config.base_url ( ) , " repo:andy/other:bookmark:main" , & aud, now, 600 ) ;
6212
assert_eq! ( send ( " GET" , path.into ( ) , Some ( format! ( " Bearer {other} " ) ) , " " ) .await.0 , StatusCode:: FORBIDDEN ) ;
6213
let elsewhere = issuer.mint_for ( h.app.config.base_url ( ) , " repo:andy/infra:bookmark:main" , " sts.amazonaws.com" , now, 600 ) ;
6214
assert_eq! ( send ( " GET" , path.into ( ) , Some ( format! ( " Bearer {elsewhere} " ) ) , " " ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
16ee5d19 Andy McCall 18 days ago ↶
6215
bb018142 Andy McCall 18 days ago ↶
6216
6217
let res = oxygenhub:: router( h.app.clone ( ) )
6218
.oneshot (
6219
Request:: post( " /register" )
6220
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
6221
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
6222
.unwrap ( ) ,
6223
)
6224
.await
6225
.unwrap ( ) ;
6226
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
6227
let res = oxygenhub:: router( h.app.clone ( ) )
67523e06 Andy McCall 18 days ago ↶
6228
.oneshot ( Request:: get( " /andy/infra/terraform" ) .header ( header:: COOKIE , cookie.clone ( ) ) .body ( Body:: empty( ) ) .unwrap ( ) )
bb018142 Andy McCall 18 days ago ↶
6229
.await
6230
.unwrap ( ) ;
6231
let st = res.status ( ) ;
6232
let page = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
16ee5d19 Andy McCall 18 days ago ↶
6233
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
6234
assert! ( page.contains ( " <strong>prod</strong>" ) & & page.contains ( " /api/v1/repos/andy/infra/terraform/state/NAME" ) , " {page}" ) ;
6235
assert! ( page.contains ( " allow "" ) | | page.contains ( " allow \" " ) , " the egress line names the hub" ) ;
67523e06 Andy McCall 18 days ago ↶
6236
assert! ( page.contains ( " 1 kept version" ) & & page.contains ( " >restore</button>" ) , " its versions, restorable by its owner: {page}" ) ;
6237
6238
6239
6240
6241
let andy = db:: accounts:: account_by_name( & h.app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
6242
let ( pat, hash) = oxygenhub:: auth:: new_token( ) ;
6243
db:: accounts:: create_token( & h.app.db, andy.id, " cli" , & hash, " repo:write" , " " , None ) .await.unwrap ( ) ;
6244
let bearer = format! ( " Bearer {pat} " ) ;
6245
let ( st, list) = send ( " GET" , format! ( " {path} /versions" ) , Some ( bearer.clone ( ) ) , " " ) .await;
6246
assert_eq! ( st, StatusCode:: OK , " {list}" ) ;
6247
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
6248
assert_eq! ( ( list.as_array ( ) .unwrap ( ) .len ( ) , list[ 0 ] [ " serial" ] .as_i64 ( ) ) , ( 1 , Some ( 1 ) ) , " {list}" ) ;
6249
let first = list[ 0 ] [ " id" ] .as_i64 ( ) .unwrap ( ) ;
6250
let ( st, got) = send ( " GET" , format! ( " {path} /versions/{first} " ) , Some ( bearer.clone ( ) ) , " " ) .await;
6251
assert_eq! ( ( st, got.as_str ( ) ) , ( StatusCode:: OK , state1) ) ;
6252
assert_eq! ( send ( " GET" , format! ( " {path} /versions" ) , Some ( format! ( " Bearer {secret} " ) ) , " " ) .await.0 , StatusCode:: FORBIDDEN , " a stranger" ) ;
6253
let restore = | id : i64 , auth : String| send ( " POST" , format! ( " {path} /versions/{id} /restore" ) , Some ( auth) , " " ) ;
6254
assert_eq! ( restore ( first, basic.clone ( ) ) .await.0 , StatusCode:: FORBIDDEN , " a job does not restore" ) ;
6255
assert_eq! ( restore ( first, format! ( " Bearer {secret} " ) ) .await.0 , StatusCode:: FORBIDDEN , " nor a stranger" ) ;
6256
assert_eq! ( restore ( 9999 , bearer.clone ( ) ) .await.0 , StatusCode:: NOT_FOUND ) ;
6257
assert_eq! ( send ( " LOCK" , path.into ( ) , Some ( basic.clone ( ) ) , lock) .await.0 , StatusCode:: OK ) ;
6258
assert_eq! ( restore ( first, bearer.clone ( ) ) .await.0 , StatusCode:: LOCKED , " not under someone's lock" ) ;
6259
assert_eq! ( send ( " UNLOCK" , path.into ( ) , Some ( basic.clone ( ) ) , lock) .await.0 , StatusCode:: OK ) ;
6260
let ( st, got) = restore ( first, bearer.clone ( ) ) .await;
6261
assert_eq! ( ( st, got.contains ( " \" serial\" :3" ) ) , ( StatusCode:: OK , true ) , " {got}" ) ;
6262
let ( _ , now) = send ( " GET" , path.into ( ) , Some ( bearer.clone ( ) ) , " " ) .await;
6263
let now: serde_json:: Value = serde_json:: from_str( & now) .unwrap ( ) ;
6264
assert_eq! (
6265
( now[ " serial" ] .as_i64 ( ) , now[ " lineage" ] .as_str ( ) , now[ " resources" ] [ 0 ] [ " secret" ] .as_str ( ) ) ,
6266
( Some ( 3 ) , Some ( " l" ) , Some ( " hunter2" ) )
6267
) ;
6268
6269
let ( _ , list) = send ( " GET" , format! ( " {path} /versions" ) , Some ( bearer.clone ( ) ) , " " ) .await;
6270
let list: serde_json:: Value = serde_json:: from_str( & list) .unwrap ( ) ;
6271
let serials: Vec< i64 > = list.as_array ( ) .unwrap ( ) .iter ( ) .map ( | v | v[ " serial" ] .as_i64 ( ) .unwrap ( ) ) .collect ( ) ;
6272
assert_eq! ( serials, vec! [ 2 , 1 ] , " {list}" ) ;
6273
let ( st, got) = restore ( list[ 0 ] [ " id" ] .as_i64 ( ) .unwrap ( ) , bearer.clone ( ) ) .await;
6274
assert_eq! ( ( st, got.contains ( " \" serial\" :4" ) ) , ( StatusCode:: OK , true ) , " {got}" ) ;
6275
6276
6277
let page_with = | req : axum:: http:: request:: Builder, body : Body| {
6278
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
6279
async move {
6280
let res = oxygenhub:: router( app) .oneshot ( req.header ( header:: COOKIE , cookie) .body ( body) .unwrap ( ) ) .await.unwrap ( ) ;
6281
let st = res.status ( ) ;
6282
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
6283
}
6284
} ;
6285
let ( st, file) = page_with ( Request:: get( format! ( " /andy/infra/terraform/prod/download?version={first} " ) ) , Body:: empty( ) ) .await;
6286
assert_eq! ( ( st, file.as_str ( ) ) , ( StatusCode:: OK , state1) ) ;
6287
let ( _ , page) = page_with ( Request:: get( " /andy/infra/terraform" ) , Body:: empty( ) ) .await;
6288
let csrf = page.split ( " name=\" _csrf\" value=\" " ) .nth ( 1 ) .unwrap ( ) .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( ) ;
6289
let ( st, page) = page_with (
6290
Request:: post( " /andy/infra/terraform/prod/restore" ) .header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ,
6291
Body:: from( format! ( " _csrf={csrf} &version={first} " ) ) ,
6292
)
6293
.await;
6294
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
6295
assert! ( page.contains ( " Restored prod: it is now serial 5." ) , " {page}" ) ;
6296
let audit = db:: accounts:: audit_rows( & h.app.db, 50 ) .await.unwrap ( ) ;
6297
assert_eq! ( audit.iter ( ) .filter ( | r | r.action = = " terraform.restored" ) .count ( ) , 3 , " every restore audited" ) ;
8dcaeeb8 Andy McCall 18 days ago ↶
6298
}
6299
6300
6301
6302
6303
6304
# [ tokio ::test ( flavor = " multi_thread" ) ]
6305
# [ ignore ]
6306
async fn tofu_keeps_its_state_on_the_hub ( ) {
6307
let h = hub ( ) .await;
6308
create ( & h.app, " andy" , " infra" ) .await;
6309
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6310
let c = client ( & format! ( " {} /andy/infra" , h.base) , work.path ( ) ) ;
6311
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
6312
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
6313
c.snapshot ( " test" ) .unwrap ( ) ;
6314
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
6315
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6316
assert_eq! ( runs_of ( & h.app, " andy" , " infra" ) .await.len ( ) , 1 ) ;
6317
let job = claim ( & h.app) .await.expect ( " a job" ) ;
6318
let token = job[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
6319
let tf = tempfile:: tempdir( ) .unwrap ( ) ;
6320
let address = format! ( " {} /api/v1/repos/andy/infra/terraform/state/prod" , h.base) ;
6321
std:: fs:: write(
6322
tf.path ( ) .join ( " main.tf" ) ,
6323
format! (
6324
" terraform {{ \n backend \" http\" {{ \n address = \" {address} \" \n lock_address = \" {address} \" \n unlock_address = \" {address} \" \n username = \" flow\" \n }} \n }} \n resource \" terraform_data\" \" x\" {{ \n input = \" hello\" \n }} \n "
6325
) ,
6326
)
6327
.unwrap ( ) ;
6328
let tofu = | args : & [& str ]| {
6329
let out = std:: process:: Command:: new( " docker" )
6330
.args ( [ " run" , " --rm" , " --network" , " host" , " -v" ] )
6331
.arg ( format! ( " {} :/w" , tf.path ( ) .display ( ) ) )
6332
.args ( [ " -w" , " /w" , " -e" ] )
6333
.arg ( format! ( " TF_HTTP_PASSWORD={token} " ) )
6334
.args ( [ " --entrypoint" , " tofu" , " oxygenflow-tofu:latest" ] )
6335
.args ( args)
6336
.output ( )
6337
.expect ( " docker" ) ;
6338
let text = format! ( " {} {} " , String :: from_utf8_lossy( & out.stdout) , String :: from_utf8_lossy( & out.stderr) ) ;
6339
assert! ( out.status.success ( ) , " tofu {args:?}: {text}" ) ;
6340
text
6341
} ;
6342
tofu ( & [ " init" , " -input=false" , " -no-color" ] ) ;
6343
let applied = tofu ( & [ " apply" , " -auto-approve" , " -input=false" , " -no-color" ] ) ;
6344
assert! ( applied.contains ( " 1 added" ) , " {applied}" ) ;
6345
let again = tofu ( & [ " apply" , " -auto-approve" , " -input=false" , " -no-color" ] ) ;
6346
assert! ( again.contains ( " No changes" ) , " the state was kept: {again}" ) ;
6347
let row = db:: repos:: get( & h.app.db, " andy" , " infra" ) .await.unwrap ( ) .unwrap ( ) ;
6348
let kept = db:: terraform:: get( & h.app.db, row.id, " prod" ) .await.unwrap ( ) .unwrap ( ) ;
6349
assert! ( kept.lock_id.is_empty ( ) , " unlocked after each run" ) ;
6350
assert! ( kept.serial > = 1 ) ;
6351
}
6352
8a48dd83 Andy McCall 18 days ago ↶
6353
6354
6355
# [ tokio ::test ( flavor = " multi_thread" ) ]
6356
async fn metrics_are_private_and_count_flow_jobs ( ) {
6357
let h = hub ( ) .await;
6358
let ( st, _ ) = call ( & h.app, " GET" , " /metrics" , None , None ) .await;
6359
assert_eq! ( st, StatusCode:: NOT_FOUND , " off by default" ) ;
6360
let text = oxygenhub:: metrics:: render( & h.app) .await;
6361
for want in [
6362
" # TYPE oxyhub_uptime_seconds gauge" ,
6363
" oxyhub_flow_jobs{state=\" queued\" } 0" ,
6364
" oxyhub_bucket_requests_total{kind=\" list\" }" ,
6365
" oxyhub_tlog_entries" ,
a6d824c5 Andy McCall 18 days ago ↶
6366
" oxyhub_db_connections{idle=\" true\" }" ,
6367
" oxyhub_pushes_last_day{result=\" refused\" } 0" ,
6368
" # TYPE oxyhub_backup_last_run_ok gauge" ,
2c47eecf Andy McCall 18 days ago ↶
6369
" # TYPE oxyhub_flow_runner_last_seen_seconds gauge" ,
6370
" oxyhub_flow_runs_last_day{result=\" failed\" } 0" ,
6371
" oxyhub_mail_total{result=\" sent\" }" ,
8a48dd83 Andy McCall 18 days ago ↶
6372
] {
6373
assert! ( text.contains ( want) , " missing {want}:\n {text}" ) ;
6374
}
6375
create ( & h.app, " andy" , " demo" ) .await;
6376
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6377
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
6378
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
6379
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
6380
c.snapshot ( " test" ) .unwrap ( ) ;
6381
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
6382
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6383
assert_eq! ( runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) , 1 ) ;
6384
let text = oxygenhub:: metrics:: render( & h.app) .await;
6385
assert! ( ! text.contains ( " oxyhub_flow_jobs{state=\" queued\" } 0" ) , " the run's jobs are queued:\n {text}" ) ;
a6d824c5 Andy McCall 18 days ago ↶
6386
assert! ( text.contains ( " oxyhub_pushes_last_day{result=\" accepted\" }" ) , " {text}" ) ;
85ec74fd Andy McCall 18 days ago ↶
6387
6388
6389
let pushed = text
6390
.lines ( )
6391
.find_map ( | l | l.strip_prefix ( " oxyhub_protocol_bytes_total{endpoint=\" push\" ,direction=\" received\" } " ) )
6392
.and_then ( | v | v.parse:: < f64 > ( ) .ok ( ) )
6393
.unwrap_or ( 0. 0 ) ;
6394
assert! ( pushed > 0. 0 , " {text}" ) ;
b9b74d99 Andy McCall 18 days ago ↶
6395
6396
call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
6397
let text = oxygenhub:: metrics:: render( & h.app) .await;
6398
assert! ( text.contains ( " oxyhub_http_requests_total{route=\" page:repository\" ,method=\" GET\" ,status=\" 2xx\" }" ) , " {text}" ) ;
6399
assert! ( text.contains ( " oxyhub_http_request_duration_seconds_bucket{route=\" page:repository\" ,method=\" GET\" ,le=\" +Inf\" }" ) ) ;
8a48dd83 Andy McCall 18 days ago ↶
6400
}
6401
e97500b7 Andy McCall 18 days ago ↶
6402
6403
6404
6405
6406
# [ tokio ::test ( flavor = " multi_thread" ) ]
6407
async fn insights_chart_pickup_from_when_a_job_could_run ( ) {
6408
let h = hub ( ) .await;
6409
create ( & h.app, " andy" , " demo" ) .await;
6410
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6411
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
6412
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
6413
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
6414
c.snapshot ( " test" ) .unwrap ( ) ;
6415
c.commit ( " Add a pipeline" , & [ ] ) .unwrap ( ) ;
6416
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6417
assert_eq! ( runs_of ( & h.app, " andy" , " demo" ) .await.len ( ) , 1 ) ;
6418
let mut finished = std:: collections:: HashMap:: new( ) ;
6419
for want in [ " build" , " lint" , " test" ] {
6420
let j = claim ( & h.app) .await.expect ( " a job" ) ;
6421
assert_eq! ( j[ " job" ] [ " name" ] .as_str ( ) , Some ( want) ) ;
6422
let id = j[ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
6423
let row = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) ;
6424
let ( queued, ready, started) = ( row.queued_ms.unwrap ( ) , row.ready_ms.unwrap ( ) , row.started_ms.unwrap ( ) ) ;
6425
assert! ( queued < = ready & & ready < = started, " {want}: {queued} {ready} {started}" ) ;
6426
if want = = " test" {
6427
assert_eq! ( Some ( ready) , finished[ " build" ] , " ready when build passed, not when it was queued" ) ;
6428
}
6429
let ph = j[ " pipeline_hash" ] .as_str ( ) .unwrap ( ) ;
6430
let st = finish ( & h.app, id, serde_json:: json! ( { " exit_code" : 0 , " duration_ms" : 700 , " receipt" : receipt ( want, ph, 0 ) } ) ) .await;
6431
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
6432
finished.insert ( want, db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .finished_ms) ;
6433
}
6434
let ( st, page) = call ( & h.app, " GET" , " /andy/demo/insights" , None , None ) .await;
6435
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
6436
assert! ( page.contains ( " <span class=\" kpi-v\" >3</span><span class=\" kpi-l\" >jobs finished" ) , " {page}" ) ;
6437
assert! ( page.contains ( " 100% passed" ) , " {page}" ) ;
6438
assert_eq! ( page.matches ( " <svg class=\" chart\" " ) .count ( ) , 5 , " five charts" ) ;
6439
assert! ( page.contains ( " class=\" bar s-ok\" " ) , " the passed jobs drawn" ) ;
6440
assert! ( page.contains ( " data-refresh=\" 30\" data-fragment=\" insights\" " ) , " it refreshes itself" ) ;
6441
assert! ( page.contains ( " <summary class=\" on\" >More</summary>" ) , " the bar's More menu holds it" ) ;
6442
let ( _ , wall) = call ( & h.app, " GET" , " /andy/demo/insights?range=7d&wall=1" , None , None ) .await;
6443
assert! ( wall.contains ( " <body class=\" wall\" " ) & & wall.contains ( " the last 7 days" ) & & wall.contains ( " leave the wall view" ) , " {wall}" ) ;
6444
6445
assert_eq! ( call ( & h.app, " GET" , " /insights/andy" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
6446
assert_eq! ( call ( & h.app, " GET" , " /admin/insights" , None , None ) .await.0 , StatusCode:: NOT_FOUND ) ;
6447
let res = oxygenhub:: router( h.app.clone ( ) )
6448
.oneshot (
6449
Request:: post( " /register" )
6450
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
6451
.body ( Body:: from( " username=andy&email=a%40example.com&password=correct-horse-battery&setup=test-setup" ) )
6452
.unwrap ( ) ,
6453
)
6454
.await
6455
.unwrap ( ) ;
6456
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
6457
let andy = db:: accounts:: account_by_name( & h.app.db, " andy" ) .await.unwrap ( ) .unwrap ( ) ;
6458
db:: accounts:: set_admin( & h.app.db, andy.id, true ) .await.unwrap ( ) ;
6459
for path in [ " /insights/andy" , " /admin/insights?runner=test-1&range=1h" ] {
6460
let res = oxygenhub:: router( h.app.clone ( ) )
6461
.oneshot ( Request:: get( path) .header ( header:: COOKIE , cookie.clone ( ) ) .body ( Body:: empty( ) ) .unwrap ( ) )
6462
.await
6463
.unwrap ( ) ;
6464
let st = res.status ( ) ;
6465
let body = String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) ;
6466
assert_eq! ( st, StatusCode:: OK , " {path}: {body}" ) ;
6467
assert! ( body.contains ( " <svg class=\" chart\" " ) , " {path}" ) ;
6468
}
6469
}
6470
7c06f5b2 Andy McCall 18 days ago ↶
6471
6472
6473
6474
# [ tokio ::test ( flavor = " multi_thread" ) ]
6475
async fn reads_do_not_wait_for_a_writer ( ) {
6476
use std:: time:: Duration;
6477
let h = hub ( ) .await;
6478
create ( & h.app, " andy" , " demo" ) .await;
6479
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6480
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
6481
write ( work.path ( ) , " a.txt" , " a\n " ) ;
6482
c.commit ( " A change" , & [ ] ) .unwrap ( ) ;
6483
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6484
let name = RepoName:: parse( " andy/demo" ) .unwrap ( ) ;
6485
let writer = h.app.repos.write_guard ( & name) .await;
6486
for path in [ " /andy/demo" , " /andy/demo/changes/main" , " /api/v1/repos/andy/demo/bookmarks" ] {
6487
let ( st, body) = tokio:: time:: timeout( Duration:: from_secs( 10 ) , call ( & h.app, " GET" , path, None , None ) )
6488
.await
6489
.unwrap_or_else ( | _| panic! ( " {path} waited for the writer" ) ) ;
6490
assert_eq! ( st, StatusCode:: OK , " {path}: {body}" ) ;
6491
}
6492
let refs = tokio:: time:: timeout( Duration:: from_secs( 10 ) , call ( & h.app, " POST" , " /andy/demo/oxy/v1/refs" , Some ( " site" ) , None ) ) .await;
6493
assert! ( refs.is_ok ( ) , " a fetch's refs waited for the writer" ) ;
6494
assert! ( tokio:: time:: timeout( Duration:: from_millis( 300 ) , h.app.repos.write_guard ( & name) ) .await.is_err ( ) , " writers take turns" ) ;
6495
assert! ( tokio:: time:: timeout( Duration:: from_millis( 300 ) , h.app.repos.exclusive_guard ( & name) ) .await.is_err ( ) , " a delete waits" ) ;
6496
drop ( writer) ;
6497
let all = tokio:: time:: timeout( Duration:: from_secs( 5 ) , h.app.repos.exclusive_guard ( & name) ) .await.expect ( " then it runs" ) ;
6498
assert! (
6499
tokio:: time:: timeout( Duration:: from_millis( 300 ) , h.app.repos.read_guard ( & name) ) .await.is_err ( ) ,
6500
" nothing reads a repository being deleted"
6501
) ;
6502
drop ( all) ;
6503
}
6504
a10446c6 Andy McCall 18 days ago ↶
6505
6506
6507
6508
6509
# [ tokio ::test ( flavor = " multi_thread" ) ]
6510
async fn the_issue_overview_and_diffusion_pages_listen_on_their_topics ( ) {
6511
let h = hub ( ) .await;
6512
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
6513
create ( & h.app, " andy" , " demo" ) .await;
6514
let row = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
6515
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6516
let c = client ( & format! ( " {} /andy/demo" , h.base) , work.path ( ) ) ;
6517
write ( work.path ( ) , " a.txt" , " a\n " ) ;
6518
c.snapshot ( " t" ) .unwrap ( ) ;
6519
c.commit ( " a" , & [ ] ) .unwrap ( ) ;
6520
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6521
let ( st, body) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/issues" , Some ( " site" ) , Some ( r # "{"title":"Broken"}"# .into ( ) ) ) .await;
6522
assert! ( st.is_success ( ) , " {st} {body}" ) ;
6523
let repo = format! ( " repo:{} " , row.id) ;
6524
let ( _ , issue) = call ( & h.app, " GET" , " /andy/demo/issue/1" , None , None ) .await;
6525
assert! ( issue.contains ( & format! ( " data-live=\" {repo} /issue:1 {repo} \" data-fragment=\" timeline\" " ) ) , " the issue's timeline" ) ;
6526
let ( _ , overview) = call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
6527
assert! ( overview.contains ( & format! ( " data-live=\" {repo} \" data-fragment=\" latest\" " ) ) , " the latest change" ) ;
6528
assert! ( overview.contains ( & format! ( " data-live=\" {repo} \" data-fragment=\" bookmarks\" " ) ) , " the bookmarks" ) ;
6529
let ( _ , diffusion) = call ( & h.app, " GET" , " /andy/demo/diffusion" , None , None ) .await;
6530
assert! ( diffusion.contains ( & format! ( " data-live=\" diffusion:{} {repo} \" data-fragment=\" diffusion\" " , row.id) ) , " the Diffusion tab" ) ;
6531
}
6532
3eb491e9 Andy McCall 19 days ago ↶
6533
6534
6535
6536
6537
# [ tokio ::test ( flavor = " multi_thread" ) ]
6538
async fn the_hub_countersigns_an_update_by_a_key_it_certified ( ) {
6539
use oxy_core:: attest:: AttestBody;
6540
use oxy_core:: cert:: { Certificate, SignedCertificate} ;
6541
use oxy_core:: identity:: Scope;
6542
use oxy_core:: repo:: IdentityScope;
6543
let h = hub ( ) .await;
6544
create ( & h.app, " andy" , " demo" ) .await;
6545
let url = format! ( " {} /andy/demo" , h.base) ;
6546
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6547
let mut a = client ( & url, & work.path ( ) .join ( " a" ) ) .with_home ( work.path ( ) .join ( " home-a" ) ) ;
6548
a.create_identity ( " Andy" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
6549
write ( & work.path ( ) .join ( " a" ) , " a.txt" , " a\n " ) ;
6550
a.commit ( " a" , & [ ] ) .unwrap ( ) ;
6551
a.policy_init ( ) .unwrap ( ) ;
6552
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6553
let hub_id = h.app.hub_key.identity.clone ( ) ;
6554
a.policy_grant_subject ( " agent:claude@andy" , hub_id.clone ( ) , Scope:: CHANGES | Scope:: REFS , None ) .unwrap ( ) ;
6555
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .any ( | r | r.name = = " policy" & & r.ok ) ) ;
6556
6557
6558
let mut b = Repo:: clone( & url, & work.path ( ) .join ( " b" ) ) .unwrap ( ) .with_home ( work.path ( ) .join ( " home-b" ) ) ;
6559
b.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
6560
b.save_config ( ) .unwrap ( ) ;
6561
let ( _ , key) = b.create_identity ( " claude" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
6562
let ( _ , secret) = oxygenhub:: keys:: load_secret( & h.app.config.keys_dir ( ) ) .unwrap ( ) ;
6563
let now = db:: now_ms( ) as u64 ;
6564
let cert = SignedCertificate:: sign(
6565
Certificate {
6566
key,
6567
issuer: hub_id.hash ( ) ,
6568
subject: " agent:claude@andy" .into ( ) ,
6569
claims: vec! [ ] ,
6570
scope: Scope:: CHANGES | Scope:: REFS ,
6571
not_before: now - 60_000 ,
6572
expires: now + 600_000 ,
8aa5ef38 Andy McCall 19 days ago ↶
6573
grant: None ,
3eb491e9 Andy McCall 19 days ago ↶
6574
} ,
6575
& hub_id,
6576
& secret,
6577
) ;
6578
b.store ( ) .put_identity ( & hub_id) .unwrap ( ) ;
6579
b.store ( ) .put_certificate ( & cert) .unwrap ( ) ;
6580
write ( & work.path ( ) .join ( " b" ) , " b.txt" , " agent work\n " ) ;
6581
b.commit ( " work" , & [ ] ) .unwrap ( ) ;
6582
let r = b.push ( " origin" , false ) .unwrap ( ) ;
6583
assert! ( r.iter ( ) .all ( | x | x.ok ) , " {r:?}" ) ;
6584
6585
6586
let refs = open_remote ( & url, & RemoteOptions:: default( ) ) .unwrap ( ) .refs ( ) .unwrap ( ) ;
6587
let main = refs.refs.iter ( ) .find ( | r | r.name = = " main" ) .unwrap ( ) ;
6588
let ( attest, acc) = ( main.attestation.clone ( ) .unwrap ( ) , main.accepted.clone ( ) .expect ( " an acceptance" ) ) ;
6589
assert! ( matches! ( acc.attest.body, AttestBody:: Accepted { attest: r } if r = = attest.attest.hash ( ) ) ) ;
6590
assert_eq! ( acc.sigs[ 0 ] .key, hub_id.hash ( ) ) ;
6591
assert! ( acc.attest.hlc.wall_ms > = now) ;
6592
6593
a.pull ( " origin" ) .unwrap ( ) ;
6594
write ( & work.path ( ) .join ( " a" ) , " a.txt" , " again\n " ) ;
6595
a.commit ( " again" , & [ ] ) .unwrap ( ) ;
6596
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6597
let refs = open_remote ( & url, & RemoteOptions:: default( ) ) .unwrap ( ) .refs ( ) .unwrap ( ) ;
6598
assert! ( refs.refs.iter ( ) .find ( | r | r.name = = " main" ) .unwrap ( ) .accepted.is_none ( ) ) ;
6599
}
8aa5ef38 Andy McCall 19 days ago ↶
6600
b5c199bd Andy McCall 16 days ago ↶
6601
6602
6603
6604
6605
# [ tokio ::test ( flavor = " multi_thread" ) ]
6606
async fn a_tag_by_a_key_the_hub_certified_is_accepted_and_fetched ( ) {
6607
use oxy_core:: attest:: AttestBody;
6608
use oxy_core:: cert:: { Certificate, SignedCertificate} ;
6609
use oxy_core:: identity:: Scope;
6610
use oxy_core:: repo:: IdentityScope;
6611
use oxy_core:: tlog:: Entry;
6612
let h = hub ( ) .await;
6613
create ( & h.app, " andy" , " demo" ) .await;
6614
let url = format! ( " {} /andy/demo" , h.base) ;
6615
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6616
let mut a = client ( & url, & work.path ( ) .join ( " a" ) ) .with_home ( work.path ( ) .join ( " home-a" ) ) ;
6617
a.create_identity ( " Andy" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
6618
write ( & work.path ( ) .join ( " a" ) , " a.txt" , " a\n " ) ;
6619
a.commit ( " a" , & [ ] ) .unwrap ( ) ;
6620
a.policy_init ( ) .unwrap ( ) ;
6621
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6622
let hub_id = h.app.hub_key.identity.clone ( ) ;
6623
a.policy_grant_subject ( " agent:claude@andy" , hub_id.clone ( ) , Scope:: CHANGES | Scope:: REFS , None ) .unwrap ( ) ;
6624
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .any ( | r | r.name = = " policy" & & r.ok ) ) ;
6625
6626
6627
let mut b = Repo:: clone( & url, & work.path ( ) .join ( " b" ) ) .unwrap ( ) .with_home ( work.path ( ) .join ( " home-b" ) ) ;
6628
b.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
6629
b.save_config ( ) .unwrap ( ) ;
6630
let ( _ , key) = b.create_identity ( " claude" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
6631
let ( _ , secret) = oxygenhub:: keys:: load_secret( & h.app.config.keys_dir ( ) ) .unwrap ( ) ;
6632
let now = db:: now_ms( ) as u64 ;
6633
let cert = SignedCertificate:: sign(
6634
Certificate {
6635
key,
6636
issuer: hub_id.hash ( ) ,
6637
subject: " agent:claude@andy" .into ( ) ,
6638
claims: vec! [ ] ,
6639
scope: Scope:: CHANGES | Scope:: REFS ,
6640
not_before: now - 60_000 ,
6641
expires: now + 600_000 ,
6642
grant: None ,
6643
} ,
6644
& hub_id,
6645
& secret,
6646
) ;
6647
b.store ( ) .put_identity ( & hub_id) .unwrap ( ) ;
6648
b.store ( ) .put_certificate ( & cert) .unwrap ( ) ;
6649
let heads = b.bookmark_heads ( ) .unwrap ( ) ;
6650
b.tag_create ( " v0.2.0" , & heads, " OxygenFlow 0.2.0" ) .unwrap ( ) ;
6651
let r = b.push ( " origin" , false ) .unwrap ( ) ;
6652
assert! ( r.iter ( ) .any ( | x | x.name = = " tags/v0.2.0" & & x.ok ) , " {r:?}" ) ;
6653
6654
6655
6656
h.app.tlog.refresh_checkpoint ( & hub_id, & secret) .unwrap ( ) ;
6657
let refs = open_remote ( & url, & RemoteOptions:: default( ) ) .unwrap ( ) .refs ( ) .unwrap ( ) ;
6658
let tag = refs.tags.iter ( ) .find ( | t | t.name = = " v0.2.0" ) .expect ( " the tag" ) ;
6659
let ( attest, acc) = ( tag.attestation.clone ( ) .unwrap ( ) , tag.accepted.clone ( ) .expect ( " an acceptance" ) ) ;
6660
assert! ( matches! ( acc.attest.body, AttestBody:: Accepted { attest: r } if r = = attest.attest.hash ( ) ) ) ;
6661
assert_eq! ( acc.sigs[ 0 ] .key, hub_id.hash ( ) ) ;
6662
assert! ( acc.attest.hlc.wall_ms > = now) ;
6663
let logged = tag.logged.as_ref ( ) .expect ( " the tag's place in the log" ) ;
6664
let entry = Entry:: decode( & logged.entry) .unwrap ( ) ;
6665
assert_eq! ( ( entry.kind.as_str ( ) , entry.object) , ( " tag" , attest.attest.hash ( ) ) ) ;
6666
let logged_acceptance = db:: tlog:: entry_of( & h.app.db, & acc.attest.hash ( ) .to_hex ( ) , " accepted" ) .await.unwrap ( ) ;
6667
assert! ( logged_acceptance.is_some ( ) , " the acceptance is logged too" ) ;
6668
6669
6670
let c = Repo:: clone( & url, & work.path ( ) .join ( " c" ) ) .unwrap ( ) .with_home ( work.path ( ) .join ( " home-c" ) ) ;
6671
assert_eq! ( c.tag_heads ( " v0.2.0" ) .unwrap ( ) , Some ( heads) ) ;
6672
let report = c.fetch ( " origin" ) .unwrap ( ) ;
6673
assert! ( report.skipped.is_empty ( ) , " {:?}" , report.skipped) ;
6674
6675
a.pull ( " origin" ) .unwrap ( ) ;
6676
let t = a.tags ( ) .unwrap ( ) .into_iter ( ) .find ( | t | t.name = = " v0.2.0" ) .expect ( " pulled" ) ;
6677
assert_eq! ( t.signer.as_deref ( ) , Some ( " claude" ) ) ;
a0a1601e Andy McCall 16 days ago ↶
6678
6679
6680
6681
6682
a.tag_create ( " v0.1.0" , & a.bookmark_heads ( ) .unwrap ( ) , " by Andy" ) .unwrap ( ) ;
6683
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .any ( | r | r.name = = " tags/v0.1.0" & & r.ok ) ) ;
6684
let bare = oxy_core:: protocol:: http_get( & format! ( " {url} /oxy/v1/refs" ) , & RemoteOptions:: default( ) ) .unwrap ( ) ;
6685
let ( _ , old) = oxy_core:: protocol:: wire:: decode_refs( & bare) .unwrap ( ) ;
6686
let names: Vec< String> = old.tags.into_iter ( ) .map ( | t | t.name ) .collect ( ) ;
6687
assert_eq! ( names, [ " v0.1.0" ] , " no Oxy-Features header" ) ;
6688
let refs = open_remote ( & url, & RemoteOptions:: default( ) ) .unwrap ( ) .refs ( ) .unwrap ( ) ;
6689
let mut names: Vec< String> = refs.tags.into_iter ( ) .map ( | t | t.name ) .collect ( ) ;
6690
names.sort ( ) ;
6691
assert_eq! ( names, [ " v0.1.0" , " v0.2.0" ] , " a current client declares certified-tags" ) ;
b5c199bd Andy McCall 16 days ago ↶
6692
}
6693
8aa5ef38 Andy McCall 19 days ago ↶
6694
6695
6696
6697
6698
6699
# [ tokio ::test ( flavor = " multi_thread" ) ]
6700
async fn an_agent_with_a_grant_pushes_in_its_owner_s_name ( ) {
6701
use oxy_core:: cert:: { Grant, SignedGrant} ;
6702
use oxy_core:: identity:: { Identity, Scope, SecretKey} ;
6703
use oxy_core:: repo:: { IdentityScope, SignatureStatus} ;
6704
let h = hub ( ) .await;
6705
let andy = db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
6706
let ( pat, pat_hash) = oxygenhub:: auth:: new_token( ) ;
6707
db:: accounts:: create_token( & h.app.db, andy.id, " cli" , & pat_hash, " repo:write" , " " , None ) .await.unwrap ( ) ;
6708
create ( & h.app, " andy" , " demo" ) .await;
6709
let url = format! ( " {} /andy/demo" , h.base) ;
6710
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6711
6712
let mut a = client ( & url, & work.path ( ) .join ( " a" ) ) .with_home ( work.path ( ) .join ( " home-a" ) ) ;
6713
let ( andy_key, andy_id) = a.create_identity ( " Andy" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
6714
db:: accounts:: link_identity( & h.app.db, andy.id, & andy_key.to_hex ( ) , " Andy" , " [email protected] " , & andy_id.encode ( ) ) .await.unwrap ( ) ;
6715
write ( & work.path ( ) .join ( " a" ) , " a.txt" , " a\n " ) ;
6716
a.commit ( " a" , & [ ] ) .unwrap ( ) ;
6717
a.policy_init ( ) .unwrap ( ) ;
6718
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6719
let hub_id = h.app.hub_key.identity.clone ( ) ;
6720
a.policy_grant_subject ( " agent:claude@andy" , hub_id.clone ( ) , Scope:: CHANGES | Scope:: REFS , None ) .unwrap ( ) ;
6721
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6722
6723
6724
let ( _ , andy_sk) = a.secret_key ( ) .unwrap ( ) .unwrap ( ) ;
6725
let now = db:: now_ms( ) as u64 ;
6726
let grant = | by : & Identity, sk : & SecretKey, subject : & str | {
6727
SignedGrant:: sign(
6728
Grant {
6729
grantor: by.hash ( ) ,
6730
subject: subject.into ( ) ,
6731
issuer: hub_id.hash ( ) ,
6732
scope: Scope:: CHANGES | Scope:: SEAL | Scope:: REFS ,
6733
not_before: now - 60_000 ,
6734
expires: now + 86_400_000 ,
6735
} ,
6736
by,
6737
sk,
6738
)
6739
} ;
6740
let post = | g : SignedGrant| {
6741
let ( app, pat) = ( h.app.clone ( ) , pat.clone ( ) ) ;
6742
let body = serde_json:: json! ( { " grant" : oxy_core:: dsse:: base64( & g.encode ( ) ) } ) .to_string ( ) ;
6743
async move { call ( & app, " POST" , " /api/v1/agents/grants" , Some ( & pat) , Some ( body) ) .await }
6744
} ;
6745
let stray_sk = SecretKey:: generate( ) ;
6746
let stray = stray_sk.identity ( " stray" , " [email protected] " , 1 ) ;
6747
let ( st, body) = post ( grant ( & stray, & stray_sk, " agent:claude@andy" ) ) .await;
6748
assert_eq! ( st, StatusCode:: FORBIDDEN , " a key not linked to the account: {body}" ) ;
6749
let ( st, body) = post ( grant ( & andy_id, & andy_sk, " agent:claude@mallory" ) ) .await;
6750
assert_eq! ( st, StatusCode:: BAD_REQUEST , " another account's agent: {body}" ) ;
6751
let ( st, body) = post ( grant ( & andy_id, & andy_sk, " agent:claude@andy" ) ) .await;
6752
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
6753
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
6754
let token = v[ " token" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
6755
assert! ( token.starts_with ( " oxyagent_" ) ) ;
6756
let grant_id = v[ " id" ] .as_i64 ( ) .unwrap ( ) ;
205d8f86 Andy McCall 18 days ago ↶
6757
6758
6759
let ( st, me) = call ( & h.app, " GET" , " /api/v1/user" , Some ( & token) , None ) .await;
6760
assert! ( st = = StatusCode:: OK & & me.contains ( " \" andy\" " ) , " {st} {me}" ) ;
6761
let ( st, _ ) = call ( & h.app, " GET" , " /api/v1/agents/grants" , Some ( & token) , None ) .await;
6762
assert_eq! ( st, StatusCode:: FORBIDDEN , " an agent does not manage grants" ) ;
d4a0043f Andy McCall 18 days ago ↶
6763
6764
6765
let repo = db:: repos:: get( & h.app.db, " andy" , " demo" ) .await.unwrap ( ) .unwrap ( ) ;
6766
db:: notify:: set_watch( & h.app.db, andy.id, repo.id, true ) .await.unwrap ( ) ;
6767
let body = serde_json:: json! ( { " title" : " Filed by the agent" , " body" : " " } ) .to_string ( ) ;
6768
let ( st, out) = call ( & h.app, " POST" , " /api/v1/repos/andy/demo/issues" , Some ( & token) , Some ( body) ) .await;
6769
assert_eq! ( st, StatusCode:: CREATED , " {out}" ) ;
6770
let actor: String = sqlx:: query_scalar( " SELECT actor FROM notifications WHERE account_id = $1 AND title LIKE '%Filed by the agent%'" )
6771
.bind ( andy.id)
6772
.fetch_one ( & h.app.db)
6773
.await
6774
.expect ( " told of the agent's issue" ) ;
6775
assert! ( actor.starts_with ( " andy (agent token " ) , " {actor}" ) ;
8aa5ef38 Andy McCall 19 days ago ↶
6776
6777
6778
let agent_home = work.path ( ) .join ( " home-claude" ) ;
6779
oxy_core:: credentials:: Credentials:: at( & agent_home) .save_grant ( & h.base, & token) .unwrap ( ) ;
6780
let agent = | | {
6781
let n = std:: time:: SystemTime:: now( ) .duration_since ( std:: time:: UNIX_EPOCH ) .unwrap ( ) .as_nanos ( ) ;
6782
let dir = work.path ( ) .join ( format! ( " claude-{n} " ) ) ;
6783
let mut r = Repo:: clone( & url, & dir) .unwrap ( ) .with_home ( agent_home.clone ( ) ) ;
559c3920 Andy McCall 18 days ago ↶
6784
6785
r.config.set ( " remote.origin.token" , & token) .unwrap ( ) ;
8aa5ef38 Andy McCall 19 days ago ↶
6786
r.save_config ( ) .unwrap ( ) ;
6787
r.set_user_config ( " author.key" , & andy_key.to_hex ( ) ) .unwrap ( ) ;
6788
r.set_user_config ( " author.signer" , " keyless" ) .unwrap ( ) ;
6789
( r, dir)
6790
} ;
6791
let ( claude, dir) = agent ( ) ;
6792
assert! ( ! agent_home.join ( " keys" ) .exists ( ) , " no key on disk" ) ;
6793
write ( & dir, " work.txt" , " agent work\n " ) ;
6794
let change = claude.commit ( " Agent work" , & [ ] ) .unwrap ( ) ;
6795
let r = claude.push ( " origin" , false ) .unwrap ( ) ;
6796
assert! ( r.iter ( ) .all ( | x | x.ok ) , " {r:?}" ) ;
6797
let signer = claude.signing_key ( ) .expect ( " a certified key" ) ;
6798
assert_ne! ( signer, andy_key) ;
6799
assert! ( ! agent_home.join ( " keys" ) .exists ( ) , " still no key on disk" ) ;
6800
6801
6802
6803
let host = h.app.repos.open ( & RepoName:: parse( " andy/demo" ) .unwrap ( ) ) .unwrap ( ) ;
6804
assert_eq! ( host.store ( ) .get_change_meta ( & change) .unwrap ( ) .author, andy_key) ;
6805
assert! ( matches! (
6806
host.authenticate_change ( & change) .unwrap ( ) ,
6807
SignatureStatus:: Valid { key, sealed: true , .. } if key = = signer
6808
) ) ;
6809
let refs = open_remote ( & url, & RemoteOptions:: default( ) ) .unwrap ( ) .refs ( ) .unwrap ( ) ;
6810
assert! ( refs.refs.iter ( ) .find ( | r | r.name = = " main" ) .unwrap ( ) .accepted.is_some ( ) ) ;
6811
a.pull ( " origin" ) .unwrap ( ) ;
6812
assert! ( matches! ( a.authenticate_change ( & change) .unwrap ( ) , SignatureStatus:: Valid { .. } ) ) ;
6813
a66c05a6 Andy McCall 11 days ago ↶
6814
6815
6816
6817
create ( & h.app, " andy" , " fresh" ) .await;
6818
let fresh_url = format! ( " {} /andy/fresh" , h.base) ;
6819
let fresh_dir = work.path ( ) .join ( " claude-fresh" ) ;
6820
let mut fresh = client ( & fresh_url, & fresh_dir) .with_home ( agent_home.clone ( ) ) ;
6821
fresh.config.set ( " remote.origin.token" , & token) .unwrap ( ) ;
6822
fresh.save_config ( ) .unwrap ( ) ;
6823
fresh.set_user_config ( " author.key" , & andy_key.to_hex ( ) ) .unwrap ( ) ;
6824
fresh.set_user_config ( " author.signer" , " keyless" ) .unwrap ( ) ;
6825
assert! ( fresh.store ( ) .get_identity ( & andy_key) .is_err ( ) , " the fresh repository has never seen Andy" ) ;
6826
write ( & fresh_dir, " first.txt" , " first\n " ) ;
6827
let first = fresh.commit ( " First, by the agent" , & [ ] ) .unwrap ( ) ;
6828
let r = fresh.push ( " origin" , false ) .unwrap ( ) ;
6829
assert! ( r.iter ( ) .all ( | x | x.ok ) , " {r:?}" ) ;
6830
assert! ( matches! ( fresh.authenticate_change ( & first) .unwrap ( ) , SignatureStatus:: Valid { .. } ) , " locally" ) ;
6831
let fresh_host = h.app.repos.open ( & RepoName:: parse( " andy/fresh" ) .unwrap ( ) ) .unwrap ( ) ;
6832
assert! (
6833
matches! ( fresh_host.authenticate_change ( & first) .unwrap ( ) , SignatureStatus:: Valid { sealed: true , .. } ) ,
6834
" on the hub: {:?}" ,
6835
fresh_host.authenticate_change ( & first) .unwrap ( )
6836
) ;
6837
27babfa1 Andy McCall 10 days ago ↶
6838
6839
6840
6841
let rot = a.rotate_identity ( ) .unwrap ( ) ;
6842
db:: accounts:: link_identity( & h.app.db, andy.id, & rot.new.to_hex ( ) , " Andy" , " [email protected] " , & rot.identity.encode ( ) ) .await.unwrap ( ) ;
6843
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6844
let ( mut warm, dir) = agent ( ) ;
6845
warm.set_user_config ( " author.for" , " andy" ) .unwrap ( ) ;
6846
write ( & dir, " warm.txt" , " before the hub has said\n " ) ;
6847
warm.commit ( " Warm" , & [ ] ) .unwrap ( ) ;
6848
assert! ( warm.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6849
let followed = std:: fs:: read_to_string( agent_home.join ( " keys" ) .join ( " author.for" ) ) .unwrap ( ) ;
6850
assert_eq! ( followed.trim ( ) , rot.new.to_hex ( ) , " the hub named Andy's newest identity" ) ;
6851
let ( follower, dir) = agent ( ) ;
6852
assert_eq! ( follower.config.author_key, Some ( rot.new) ) ;
6853
write ( & dir, " follow.txt" , " as Andy's newest identity\n " ) ;
6854
let followed = follower.commit ( " Followed" , & [ ] ) .unwrap ( ) ;
6855
assert! ( follower.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6856
assert_eq! ( host.store ( ) .get_change_meta ( & followed) .unwrap ( ) .author, rot.new) ;
6857
assert! (
6858
matches! ( host.authenticate_change ( & followed) .unwrap ( ) , SignatureStatus:: Valid { sealed: true , .. } ) ,
6859
" {:?}" ,
6860
host.authenticate_change ( & followed) .unwrap ( )
6861
) ;
6862
8aa5ef38 Andy McCall 19 days ago ↶
6863
6864
let ( st, _ ) = call ( & h.app, " DELETE" , & format! ( " /api/v1/agents/grants/{grant_id} " ) , Some ( & pat) , None ) .await;
6865
assert_eq! ( st, StatusCode:: NO_CONTENT ) ;
6866
let ( late, dir) = agent ( ) ;
6867
write ( & dir, " late.txt" , " after the revocation\n " ) ;
6868
late.commit ( " Late" , & [ ] ) .unwrap ( ) ;
6869
let err = late.push ( " origin" , false ) .unwrap_err ( ) .to_string ( ) ;
559c3920 Andy McCall 18 days ago ↶
6870
assert! ( err.contains ( " not valid" ) | | err.contains ( " would not certify" ) , " {err}" ) ;
6871
6872
let by_agent: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM pushes WHERE via = 'claude'" ) .fetch_one ( & h.app.db) .await.unwrap ( ) ;
6873
assert! ( by_agent > = 1 , " recorded as via the agent" ) ;
8aa5ef38 Andy McCall 19 days ago ↶
6874
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/agents/grants" , Some ( & pat) , None ) .await;
6875
let list: Vec< serde_json:: Value> = serde_json:: from_str( & body) .unwrap ( ) ;
6876
assert! ( list[ 0 ] [ " revoked_ms" ] .is_i64 ( ) & & list[ 0 ] .get ( " token" ) .is_none ( ) , " {body}" ) ;
6877
}
a9f1c0bf Andy McCall 17 days ago ↶
6878
6879
6880
6881
6882
6883
# [ tokio ::test ( flavor = " multi_thread" ) ]
6884
async fn newest_run_wins ( ) {
6885
use oxy_core:: hash:: ChangeHash;
6886
let h = hub ( ) .await;
6887
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
6888
create ( & h.app, " andy" , " newest" ) .await;
6889
let url = format! ( " {} /andy/newest" , h.base) ;
6890
let work = tempfile:: tempdir( ) .unwrap ( ) ;
6891
let c = client ( & url, work.path ( ) ) ;
6892
write ( work.path ( ) , " .flow/pipeline.kdl" , & pipeline ( " " ) ) ;
6893
write ( work.path ( ) , " test.sh" , " echo ok\n " ) ;
6894
c.commit ( " Start" , & [ ] ) .unwrap ( ) ;
6895
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
6896
let pushed = runs_of ( & h.app, " andy" , " newest" ) .await[ 0 ] .clone ( ) ;
6897
assert_eq! ( ( pushed.number, pushed.trigger.as_str ( ) ) , ( 1 , " push" ) ) ;
6898
let heads: Vec< ChangeHash> = pushed.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
6899
let start = | row : db:: repos:: RepoRow, bookmark : & 'static str , trigger : & 'static str | {
6900
let ( app, state, heads) = ( h.app.clone ( ) , pushed.state.clone ( ) , heads.clone ( ) ) ;
6901
async move {
6902
oxygenhub:: flow:: start_run_scoped( & app, & row, bookmark, & state, & heads, " andy" , trigger, None ) .await.unwrap ( ) .expect ( " a run" )
6903
}
6904
} ;
6905
let row = db:: repos:: get( & h.app.db, " andy" , " newest" ) .await.unwrap ( ) .unwrap ( ) ;
6906
let run = | n : i64 | {
6907
let ( app, id) = ( h.app.clone ( ) , row.id) ;
6908
async move { db:: flow:: run( & app.db, id, n) .await.unwrap ( ) .unwrap ( ) }
6909
} ;
6910
let superseded_by = | n : i64 , by : i64 | {
6911
let app = h.app.clone ( ) ;
6912
let run = run ( n) ;
6913
async move {
6914
let r = run.await;
6915
assert_eq! ( r.status, " cancelled" , " run #{n} is superseded" ) ;
6916
let jobs = db:: flow:: jobs( & app.db, r.id) .await.unwrap ( ) ;
6917
let why = format! ( " superseded by run #{by} " ) ;
6918
assert! ( jobs.iter ( ) .all ( | j | j.status = = " cancelled" & & j.error = = why ) , " {jobs:?}" ) ;
6919
}
6920
} ;
6921
6922
6923
6924
assert_eq! ( start ( row.clone ( ) , " proposal/7" , " proposal" ) .await, 2 ) ;
6925
assert_eq! ( start ( row.clone ( ) , " proposal/7" , " proposal" ) .await, 3 ) ;
6926
superseded_by ( 2 , 3 ) .await;
6927
assert_eq! ( start ( row.clone ( ) , " proposal/8" , " proposal" ) .await, 4 ) ;
6928
assert! ( run ( 3 ) .await.live ( ) , " another proposal's run is not superseded" ) ;
6929
assert! ( run ( 1 ) .await.live ( ) , " a proposal's run leaves push runs alone" ) ;
6930
6931
6932
6933
assert_eq! ( start ( row.clone ( ) , " main" , " manual" ) .await, 5 ) ;
6934
assert_eq! ( start ( row.clone ( ) , " main" , " rerun:1:push" ) .await, 6 ) ;
6935
assert! ( run ( 1 ) .await.live ( ) , " a manual run or a rerun supersedes nothing" ) ;
6936
assert_eq! ( start ( row.clone ( ) , " main" , " push" ) .await, 7 ) ;
6937
superseded_by ( 1 , 7 ) .await;
6938
assert! ( run ( 5 ) .await.live ( ) & & run ( 6 ) .await.live ( ) , " a push leaves manual runs and reruns alone" ) ;
6939
6940
6941
6942
db:: repos:: set_flow_keep_running( & h.app.db, row.id, true ) .await.unwrap ( ) ;
6943
let kept = db:: repos:: get( & h.app.db, " andy" , " newest" ) .await.unwrap ( ) .unwrap ( ) ;
6944
assert! ( kept.flow_keep_running) ;
6945
let three = run ( 3 ) .await;
6946
sqlx:: query( " UPDATE flow_runs SET status = 'running' WHERE id = $1" ) .bind ( three.id) .execute ( & h.app.db) .await.unwrap ( ) ;
6947
sqlx:: query( " UPDATE flow_jobs SET status = 'running' WHERE run_id = $1 AND name = 'build'" )
6948
.bind ( three.id)
6949
.execute ( & h.app.db)
6950
.await
6951
.unwrap ( ) ;
6952
assert_eq! ( start ( kept.clone ( ) , " proposal/7" , " proposal" ) .await, 8 ) ;
6953
assert_eq! ( start ( kept.clone ( ) , " proposal/7" , " proposal" ) .await, 9 ) ;
6954
assert_eq! ( run ( 3 ) .await.status, " running" , " a running run is kept" ) ;
6955
superseded_by ( 8 , 9 ) .await;
6956
6957
db:: repos:: set_flow_keep_running( & h.app.db, row.id, false ) .await.unwrap ( ) ;
6958
let row = db:: repos:: get( & h.app.db, " andy" , " newest" ) .await.unwrap ( ) .unwrap ( ) ;
6959
assert_eq! ( start ( row.clone ( ) , " proposal/7" , " proposal" ) .await, 10 ) ;
6960
superseded_by ( 3 , 10 ) .await;
6961
superseded_by ( 9 , 10 ) .await;
6962
}
8501fd58 Andy McCall 10 days ago ↶
6963
6964
6965
6966
6967
# [ tokio ::test ( flavor = " multi_thread" ) ]
6968
async fn runners_are_offered_updates_by_their_nearest_setting ( ) {
6969
use oxy_core:: identity:: SecretKey;
6970
let h = hub ( ) .await;
6971
create ( & h.app, " andy" , " oxygenflow" ) .await;
6972
let res = oxygenhub:: router( h.app.clone ( ) )
6973
.oneshot (
6974
Request:: post( " /register" )
6975
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
6976
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery&setup=test-setup" ) )
6977
.unwrap ( ) ,
6978
)
6979
.await
6980
.unwrap ( ) ;
6981
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
6982
let admin = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
6983
db:: accounts:: set_admin( & h.app.db, admin.id, true ) .await.unwrap ( ) ;
6984
let web = | method : & 'static str , path : & str , body : Option< String> | {
6985
let ( app, cookie, path) = ( h.app.clone ( ) , cookie.clone ( ) , path.to_string ( ) ) ;
6986
async move {
6987
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
6988
if body.is_some ( ) {
6989
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
6990
}
6991
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
6992
let st = res.status ( ) ;
6993
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
6994
}
6995
} ;
6996
let ( st, page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
6997
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
6998
let csrf = {
6999
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
7000
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
7001
} ;
7002
assert_eq! ( call ( & h.app, " GET" , " /admin/runners/updates" , None , None ) .await.0 , StatusCode:: NOT_FOUND , " administrators only" ) ;
7003
let mut creds = vec! [ ] ;
7004
for name in [ " plex" , " narvi" ] {
7005
let ( _ , page) = web ( " POST" , " /admin/runners/token" , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
7006
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
7007
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
7008
let identity = SecretKey:: generate( ) .identity ( & format! ( " {name} (OxygenFlow runner)" ) , " " , 1 ) ;
7009
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0 (build)" , " max_jobs" : 1 } ) ;
7010
let reg = serde_json:: json! ( { " token" : token, " name" : name, " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
7011
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
7012
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7013
creds.push ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
7014
}
7015
let ask = | who : usize | {
7016
let ( app, cred) = ( h.app.clone ( ) , creds[ who] .clone ( ) ) ;
7017
async move {
7018
let ( st, body) = call ( & app, " GET" , " /api/v1/flow/runner/update" , Some ( & cred) , None ) .await;
7019
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7020
serde_json:: from_str:: < flow_core:: api:: UpdateAnswer> ( & body) .unwrap ( )
7021
}
7022
} ;
7023
let a = ask ( 0 ) .await;
7024
assert! ( ! a.policy.updates & & a.offer.is_none ( ) , " off unless chosen: {a:?}" ) ;
7025
assert_eq! ( a.note, " updates are off (default)" ) ;
7026
assert_eq! ( call ( & h.app, " GET" , " /api/v1/flow/runner/update" , Some ( " flowrunner_nope" ) , None ) .await.0 , StatusCode:: UNAUTHORIZED ) ;
7027
7028
7029
let repo = db:: repos:: get( & h.app.db, " andy" , " oxygenflow" ) .await.unwrap ( ) .unwrap ( ) ;
7030
let rel = db:: releases:: create( & h.app.db, repo.id, " v0.2.0" , " 0.2.0" , " " , false , None ) .await.unwrap ( ) ;
c26267b3 Andy McCall 7 days ago ↶
7031
let build: Vec< u8 > = ( 0 .. 300_000 u32 ) .map ( | i | ( i.wrapping_mul ( 2654435761 ) > > 13 ) as u8 ) .collect ( ) ;
7032
let build_hash = blake3:: hash( & build) .to_hex ( ) .to_string ( ) ;
7033
let at = h.app.config.data.join ( " assets" ) .join ( & build_hash[ .. 2 ] ) ;
7034
std:: fs:: create_dir_all( & at) .unwrap ( ) ;
7035
std:: fs:: write( at.join ( & build_hash) , & build) .unwrap ( ) ;
7036
db:: releases:: add_asset( & h.app.db, rel.id, " flow-linux-x86_64" , build.len ( ) as i64 , & build_hash, " application/octet-stream" )
7037
.await
7038
.unwrap ( ) ;
8501fd58 Andy McCall 10 days ago ↶
7039
let ( _ , page) = web ( " POST" , " /admin/runners/updates/source" , Some ( format! ( " _csrf={csrf} &repo=andy%2Fnope" ) ) ) .await;
7040
assert! ( page.contains ( " There is no repository" ) , " {page}" ) ;
7041
let ( _ , page) = web ( " POST" , " /admin/runners/updates/source" , Some ( format! ( " _csrf={csrf} &repo=andy%2Foxygenflow" ) ) ) .await;
7042
assert! ( page.contains ( " runner builds come from andy/oxygenflow" ) , " {page}" ) ;
7043
7044
7045
let plex = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
7046
let ( _ , page) =
7047
web ( " POST" , " /admin/runners/updates/groups" , Some ( format! ( " _csrf={csrf} &name=canary&mode=chosen&runner={} " , plex.id) ) ) .await;
7048
assert! ( page.contains ( " Test group canary made: 1 chosen runners" ) , " {page}" ) ;
7049
let group = db:: runner_updates:: groups( & h.app.db) .await.unwrap ( ) .remove ( 0 ) ;
7050
let ( _ , page) =
7051
web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=group&key={} &updates=on" , group.id) ) ) .await;
7052
assert! ( page.contains ( " Saved for group canary: updates on." ) , " {page}" ) ;
7053
let a = ask ( 0 ) .await;
7054
let offer = a.offer.clone ( ) .expect ( " plex is offered the build" ) ;
7055
assert_eq! ( ( offer.version.as_str ( ) , offer.asset.as_str ( ) , offer.blake3.len ( ) ) , ( " 0.2.0" , " flow-linux-x86_64" , 64 ) ) ;
7056
assert! ( offer.url.starts_with ( " /andy/oxygenflow/asset/" ) & & ! offer.older, " {offer:?}" ) ;
7057
assert_eq! ( a.policy.from[ 0 ] , " group canary" ) ;
7058
assert! ( ask ( 1 ) .await.offer.is_none ( ) , " narvi is not in the group" ) ;
c26267b3 Andy McCall 7 days ago ↶
7059
7060
7061
let mut shared = None ;
7062
for _ in 0 .. 100 {
7063
shared = h.app.images ( ) .manifest ( & build_hash) ;
7064
if shared.is_some ( ) {
7065
break ;
7066
}
7067
tokio:: time:: sleep( std:: time:: Duration:: from_millis( 50 ) ) .await;
7068
}
7069
let m = shared.expect ( " the build is shared with the swarm" ) ;
7070
assert_eq! ( ( m.size, m.arch.as_deref ( ) ) , ( build.len ( ) as u64 , Some ( " x86_64" ) ) ) ;
7071
let mut whole = vec! [ ] ;
7072
for c in & m.chunks {
7073
whole.extend ( h.app.images ( ) .chunk ( c.hash ( ) ) .unwrap ( ) ) ;
7074
}
7075
assert_eq! ( whole, build) ;
7076
let kept = oxygenhub:: images:: kept( & h.app.db) .await.unwrap ( ) ;
7077
assert! ( kept.iter ( ) .any ( | k | k.hash = = build_hash & & k.uploader = = " runner build flow 0.2.0" ) ) ;
8501fd58 Andy McCall 10 days ago ↶
7078
7079
7080
let ( _ , page) = web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=site&updates=off&dry_run=on" ) ) ) .await;
7081
assert! ( page.contains ( " Saved for the site" ) , " {page}" ) ;
7082
let a = ask ( 0 ) .await;
7083
assert! ( a.policy.dry_run & & a.policy.from[ 3 ] = = " site" & & a.note.contains ( " dry run" ) , " {a:?}" ) ;
7084
let narvi = db:: runners:: list( & h.app.db) .await.unwrap ( ) .into_iter ( ) .find ( | r | r.name = = " narvi" ) .unwrap ( ) ;
7085
web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=runner&key={} &updates=on&channel=0.1.0" , narvi.id) ) )
7086
.await;
7087
let b = ask ( 1 ) .await;
7088
assert! ( b.policy.updates & & b.offer.is_none ( ) & & b.note.contains ( " has no release" ) , " {b:?}" ) ;
7089
7090
7091
let now = time:: OffsetDateTime:: now_utc( ) .hour ( ) ;
7092
let ( from, to) = ( ( now + 2 ) % 24 , ( now + 4 ) % 24 ) ;
7093
web (
7094
" POST" ,
7095
" /admin/runners/updates/policy" ,
7096
Some ( format! ( " _csrf={csrf} &scope=group&key={} &updates=on&window={from:02} -{to:02} " , group.id) ) ,
7097
)
7098
.await;
7099
let a = ask ( 0 ) .await;
7100
assert! ( a.offer.is_none ( ) & & a.note.contains ( " waits for its window" ) , " {a:?}" ) ;
7101
7102
7103
let ( _ , page) = web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=site&channel=latest" ) ) ) .await;
7104
assert! ( page.contains ( " A channel is stable, canary or a version" ) , " {page}" ) ;
7105
let ( _ , page) = web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=site&window=9" ) ) ) .await;
7106
assert! ( page.contains ( " a window is two hours" ) , " {page}" ) ;
7107
let ( _ , page) = web ( " POST" , " /admin/runners/updates/groups" , Some ( format! ( " _csrf={csrf} &name=farm&mode=percent&percent=0" ) ) ) .await;
7108
assert! ( page.contains ( " A percentage is a whole number from 1 to 100." ) , " {page}" ) ;
7109
7110
7111
let report = serde_json:: json! ( { " note" : " dry run: flow 0.2.0 checks out" } ) .to_string ( ) ;
7112
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runner/update" , Some ( & creds[ 0 ] ) , Some ( report) ) .await.0 , StatusCode:: NO_CONTENT ) ;
7113
let ( _ , page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
7114
assert! ( page.contains ( " dry run: flow 0.2.0 checks out" ) & & page.contains ( " >canary</span>" ) , " {page}" ) ;
7115
assert! ( page.contains ( " updates on (group canary)" ) & & page.contains ( " updates on (runner) · channel 0.1.0 (runner)" ) , " {page}" ) ;
7116
9bbca80f Andy McCall 10 days ago ↶
7117
7118
let ( _ , tokens) = web ( " GET" , " /settings/tokens" , None ) .await;
7119
let tcsrf = {
7120
let i = tokens.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
7121
tokens[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
7122
} ;
7123
let ( _ , made) = web ( " POST" , " /settings/tokens" , Some ( format! ( " _csrf={tcsrf} &name=admin&scopes=user%3Aread" ) ) ) .await;
7124
let i = made.find ( " oxyhub_" ) .unwrap ( ) ;
7125
let admin_token: String = made[ i.. ] .chars ( ) .take_while ( | c | c.is_ascii_alphanumeric ( ) | | * c = = ' _' ) .collect ( ) ;
7126
let ( st, body) = call ( & h.app, " GET" , " /api/v1/admin/runner-updates" , Some ( & admin_token) , None ) .await;
7127
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7128
let all: hub_api:: RunnerUpdates = serde_json:: from_str( & body) .unwrap ( ) ;
7129
assert_eq! ( all.repo, " andy/oxygenflow" ) ;
7130
assert! ( all.settings.iter ( ) .any ( | s | s.scope = = " group" & & s.updates = = Some ( true ) & & s.window.is_some ( ) ) , " {all:?}" ) ;
7131
let plex_row = all.runners.iter ( ) .find ( | r | r.name = = " plex" ) .unwrap ( ) ;
7132
assert! ( plex_row.updates & & plex_row.from[ 0 ] = = " group canary" & & plex_row.groups = = [ " canary" ] , " {plex_row:?}" ) ;
7133
let put = | body : serde_json:: Value| {
7134
let ( app, t) = ( h.app.clone ( ) , admin_token.clone ( ) ) ;
7135
async move { call ( & app, " PUT" , " /api/v1/admin/runner-updates/settings" , Some ( & t) , Some ( body.to_string ( ) ) ) .await }
7136
} ;
7137
let ( st, body) =
7138
put ( serde_json:: json! ( { " scope" : " runner" , " key" : narvi.id.to_string ( ) , " updates" : true , " channel" : " 0.1.0" , " dry_run" : true } ) )
7139
.await;
7140
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7141
assert! ( body.contains ( " Saved for runner narvi: updates on, channel 0.1.0, dry run on." ) , " {body}" ) ;
7142
let ( st, body) = put ( serde_json:: json! ( { " scope" : " site" , " channel" : " latest" } ) ) .await;
7143
assert_eq! ( st, StatusCode:: UNPROCESSABLE_ENTITY , " {body}" ) ;
7144
assert_eq! (
7145
call ( & h.app, " GET" , " /api/v1/admin/runner-updates" , Some ( & creds[ 0 ] ) , None ) .await.0 ,
7146
StatusCode:: UNAUTHORIZED ,
7147
" not a runner's"
7148
) ;
7149
faf5246c Andy McCall 10 days ago ↶
7150
7151
let report = serde_json:: json! ( { " note" : " rolled back from flow 0.2.0 to 0.1.0: 3 starts without reaching the hub" } ) .to_string ( ) ;
7152
assert_eq! ( call ( & h.app, " POST" , " /api/v1/flow/runner/update" , Some ( & creds[ 0 ] ) , Some ( report) ) .await.0 , StatusCode:: NO_CONTENT ) ;
7153
let a = ask ( 0 ) .await;
7154
assert! ( a.offer.is_none ( ) & & a.note.contains ( " flow 0.2.0 is held: it rolled back on plex (3 starts" ) , " {a:?}" ) ;
7155
let ( _ , page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
7156
assert! ( page.contains ( " Held builds" ) & & page.contains ( " <strong>flow 0.2.0</strong>" ) , " {page}" ) ;
9bbca80f Andy McCall 10 days ago ↶
7157
let ( _ , body) = call ( & h.app, " GET" , " /api/v1/admin/runner-updates" , Some ( & admin_token) , None ) .await;
7158
assert! ( body.contains ( " \" version\" :\" 0.2.0\" " ) & & body.contains ( " \" runner\" :\" plex\" " ) , " the hold through the API: {body}" ) ;
faf5246c Andy McCall 10 days ago ↶
7159
let ( _ , page) = web ( " POST" , " /admin/runners/updates/holds/lift" , Some ( format! ( " _csrf={csrf} &version=0.2.0" ) ) ) .await;
7160
assert! ( page.contains ( " flow 0.2.0 is offered again" ) , " {page}" ) ;
9bbca80f Andy McCall 10 days ago ↶
7161
let ( st, _ ) = call ( & h.app, " POST" , " /api/v1/admin/runner-updates/holds/0.2.0/lift" , Some ( & admin_token) , Some ( " {}" .into ( ) ) ) .await;
7162
assert_eq! ( st, StatusCode:: NOT_FOUND , " no longer held" ) ;
faf5246c Andy McCall 10 days ago ↶
7163
assert! ( ask ( 0 ) .await.note.contains ( " waits for its window" ) , " lifted: back to what its settings say" ) ;
7164
let ( _ , page) = web ( " POST" , " /admin/runners/updates/holds/lift" , Some ( format! ( " _csrf={csrf} &version=0.2.0" ) ) ) .await;
7165
assert! ( page.contains ( " flow 0.2.0 is not held." ) , " {page}" ) ;
7166
8501fd58 Andy McCall 10 days ago ↶
7167
7168
let ( _ , page) =
7169
web ( " POST" , & format! ( " /admin/runners/updates/groups/{} " , group.id) , Some ( format! ( " _csrf={csrf} &mode=percent&percent=100" ) ) ) .await;
7170
assert! ( page.contains ( " Test group canary is now 100% of its runners." ) , " {page}" ) ;
7171
assert! ( ask ( 1 ) .await.policy.from[ 0 ] = = " runner" , " narvi's own setting is nearer than any group" ) ;
7172
let ( _ , page) = web ( " POST" , & format! ( " /admin/runners/updates/groups/{} /delete" , group.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
7173
assert! ( page.contains ( " Test group canary removed" ) , " {page}" ) ;
7174
assert! ( ! ask ( 0 ) .await.policy.updates, " back to the site's off" ) ;
2f79b17e Andy McCall 7 days ago ↶
7175
7176
7177
7178
let ( _ , page) = web ( " POST" , " /admin/runners/token" , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
7179
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
7180
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
7181
let identity = SecretKey:: generate( ) .identity ( " phone (OxygenFlow runner)" , " " , 1 ) ;
7182
let info = serde_json:: json! ( { " arch" : " aarch64" , " os" : " android" , " version" : " 0.1.0 (app)" , " max_jobs" : 1 } ) ;
7183
let reg = serde_json:: json! ( { " token" : token, " name" : " phone" , " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
7184
let ( _ , body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
7185
let phone = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ;
7186
web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=site&updates=on" ) ) ) .await;
7187
let ( st, body) = call ( & h.app, " GET" , " /api/v1/flow/runner/update" , Some ( & phone) , None ) .await;
7188
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7189
let a: flow_core:: api:: UpdateAnswer = serde_json:: from_str( & body) .unwrap ( ) ;
7190
assert! ( a.offer.is_none ( ) & & a.note.contains ( " follows the OxygenHub app's releases" ) , " {a:?}" ) ;
8501fd58 Andy McCall 10 days ago ↶
7191
}
d7050703 Andy McCall 10 days ago ↶
7192
1ec29691 Andy McCall 8 days ago ↶
7193
7194
7195
7196
# [ tokio ::test ( flavor = " multi_thread" ) ]
7197
async fn a_version_reaches_wave_two_once_wave_one_proves_it ( ) {
7198
use oxy_core:: identity:: SecretKey;
7199
let h = hub ( ) .await;
7200
create ( & h.app, " andy" , " oxygenflow" ) .await;
7201
let res = oxygenhub:: router( h.app.clone ( ) )
7202
.oneshot (
7203
Request:: post( " /register" )
7204
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
7205
.body ( Body:: from( " username=carol&email=carol%40example.com&password=correct-horse-battery&setup=test-setup" ) )
7206
.unwrap ( ) ,
7207
)
7208
.await
7209
.unwrap ( ) ;
7210
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
7211
let admin = db:: accounts:: account_by_name( & h.app.db, " carol" ) .await.unwrap ( ) .unwrap ( ) ;
7212
db:: accounts:: set_admin( & h.app.db, admin.id, true ) .await.unwrap ( ) ;
7213
let web = | method : & 'static str , path : & str , body : Option< String> | {
7214
let ( app, cookie, path) = ( h.app.clone ( ) , cookie.clone ( ) , path.to_string ( ) ) ;
7215
async move {
7216
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
7217
if body.is_some ( ) {
7218
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
7219
}
7220
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
7221
( res.status ( ) , String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
7222
}
7223
} ;
7224
let ( _ , page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
7225
let csrf = {
7226
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
7227
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
7228
} ;
7229
let mut creds = vec! [ ] ;
7230
for name in [ " plex" , " narvi" ] {
7231
let ( _ , page) = web ( " POST" , " /admin/runners/token" , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
7232
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
7233
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
7234
let identity = SecretKey:: generate( ) .identity ( & format! ( " {name} (OxygenFlow runner)" ) , " " , 1 ) ;
7235
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0 (build)" , " max_jobs" : 1 } ) ;
7236
let reg = serde_json:: json! ( { " token" : token, " name" : name, " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
7237
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
7238
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7239
creds.push ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
7240
}
7241
let ask = | who : usize | {
7242
let ( app, cred) = ( h.app.clone ( ) , creds[ who] .clone ( ) ) ;
7243
async move {
7244
let ( st, body) = call ( & app, " GET" , " /api/v1/flow/runner/update" , Some ( & cred) , None ) .await;
7245
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7246
serde_json:: from_str:: < flow_core:: api:: UpdateAnswer> ( & body) .unwrap ( )
7247
}
7248
} ;
7249
let repo = db:: repos:: get( & h.app.db, " andy" , " oxygenflow" ) .await.unwrap ( ) .unwrap ( ) ;
7250
let rel = db:: releases:: create( & h.app.db, repo.id, " v0.2.0" , " 0.2.0" , " " , false , None ) .await.unwrap ( ) ;
7251
db:: releases:: add_asset( & h.app.db, rel.id, " flow-linux-x86_64" , 42 , & " ab" .repeat ( 32 ) , " application/octet-stream" ) .await.unwrap ( ) ;
7252
let ( _ , page) =
7253
web ( " POST" , " /admin/runners/updates/source" , Some ( format! ( " _csrf={csrf} &repo=andy%2Foxygenflow&soak_jobs=2&soak_hours=48" ) ) ) .await;
7254
assert! ( page.contains ( " runner builds come from andy/oxygenflow" ) & & page.contains ( " value=\" 2\" " ) , " {page}" ) ;
7255
let ( _ , page) = web ( " POST" , " /admin/runners/updates/source" , Some ( format! ( " _csrf={csrf} &repo=andy%2Foxygenflow&soak_jobs=0" ) ) ) .await;
7256
assert! ( page.contains ( " is a whole number from 1 to 1000" ) , " {page}" ) ;
7257
web ( " POST" , " /admin/runners/updates/policy" , Some ( format! ( " _csrf={csrf} &scope=site&updates=on" ) ) ) .await;
7258
7259
7260
let runners = db:: runners:: list( & h.app.db) .await.unwrap ( ) ;
7261
let id = | name : & str | runners.iter ( ) .find ( | r | r.name = = name ) .unwrap ( ) .id ;
7262
let ( _ , page) =
7263
web ( " POST" , " /admin/runners/updates/groups" , Some ( format! ( " _csrf={csrf} &name=first&mode=chosen&runner={} &wave=1" , id ( " plex" ) ) ) )
7264
.await;
7265
assert! ( page.contains ( " Test group first made: 1 chosen runners, wave 1" ) & & page.contains ( " · wave 1</small>" ) , " {page}" ) ;
7266
let ( _ , page) =
7267
web ( " POST" , " /admin/runners/updates/groups" , Some ( format! ( " _csrf={csrf} &name=rest&mode=chosen&runner={} &wave=0" , id ( " narvi" ) ) ) )
7268
.await;
7269
assert! ( page.contains ( " A wave is a whole number from 1 to 9" ) , " {page}" ) ;
7270
web ( " POST" , " /admin/runners/updates/groups" , Some ( format! ( " _csrf={csrf} &name=rest&mode=chosen&runner={} &wave=2" , id ( " narvi" ) ) ) ) .await;
7271
assert_eq! ( ask ( 0 ) .await.offer.map ( | o | o.version ) .as_deref ( ) , Some ( " 0.2.0" ) , " wave 1 goes first" ) ;
7272
let b = ask ( 1 ) .await;
7273
assert! ( b.offer.is_none ( ) & & b.note.contains ( " flow 0.2.0 waits for wave 1 to prove it: 0 of 1 runner on it, 0 jobs" ) , " {b:?}" ) ;
7274
7275
7276
let run: i64 = sqlx:: query_scalar(
7277
" INSERT INTO flow_runs (repo_id, bookmark, state, heads, status, created_ms, number) VALUES ($1, 'main', 's', 'h', 'passed', 1, 1) RETURNING id" ,
7278
)
7279
.bind ( repo.id)
7280
.fetch_one ( & h.app.db)
7281
.await
7282
.unwrap ( ) ;
7283
let job = | name : & 'static str , status : & 'static str | {
7284
let ( app, plex) = ( h.app.clone ( ) , id ( " plex" ) ) ;
7285
async move {
7286
sqlx:: query(
7287
" INSERT INTO flow_jobs (run_id, name, depth, depends, spec, sandbox, runtime, status, runner, runner_id, started_ms, finished_ms, error, runner_version)
7288
VALUES ($1, $2, 0, '', '{}', 'microvm', 'microvm', $3, 'plex', $4, $5, $5, $6, '0.2.0')" ,
7289
)
7290
.bind ( run)
7291
.bind ( name)
7292
.bind ( status)
7293
.bind ( plex)
7294
.bind ( db:: now_ms( ) )
7295
.bind ( if status = = " error" { " the runner lost its disk" } else { " " } )
7296
.execute ( & app.db)
7297
.await
7298
.unwrap ( ) ;
7299
}
7300
} ;
7301
job ( " build" , " passed" ) .await;
7302
assert! ( ask ( 1 ) .await.note.contains ( " 1 of 1 runner on it, 1 job (each needs 2" ) , " still soaking" ) ;
7303
job ( " test" , " failed" ) .await;
7304
assert_eq! ( ask ( 1 ) .await.offer.map ( | o | o.version ) .as_deref ( ) , Some ( " 0.2.0" ) , " a failed test is not the runner's fault: proved" ) ;
7305
let ( _ , page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
7306
assert! ( ! page.contains ( " waits for wave" ) , " {page}" ) ;
7307
7308
7309
job ( " lint" , " error" ) .await;
7310
let ( _ , page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
7311
assert! ( page.contains ( " wave 1 stopped it on plex: job lint of andy/oxygenflow run #1: the runner lost its disk" ) , " {page}" ) ;
7312
assert! ( db:: runner_updates:: held( & h.app.db, " 0.2.0" ) .await.unwrap ( ) .is_none ( ) , " a page view holds nothing" ) ;
7313
let b = ask ( 1 ) .await;
7314
assert! ( b.offer.is_none ( ) & & b.note.contains ( " flow 0.2.0 is held: wave 1 stopped it on plex" ) , " {b:?}" ) ;
7315
assert! ( ask ( 0 ) .await.note.contains ( " is held" ) , " wave 1 too" ) ;
7316
let ( _ , page) = web ( " GET" , " /admin/runners/updates" , None ) .await;
7317
assert! ( page.contains ( " Held builds" ) & & page.contains ( " wave 1 stopped it on plex" ) , " {page}" ) ;
7318
}
7319
d7050703 Andy McCall 10 days ago ↶
7320
7321
# [ tokio ::test ( flavor = " multi_thread" ) ]
7322
async fn a_wasm_jobs_placement_names_no_image ( ) {
7323
let h = hub ( ) .await;
7324
create ( & h.app, " andy" , " wasm" ) .await;
7325
let work = tempfile:: tempdir( ) .unwrap ( ) ;
7326
let c = client ( & format! ( " {} /andy/wasm" , h.base) , work.path ( ) ) ;
7327
write (
7328
work.path ( ) ,
7329
" .flow/pipeline.kdl" ,
7330
" schema-version 1\n sandbox \" w\" {\n runtime \" wasm\" \n }\n job \" count\" {\n sandbox \" w\" \n exec \" count.wasm\" \n }\n " ,
7331
) ;
7332
write ( work.path ( ) , " count.wasm" , " \0 asm" ) ;
7333
c.commit ( " A WASM job" , & [ ] ) .unwrap ( ) ;
7334
assert! ( c.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
7335
runs_of ( & h.app, " andy" , " wasm" ) .await;
7336
let ( st, body) =
7337
call ( & h.app, " POST" , " /api/v1/flow/claim" , Some ( " runner-secret" ) , Some ( r # "{"runner":"test-1","runtimes":["wasm"]}"# .into ( ) ) ) .await;
7338
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7339
let id = serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " job_id" ] .as_i64 ( ) .unwrap ( ) ;
7340
let placed = db:: flow:: job( & h.app.db, id) .await.unwrap ( ) .unwrap ( ) .placed;
7341
assert! ( placed.starts_with ( " idle" ) & & ! placed.contains ( " image" ) , " {placed}" ) ;
c26267b3 Andy McCall 7 days ago ↶
7342
7343
7344
7345
let ( st, page) = call ( & h.app, " GET" , " /andy/wasm/flow/tab" , None , None ) .await;
7346
assert_eq! ( st, StatusCode:: OK , " {page}" ) ;
7347
assert! ( page.contains ( " Run all 1" ) & & page.contains ( " "exec":["count.wasm"]" ) , " {page}" ) ;
7348
assert! ( page.contains ( " data-archive=\" /andy/wasm/archive?ref=main&format=zip\" " ) , " {page}" ) ;
7349
let ( st, zip) = get_bytes ( & h.app, " /andy/wasm/archive?ref=main&format=zip" ) .await;
7350
assert! ( st = = StatusCode:: OK & & zip.starts_with ( b " PK" ) , " {st}" ) ;
7351
assert_eq! ( get_bytes ( & h.app, " /static/wasi-worker.js" ) .await.0 , StatusCode:: OK ) ;
7352
let ( _ , runs) = call ( & h.app, " GET" , " /andy/wasm/flow" , None , None ) .await;
7353
assert! ( runs.contains ( " /andy/wasm/flow/tab" ) , " linked from Flow" ) ;
7354
let ( _ , none) = call ( & h.app, " GET" , " /andy/wasm/flow/tab?bookmark=nope" , None , None ) .await;
7355
assert! ( none.contains ( " Run all 1" ) , " an unknown bookmark shows the default's" ) ;
d7050703 Andy McCall 10 days ago ↶
7356
}
dc5c6429 Andy McCall 10 days ago ↶
7357
7358
7359
7360
# [ tokio ::test ( flavor = " multi_thread" ) ]
7361
async fn an_organisation_sets_its_own_runners_updates ( ) {
7362
use oxy_core:: identity:: SecretKey;
7363
let h = hub ( ) .await;
7364
let res = oxygenhub:: router( h.app.clone ( ) )
7365
.oneshot (
7366
Request:: post( " /register" )
7367
.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" )
7368
.body ( Body:: from( " username=andy&email=andy%40example.com&password=correct-horse-battery&setup=test-setup" ) )
7369
.unwrap ( ) ,
7370
)
7371
.await
7372
.unwrap ( ) ;
7373
let cookie = res.headers ( ) .get ( header:: SET_COOKIE ) .unwrap ( ) .to_str ( ) .unwrap ( ) .split ( ' ;' ) .next ( ) .unwrap ( ) .to_string ( ) ;
7374
let web = | method : & 'static str , path : String, body : Option< String> | {
7375
let ( app, cookie) = ( h.app.clone ( ) , cookie.clone ( ) ) ;
7376
async move {
7377
let mut req = Request:: builder( ) .method ( method) .uri ( path) .header ( header:: COOKIE , cookie) ;
7378
if body.is_some ( ) {
7379
req = req.header ( header:: CONTENT_TYPE , " application/x-www-form-urlencoded" ) ;
7380
}
7381
let res = oxygenhub:: router( app) .oneshot ( req.body ( body.map ( Body:: from) .unwrap_or_else ( Body:: empty) ) .unwrap ( ) ) .await.unwrap ( ) ;
7382
let st = res.status ( ) ;
7383
( st, String :: from_utf8_lossy( & res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ) .into_owned ( ) )
7384
}
7385
} ;
7386
let csrf_of = | page : & str | {
7387
let i = page.find ( " name=\" _csrf\" value=\" " ) .unwrap ( ) + " name=\" _csrf\" value=\" " .len ( ) ;
7388
page[ i.. ] .split ( ' "' ) .next ( ) .unwrap ( ) .to_string ( )
7389
} ;
7390
let ( _ , page) = web ( " GET" , " /orgs/new" .into ( ) , None ) .await;
7391
let csrf = csrf_of ( & page) ;
7392
web ( " POST" , " /orgs/new" .into ( ) , Some ( format! ( " _csrf={csrf} &name=acme&display_name=Acme" ) ) ) .await;
7393
7394
let mut creds = vec! [ ] ;
7395
for name in [ " a1" , " a2" ] {
7396
let ( _ , page) = web ( " POST" , " /orgs/acme/settings/runners/token" .into ( ) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
7397
let i = page.find ( " --token " ) .unwrap ( ) + " --token " .len ( ) ;
7398
let token: String = page[ i.. ] .chars ( ) .take_while ( | c | ! c.is_whitespace ( ) ) .collect ( ) ;
7399
let identity = SecretKey:: generate( ) .identity ( name, " " , 1 ) ;
7400
let info = serde_json:: json! ( { " arch" : " x86_64" , " os" : " linux" , " version" : " 0.1.0" , " max_jobs" : 1 } ) ;
7401
let reg = serde_json:: json! ( { " token" : token, " name" : name, " identity" : hex:: encode( identity.encode ( ) ) , " info" : info } ) ;
7402
let ( st, body) = call ( & h.app, " POST" , " /api/v1/flow/runners/register" , None , Some ( reg.to_string ( ) ) ) .await;
7403
assert_eq! ( st, StatusCode:: OK , " {body}" ) ;
7404
creds.push ( serde_json:: from_str:: < serde_json:: Value> ( & body) .unwrap ( ) [ " credential" ] .as_str ( ) .unwrap ( ) .to_string ( ) ) ;
7405
}
7406
claim ( & h.app) .await;
7407
let all = db:: runners:: list( & h.app.db) .await.unwrap ( ) ;
7408
let id = | n : & str | all.iter ( ) .find ( | r | r.name = = n ) .unwrap ( ) .id ;
7409
let set = | form : String| web ( " POST" , " /orgs/acme/settings/runners/updates/policy" .into ( ) , Some ( format! ( " _csrf={csrf} &{form} " ) ) ) ;
7410
7411
let ( _ , page) = set ( " scope=owner&key=acme&updates=on" .into ( ) ) .await;
7412
assert! ( page.contains ( " Saved for owner acme: updates on." ) , " {page}" ) ;
7413
let ( _ , page) = set ( " scope=site&updates=on" .into ( ) ) .await;
7414
assert! ( page.contains ( " Choose the site, an owner, a test group or a runner." ) , " not the site's: {page}" ) ;
7415
let ( _ , page) = set ( " scope=owner&key=andy&updates=on" .into ( ) ) .await;
7416
assert! ( page.contains ( " An organisation sets its own runners" ) , " {page}" ) ;
7417
let ( _ , page) = set ( format! ( " scope=runner&key={} &updates=on" , id ( " test-1" ) ) ) .await;
7418
assert! ( page.contains ( " That runner is gone." ) , " the site's runner is not acme's: {page}" ) ;
7419
7420
7421
let ( _ , page) = web (
7422
" POST" ,
7423
" /orgs/acme/settings/runners/updates/groups" .into ( ) ,
7424
Some ( format! ( " _csrf={csrf} &name=canary&owner=&mode=chosen&runner={} &runner={} " , id ( " a1" ) , id ( " test-1" ) ) ) ,
7425
)
7426
.await;
7427
assert! ( page.contains ( " Test group canary made: 1 chosen runners" ) , " {page}" ) ;
7428
let canary = db:: runner_updates:: groups( & h.app.db) .await.unwrap ( ) .remove ( 0 ) ;
7429
assert_eq! ( canary.owner, " acme" , " an organisation's groups are its own" ) ;
7430
let ( _ , page) = set ( format! ( " scope=group&key={} &channel=canary" , canary.id) ) .await;
7431
assert! ( page.contains ( " Saved for group canary: channel canary." ) , " {page}" ) ;
7432
let ( _ , page) = set ( format! ( " scope=runner&key={} &updates=off" , id ( " a2" ) ) ) .await;
7433
assert! ( page.contains ( " Saved for runner a2: updates off." ) , " {page}" ) ;
7434
7435
7436
let site = db:: runner_updates:: create_group( & h.app.db, " " , " everyone" , Some ( 100 ) ) .await.unwrap ( ) ;
7437
let ( _ , page) =
7438
web ( " POST" , format! ( " /orgs/acme/settings/runners/updates/groups/{} /delete" , site.id) , Some ( format! ( " _csrf={csrf} " ) ) ) .await;
7439
assert! ( page.contains ( " That test group is gone." ) , " {page}" ) ;
7440
assert! ( db:: runner_updates:: group( & h.app.db, site.id) .await.unwrap ( ) .is_some ( ) ) ;
7441
7442
7443
let ( _ , page) = web ( " GET" , " /orgs/acme/settings" .into ( ) , None ) .await;
7444
assert! ( page.contains ( " id=\" runner-updates\" " ) & & page.contains ( " >canary <small" ) , " {page}" ) ;
7445
assert! ( page.contains ( " updates on (owner acme)" ) & & page.contains ( " updates off (runner)" ) , " {page}" ) ;
7446
let ask = | cred : String| {
7447
let app = h.app.clone ( ) ;
7448
async move {
7449
let ( _ , body) = call ( & app, " GET" , " /api/v1/flow/runner/update" , Some ( & cred) , None ) .await;
7450
serde_json:: from_str:: < flow_core:: api:: UpdateAnswer> ( & body) .unwrap ( ) .policy
7451
}
7452
} ;
7453
let a1 = ask ( creds[ 0 ] .clone ( ) ) .await;
7454
assert! ( a1.updates & & a1.channel = = " canary" & & a1.from[ .. 2 ] = = [ " owner acme" .to_string ( ) , " group canary" .to_string ( ) ] , " {a1:?}" ) ;
7455
assert! ( ! ask ( creds[ 1 ] .clone ( ) ) .await.updates, " a2's own setting wins" ) ;
7456
}
42c77f65 Andy McCall 4 days ago ↶
7457
7458
7459
7460
7461
7462
# [ tokio ::test ( flavor = " multi_thread" ) ]
7463
async fn a_change_a_keyless_agent_sealed_shows_the_agent ( ) {
7464
use oxy_core:: cert:: { Certificate, Grant, SignedCertificate, SignedGrant} ;
7465
use oxy_core:: identity:: Scope;
7466
use oxy_core:: repo:: IdentityScope;
7467
let h = hub ( ) .await;
7468
create ( & h.app, " andy" , " demo" ) .await;
7469
let url = format! ( " {} /andy/demo" , h.base) ;
7470
let work = tempfile:: tempdir( ) .unwrap ( ) ;
7471
let mut a = client ( & url, & work.path ( ) .join ( " a" ) ) .with_home ( work.path ( ) .join ( " home-a" ) ) ;
7472
let ( _ , andy) = a.create_identity ( " Andy" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
7473
write ( & work.path ( ) .join ( " a" ) , " a.txt" , " a\n " ) ;
7474
a.commit ( " base" , & [ ] ) .unwrap ( ) ;
7475
assert! ( a.push ( " origin" , false ) .unwrap ( ) .iter ( ) .all ( | r | r.ok ) ) ;
7476
7477
7478
7479
7480
let hub_id = h.app.hub_key.identity.clone ( ) ;
7481
let ( _ , hub_secret) = oxygenhub:: keys:: load_secret( & h.app.config.keys_dir ( ) ) .unwrap ( ) ;
7482
let ( _ , andy_secret) = a.secret_key ( ) .unwrap ( ) .unwrap ( ) ;
7483
let now = db:: now_ms( ) as u64 ;
7484
let scope = Scope:: CHANGES | Scope:: REFS | Scope:: SEAL ;
7485
let grant = SignedGrant:: sign(
7486
Grant {
7487
grantor: andy.hash ( ) ,
7488
subject: " agent:kimi@andy" .into ( ) ,
7489
issuer: hub_id.hash ( ) ,
7490
scope,
7491
not_before: now - 60_000 ,
7492
expires: now + 86_400_000 ,
7493
} ,
7494
& andy,
7495
& andy_secret,
7496
) ;
7497
let mut b = Repo:: clone( & url, & work.path ( ) .join ( " b" ) ) .unwrap ( ) .with_home ( work.path ( ) .join ( " home-b" ) ) ;
7498
b.config.set ( " remote.origin.token" , " site" ) .unwrap ( ) ;
7499
b.save_config ( ) .unwrap ( ) ;
7500
let ( _ , agent) = b.create_identity ( " kimi" , " [email protected] " , IdentityScope:: User) .unwrap ( ) ;
7501
let cert = SignedCertificate:: sign(
7502
Certificate {
7503
key: agent.clone ( ) ,
7504
issuer: hub_id.hash ( ) ,
7505
subject: " agent:kimi@andy" .into ( ) ,
7506
claims: vec! [ ] ,
7507
scope,
7508
not_before: now - 60_000 ,
7509
expires: now + 600_000 ,
7510
grant: Some ( grant) ,
7511
} ,
7512
& hub_id,
7513
& hub_secret,
7514
) ;
7515
b.store ( ) .put_identity ( & hub_id) .unwrap ( ) ;
7516
b.store ( ) .put_certificate ( & cert) .unwrap ( ) ;
7517
7518
b.config.set ( " author.key" , & andy.hash ( ) .to_hex ( ) ) .unwrap ( ) ;
7519
b.config.set ( " author.signer" , & agent.hash ( ) .to_hex ( ) ) .unwrap ( ) ;
7520
b.save_config ( ) .unwrap ( ) ;
7521
write ( & work.path ( ) .join ( " b" ) , " b.txt" , " agent work\n " ) ;
7522
let change = b.commit ( " agent work" , & [ ] ) .unwrap ( ) ;
7523
assert! ( b.store ( ) .signatures ( & change) .unwrap ( ) .is_empty ( ) , " the change itself is unsigned" ) ;
7524
b.seal ( & [ change] ) .unwrap ( ) ;
7525
let r = b.push ( " origin" , false ) .unwrap ( ) ;
7526
assert! ( r.iter ( ) .all ( | x | x.ok ) , " {r:?}" ) ;
7527
7528
let ( st, body) = call ( & h.app, " GET" , & format! ( " /api/v1/repos/andy/demo/changes/{} " , change.to_hex ( ) ) , None , None ) .await;
7529
assert! ( st.is_success ( ) , " {st} {body}" ) ;
7530
let v: serde_json:: Value = serde_json:: from_str( & body) .unwrap ( ) ;
7531
let sig = & v[ " change" ] [ " signature" ] ;
7532
assert_eq! ( sig[ " status" ] , " valid" , " a sealed change is not unsigned: {sig}" ) ;
7533
assert_eq! ( sig[ " sealed" ] , true , " {sig}" ) ;
7534
assert_eq! ( sig[ " key" ] , agent.hash ( ) .to_hex ( ) ) ;
7535
7536
let ( _ , page) = call ( & h.app, " GET" , & format! ( " /andy/demo/change/{} " , change.to_hex ( ) ) , None , None ) .await;
7537
assert! ( page.contains ( " agent:kimi@andy" ) , " the change page names the agent" ) ;
7538
assert! ( page.contains ( " keyless, under your grant" ) , " a certified key is not flagged as a lost delegation" ) ;
7539
let ( _ , overview) = call ( & h.app, " GET" , " /andy/demo" , None , None ) .await;
7540
let contributors = & overview[ overview.find ( " Contributors" ) .expect ( " a Contributors panel" ) .. ] ;
7541
assert! ( contributors.contains ( " agent:kimi@andy" ) , " Contributors names the agent under its owner" ) ;
7542
}
1c52d940 Andy McCall 4 days ago ↶
7543
7544
7545
7546
7547
7548
# [ tokio ::test ( flavor = " multi_thread" ) ]
7549
async fn attachments_are_uploaded_and_served_safely ( ) {
7550
let h = hub ( ) .await;
7551
db:: accounts:: create_account( & h.app.db, " andy" , " [email protected] " , " x" , " salt" , true ) .await.unwrap ( ) ;
7552
create ( & h.app, " andy" , " demo" ) .await;
7553
let send = | path : String, bytes : Vec< u8 > , token : Option< & 'static str > | {
7554
let app = h.app.clone ( ) ;
7555
async move {
7556
let mut req = Request:: builder( ) .method ( " POST" ) .uri ( path) .header ( header:: CONTENT_TYPE , " application/octet-stream" ) ;
7557
if let Some ( t) = token {
7558
req = req.header ( header:: AUTHORIZATION , format! ( " Bearer {t} " ) ) ;
7559
}
7560
let res = oxygenhub:: router( app) .oneshot ( req.body ( Body:: from( bytes) ) .unwrap ( ) ) .await.unwrap ( ) ;
7561
let status = res.status ( ) ;
7562
let body = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) ;
7563
( status, String :: from_utf8_lossy( & body) .into_owned ( ) )
7564
}
7565
} ;
7566
let get = | path : String| {
7567
let app = h.app.clone ( ) ;
7568
async move {
7569
let res = oxygenhub:: router( app) .oneshot ( Request:: get( path) .body ( Body:: empty( ) ) .unwrap ( ) ) .await.unwrap ( ) ;
7570
let hd = | k : header:: HeaderName| res.headers ( ) .get ( k) .and_then ( | v | v.to_str ( ) .ok ( ) ) .unwrap_or ( " " ) .to_string ( ) ;
7571
let heads = (
7572
hd ( header:: CONTENT_TYPE ) ,
7573
hd ( header:: CONTENT_DISPOSITION ) ,
7574
hd ( header:: X_CONTENT_TYPE_OPTIONS ) ,
7575
hd ( header:: CONTENT_SECURITY_POLICY ) ,
7576
hd ( header:: CACHE_CONTROL ) ,
7577
) ;
7578
let status = res.status ( ) ;
7579
let body = res.into_body ( ) .collect ( ) .await.unwrap ( ) .to_bytes ( ) .to_vec ( ) ;
7580
( status, heads, body)
7581
}
7582
} ;
7583
let png = b " \x89 PNG\r \n \x1a \n not really a picture" .to_vec ( ) ;
7584
7585
let ( st, _ ) = send ( " /api/v1/repos/andy/demo/attachments?name=shot.png" .into ( ) , png.clone ( ) , None ) .await;
7586
assert_eq! ( st, StatusCode:: UNAUTHORIZED , " attaching needs a token" ) ;
7587
let ( st, body) = send ( " /api/v1/repos/andy/demo/attachments?name=dir/shot.png" .into ( ) , png.clone ( ) , Some ( " site" ) ) .await;
7588
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
7589
let a: hub_api:: Attachment = serde_json:: from_str( & body) .unwrap ( ) ;
7590
assert_eq! ( ( a.name.as_str ( ) , a.content_type.as_str ( ) , a.inline) , ( " shot.png" , " image/png" , true ) ) ;
7591
assert_eq! ( a.markdown, format! ( " " , a.url) ) ;
7592
let path = a.url.strip_prefix ( & h.base) .unwrap ( ) .to_string ( ) ;
7593
assert_eq! ( path, format! ( " /andy/demo/attachments/{} /shot.png" , a.id) ) ;
7594
let ( st, ( ct, cd, nosniff, csp, cache) , bytes) = get ( path.clone ( ) ) .await;
7595
assert_eq! ( st, StatusCode:: OK ) ;
7596
assert_eq! ( ( ct.as_str ( ) , nosniff.as_str ( ) ) , ( " image/png" , " nosniff" ) ) ;
7597
assert! ( cd.starts_with ( " inline" ) , " {cd}" ) ;
7598
assert! ( csp.contains ( " default-src 'none'" ) & & csp.contains ( " sandbox" ) , " {csp}" ) ;
7599
assert! ( cache.starts_with ( " private" ) , " {cache}" ) ;
7600
assert_eq! ( bytes, png) ;
7601
let ( st, _ , _ ) = get ( format! ( " /andy/demo/attachments/{} /other.png" , a.id) ) .await;
7602
assert_eq! ( st, StatusCode:: NOT_FOUND , " the name must match" ) ;
7603
7604
7605
let svg = b " <svg xmlns=\" http://www.w3.org/2000/svg\" ><script>alert(1)</script></svg>" .to_vec ( ) ;
7606
let ( st, body) = send ( " /api/v1/repos/andy/demo/attachments?name=logo.svg" .into ( ) , svg, Some ( " site" ) ) .await;
7607
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
7608
let s: hub_api:: Attachment = serde_json:: from_str( & body) .unwrap ( ) ;
7609
assert! ( ! s.inline & & s.content_type = = " application/octet-stream" & & s.markdown = = format! ( " [logo.svg]({} )" , s.url) ) ;
7610
let ( _ , ( ct, cd, _ , _ , _ ) , _ ) = get ( s.url.strip_prefix ( & h.base) .unwrap ( ) .to_string ( ) ) .await;
7611
assert_eq! ( ct, " application/octet-stream" ) ;
7612
assert! ( cd.starts_with ( " attachment" ) , " {cd}" ) ;
7613
let ( st, _ ) = send ( " /api/v1/repos/andy/demo/attachments?name=.env" .into ( ) , b " x" .to_vec ( ) , Some ( " site" ) ) .await;
7614
assert_eq! ( st, StatusCode:: UNPROCESSABLE_ENTITY , " a hidden file name is refused" ) ;
7615
7616
7617
h.app.repos.create ( & RepoName:: parse( " andy/secret" ) .unwrap ( ) , None ) .unwrap ( ) ;
7618
db:: repos:: create( & h.app.db, " andy" , " secret" , " " , hub_api:: Visibility:: Private) .await.unwrap ( ) ;
7619
let ( st, body) = send ( " /api/v1/repos/andy/secret/attachments?name=s.png" .into ( ) , png.clone ( ) , Some ( " site" ) ) .await;
7620
assert_eq! ( st, StatusCode:: CREATED , " {body}" ) ;
7621
let p: hub_api:: Attachment = serde_json:: from_str( & body) .unwrap ( ) ;
7622
let ( st, _ , _ ) = get ( p.url.strip_prefix ( & h.base) .unwrap ( ) .to_string ( ) ) .await;
7623
assert_eq! ( st, StatusCode:: NOT_FOUND , " not to someone signed out" ) ;
7624
}