@@ -251,6 +251,73 @@ async fn test_sync_context_retry_on_error() {
251251 assert_eq ! ( server. frame_count( ) , 1 ) ;
252252}
253253
254+ #[ tokio:: test]
255+ async fn test_bootstrap_db_downloads_export ( ) {
256+ let server = MockServer :: start ( ) ;
257+ let temp_dir = tempdir ( ) . unwrap ( ) ;
258+ let db_path = temp_dir. path ( ) . join ( "bootstrap.db" ) ;
259+
260+ // Seed metadata so SyncContext can be constructed (generation=1)
261+ gen_metadata_file ( & db_path, 3278479626 , 0 , 0 , 1 ) ;
262+
263+ let mut sync_ctx = SyncContext :: new (
264+ server. connector ( ) ,
265+ db_path. to_str ( ) . unwrap ( ) . to_string ( ) ,
266+ server. url ( ) ,
267+ None ,
268+ None ,
269+ )
270+ . await
271+ . unwrap ( ) ;
272+
273+
274+ let _ = std:: fs:: remove_file ( & db_path) ;
275+ let _ = std:: fs:: remove_file ( format ! ( "{}-info" , db_path. to_str( ) . unwrap( ) ) ) ;
276+
277+ // Bootstrap should fetch /info and then /export/{generation}
278+ crate :: sync:: bootstrap_db ( & mut sync_ctx) . await . unwrap ( ) ;
279+
280+ assert ! ( std:: path:: Path :: new( & db_path) . exists( ) ) ;
281+ assert ! ( std:: path:: Path :: new( & format!( "{}-info" , db_path. to_str( ) . unwrap( ) ) ) . exists( ) ) ;
282+
283+ assert_eq ! ( sync_ctx. durable_generation( ) , 1 ) ;
284+ assert_eq ! ( sync_ctx. durable_frame_num( ) , 0 ) ;
285+
286+ assert ! ( server. request_count( ) >= 2 ) ;
287+ }
288+
289+ #[ tokio:: test]
290+ async fn test_bootstrap_db_is_idempotent ( ) {
291+ let server = MockServer :: start ( ) ;
292+ let temp_dir = tempdir ( ) . unwrap ( ) ;
293+ let db_path = temp_dir. path ( ) . join ( "bootstrap2.db" ) ;
294+
295+
296+ gen_metadata_file ( & db_path, 3278479626 , 0 , 0 , 1 ) ;
297+
298+ let mut sync_ctx = SyncContext :: new (
299+ server. connector ( ) ,
300+ db_path. to_str ( ) . unwrap ( ) . to_string ( ) ,
301+ server. url ( ) ,
302+ None ,
303+ None ,
304+ )
305+ . await
306+ . unwrap ( ) ;
307+
308+ let _ = std:: fs:: remove_file ( & db_path) ;
309+ let _ = std:: fs:: remove_file ( format ! ( "{}-info" , db_path. to_str( ) . unwrap( ) ) ) ;
310+
311+
312+ crate :: sync:: bootstrap_db ( & mut sync_ctx) . await . unwrap ( ) ;
313+ let first_requests = server. request_count ( ) ;
314+
315+ // Second bootstrap should be a no-op (no new network calls)
316+ crate :: sync:: bootstrap_db ( & mut sync_ctx) . await . unwrap ( ) ;
317+ let second_requests = server. request_count ( ) ;
318+ assert_eq ! ( first_requests, second_requests) ;
319+ }
320+
254321#[ test]
255322fn test_hash_verification ( ) {
256323 let mut metadata = MetadataJson {
@@ -328,12 +395,14 @@ impl Service<http::Uri> for MockConnector {
328395 }
329396}
330397
398+ #[ allow( dead_code) ]
331399struct MockServer {
332400 url : String ,
333401 frame_count : Arc < AtomicU32 > ,
334402 connector : ConnectorService ,
335403 return_error : Arc < AtomicBool > ,
336404 request_count : Arc < AtomicU32 > ,
405+ export_bytes : Arc < Vec < u8 > > , // bytes returned by /export/{generation}
337406}
338407
339408impl MockServer {
@@ -342,6 +411,25 @@ impl MockServer {
342411 let return_error = Arc :: new ( AtomicBool :: new ( false ) ) ;
343412 let request_count = Arc :: new ( AtomicU32 :: new ( 0 ) ) ;
344413
414+ let export_bytes: Arc < Vec < u8 > > = {
415+ use crate :: local:: Database ;
416+ use crate :: database:: OpenFlags ;
417+ use std:: fs;
418+ use tempfile:: NamedTempFile ;
419+
420+ let tmp = NamedTempFile :: new ( ) . expect ( "temp file for export db" ) ;
421+ let path = tmp. path ( ) . to_path_buf ( ) ;
422+ let db = Database :: open ( path. to_str ( ) . unwrap ( ) . to_string ( ) , OpenFlags :: default ( ) )
423+ . expect ( "open export db" ) ;
424+ let conn = db. connect ( ) . expect ( "connect export db" ) ;
425+
426+ let _ = conn. query ( "CREATE TABLE IF NOT EXISTS t(x INTEGER);" , crate :: params:: Params :: None ) ;
427+ drop ( conn) ;
428+ drop ( db) ;
429+ let bytes = fs:: read ( & path) . expect ( "read export db bytes" ) ;
430+ Arc :: new ( bytes)
431+ } ;
432+
345433 // Create the mock connector with Some(client_stream)
346434 let ( tx, mut rx) = tokio:: sync:: mpsc:: channel ( 1 ) ;
347435 let mock_connector = MockConnector { tx } ;
@@ -353,18 +441,21 @@ impl MockServer {
353441 connector,
354442 return_error : return_error. clone ( ) ,
355443 request_count : request_count. clone ( ) ,
444+ export_bytes : export_bytes. clone ( ) ,
356445 } ;
357446
358447 // Spawn the server handler
359448 let frame_count_clone = frame_count. clone ( ) ;
360449 let return_error_clone = return_error. clone ( ) ;
361450 let request_count_clone = request_count. clone ( ) ;
451+ let export_bytes_clone = export_bytes. clone ( ) ;
362452
363453 tokio:: spawn ( async move {
364454 while let Some ( server_stream) = rx. recv ( ) . await {
365455 let frame_count_clone = frame_count_clone. clone ( ) ;
366456 let return_error_clone = return_error_clone. clone ( ) ;
367457 let request_count_clone = request_count_clone. clone ( ) ;
458+ let export_bytes_clone = export_bytes_clone. clone ( ) ;
368459
369460 tokio:: spawn ( async move {
370461 use hyper:: server:: conn:: Http ;
@@ -377,6 +468,7 @@ impl MockServer {
377468 let frame_count = frame_count_clone. clone ( ) ;
378469 let return_error = return_error_clone. clone ( ) ;
379470 let request_count = request_count_clone. clone ( ) ;
471+ let export_bytes = export_bytes_clone. clone ( ) ;
380472 async move {
381473 request_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
382474 if return_error. load ( Ordering :: SeqCst ) {
@@ -388,9 +480,9 @@ impl MockServer {
388480 ) ;
389481 }
390482
391- let current_count = frame_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
392-
393483 if req. uri ( ) . path ( ) . contains ( "/sync/" ) {
484+ // Count only sync requests as frames to keep tests stable.
485+ let current_count = frame_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
394486 // Return the max_frame_no that has been accepted
395487 let response = serde_json:: json!( {
396488 "status" : "ok" ,
@@ -404,6 +496,23 @@ impl MockServer {
404496 . body ( Body :: from ( response. to_string ( ) ) )
405497 . unwrap ( ) ,
406498 )
499+ } else if req. uri ( ) . path ( ) . eq ( "/info" ) {
500+ let response = serde_json:: json!( {
501+ "current_generation" : 1
502+ } ) ;
503+ Ok :: < _ , hyper:: Error > (
504+ http:: Response :: builder ( )
505+ . status ( 200 )
506+ . body ( Body :: from ( response. to_string ( ) ) )
507+ . unwrap ( ) ,
508+ )
509+ } else if req. uri ( ) . path ( ) . starts_with ( "/export/" ) {
510+ Ok :: < _ , hyper:: Error > (
511+ http:: Response :: builder ( )
512+ . status ( 200 )
513+ . body ( Body :: from ( export_bytes. as_ref ( ) . clone ( ) ) )
514+ . unwrap ( ) ,
515+ )
407516 } else {
408517 Ok ( http:: Response :: builder ( )
409518 . status ( 404 )
@@ -489,4 +598,4 @@ fn gen_metadata_file(db_path: &Path, hash: u32, version: u32, durable_frame_num:
489598 . as_bytes ( ) ,
490599 )
491600 . unwrap ( ) ;
492- }
601+ }
0 commit comments