@@ -17,13 +17,12 @@ use crate::{WebData, get_db_conn};
1717const MEBIBYTE : usize = 1024 * 1024 ;
1818const MAX_ENCRYPTED_SYNC_BYTES : usize = 64 * MEBIBYTE ;
1919const MAX_ENCRYPTED_SYNC_ACCOUNT_BYTES : usize = 128 * MEBIBYTE ;
20- // ` playbackSpeeds` is deprecated for new clients, but remains part of legacy
21- // document migration until older OpenTubeX versions have been phased out .
22- const LEGACY_ENCRYPTED_COLLECTIONS : [ & str ; 6 ] = [
20+ // Deprecated playbackSpeeds is read into settings by current clients and must
21+ // not be required to finish legacy document migration .
22+ const LEGACY_ENCRYPTED_COLLECTIONS : [ & str ; 5 ] = [
2323 "subscriptions" ,
2424 "playlists" ,
2525 "history" ,
26- "playbackSpeeds" ,
2726 "profiles" ,
2827 "playlistBookmarks" ,
2928] ;
@@ -215,10 +214,126 @@ mod tests {
215214 fn encrypted_collection_limits_are_scoped_by_data_type ( ) {
216215 assert_eq ! ( collection_limit( "settings" ) . unwrap( ) , 2 * MEBIBYTE ) ;
217216 assert_eq ! ( collection_limit( "profiles" ) . unwrap( ) , 8 * MEBIBYTE ) ;
217+ assert_eq ! ( collection_limit( "playbackSpeeds" ) . unwrap( ) , 8 * MEBIBYTE ) ;
218218 assert_eq ! ( collection_limit( "sessions" ) . unwrap( ) , 8 * MEBIBYTE ) ;
219219 assert_eq ! ( collection_limit( "sessionsV2" ) . unwrap( ) , 8 * MEBIBYTE ) ;
220220 assert_eq ! ( collection_limit( "subscriptions" ) . unwrap( ) , 16 * MEBIBYTE ) ;
221221 assert_eq ! ( collection_limit( "history" ) . unwrap( ) , 64 * MEBIBYTE ) ;
222222 assert ! ( collection_limit( "unknown" ) . is_err( ) ) ;
223223 }
224224}
225+
226+ #[ cfg( all( test, feature = "sqlite" ) ) ]
227+ mod migration_tests {
228+ use actix_web:: { App , HttpMessage , test, web} ;
229+ use diesel:: connection:: SimpleConnection ;
230+ use diesel_async:: RunQueryDsl ;
231+ use diesel_async:: pooled_connection:: { AsyncDieselConnectionManager , bb8:: Pool } ;
232+ use diesel_migrations:: MigrationHarness ;
233+
234+ use crate :: { DbConnection , MIGRATIONS , models:: Account } ;
235+
236+ #[ actix_rt:: test]
237+ async fn deleting_migrated_playback_speeds_does_not_restart_legacy_migration ( ) {
238+ let pool = Pool :: builder ( )
239+ . max_size ( 1 )
240+ . build ( AsyncDieselConnectionManager :: < DbConnection > :: new (
241+ ":memory:" ,
242+ ) )
243+ . await
244+ . unwrap ( ) ;
245+ let account = Account {
246+ id : "owner" . into ( ) ,
247+ name_hash : "owner-hash" . into ( ) ,
248+ password_hash : None ,
249+ oidc_sub : None ,
250+ legacy_tokens_enabled : false ,
251+ session_generation : 0 ,
252+ } ;
253+ {
254+ let mut conn = pool. get ( ) . await . unwrap ( ) ;
255+ conn. spawn_blocking ( |conn| {
256+ conn. run_pending_migrations ( MIGRATIONS ) . unwrap ( ) ;
257+ conn. batch_execute (
258+ "INSERT INTO account (id, name_hash) VALUES ('owner', 'owner-hash');
259+ INSERT INTO encrypted_sync_single_document (account_id, revision, payload)
260+ VALUES ('owner', 1, 'legacy-ciphertext');" ,
261+ ) ?;
262+ Ok ( ( ) )
263+ } )
264+ . await
265+ . unwrap ( ) ;
266+ }
267+ let app = test:: init_service (
268+ App :: new ( ) . app_data ( web:: Data :: new ( pool. clone ( ) ) ) . service (
269+ web:: scope ( "/sync" )
270+ . service ( super :: get_encrypted_sync_manifest)
271+ . service ( super :: get_legacy_encrypted_sync)
272+ . service ( super :: get_encrypted_sync_collection)
273+ . service ( super :: put_encrypted_sync_collection) ,
274+ ) ,
275+ )
276+ . await ;
277+ // Incomplete migrations must still expose the original document.
278+ let request = test:: TestRequest :: get ( ) . uri ( "/sync" ) . to_request ( ) ;
279+ request. extensions_mut ( ) . insert ( account. clone ( ) ) ;
280+ let manifest: serde_json:: Value = test:: call_and_read_body_json ( & app, request) . await ;
281+ assert_eq ! ( manifest[ "legacy_encrypted_data" ] , true ) ;
282+
283+ // Use the actual PUT endpoint, including deprecated collection support.
284+ for collection in [
285+ "subscriptions" ,
286+ "playlists" ,
287+ "history" ,
288+ "profiles" ,
289+ "playlistBookmarks" ,
290+ "settings" ,
291+ "playbackSpeeds" ,
292+ ] {
293+ let request = test:: TestRequest :: put ( )
294+ . uri ( & format ! ( "/sync/{collection}" ) )
295+ . set_json ( serde_json:: json!( { "revision" : 0 , "payload" : "ciphertext" } ) )
296+ . to_request ( ) ;
297+ request. extensions_mut ( ) . insert ( account. clone ( ) ) ;
298+ assert ! (
299+ test:: call_service( & app, request)
300+ . await
301+ . status( )
302+ . is_success( )
303+ ) ;
304+ }
305+ for deleted in [ false , true ] {
306+ if deleted {
307+ let mut conn = pool. get ( ) . await . unwrap ( ) ;
308+ diesel:: sql_query ( "DELETE FROM encrypted_sync WHERE account_id = 'owner' AND collection = 'playbackSpeeds'" )
309+ . execute ( & mut conn) . await . unwrap ( ) ;
310+ }
311+ let request = test:: TestRequest :: get ( ) . uri ( "/sync" ) . to_request ( ) ;
312+ request. extensions_mut ( ) . insert ( account. clone ( ) ) ;
313+ let manifest: serde_json:: Value = test:: call_and_read_body_json ( & app, request) . await ;
314+ assert_eq ! ( manifest[ "legacy_data" ] , false ) ;
315+ assert_eq ! ( manifest[ "legacy_encrypted_data" ] , false ) ;
316+ assert_eq ! (
317+ manifest[ "collections" ]
318+ . as_array( )
319+ . unwrap( )
320+ . iter( )
321+ . any( |entry| entry[ "collection" ] == "playbackSpeeds" ) ,
322+ !deleted
323+ ) ;
324+ }
325+ let request = test:: TestRequest :: get ( )
326+ . uri ( "/sync/playbackSpeeds" )
327+ . to_request ( ) ;
328+ request. extensions_mut ( ) . insert ( account. clone ( ) ) ;
329+ let collection: serde_json:: Value = test:: call_and_read_body_json ( & app, request) . await ;
330+ assert_eq ! ( collection[ "revision" ] , 0 ) ;
331+ assert ! ( collection[ "payload" ] . is_null( ) ) ;
332+
333+ // The legacy document stays readable for older clients.
334+ let request = test:: TestRequest :: get ( ) . uri ( "/sync/legacy" ) . to_request ( ) ;
335+ request. extensions_mut ( ) . insert ( account) ;
336+ let legacy: serde_json:: Value = test:: call_and_read_body_json ( & app, request) . await ;
337+ assert_eq ! ( legacy[ "payload" ] , "legacy-ciphertext" ) ;
338+ }
339+ }
0 commit comments