@@ -394,7 +394,11 @@ async def test_api_backup_errors(
394394 assert job ["child_jobs" ][1 ]["child_jobs" ][0 ]["name" ] == "backup_addon_save"
395395 assert job ["child_jobs" ][1 ]["child_jobs" ][0 ]["reference" ] == "local_ssh"
396396 assert job ["child_jobs" ][1 ]["child_jobs" ][0 ]["errors" ] == [
397- {"type" : "BackupError" , "message" : "Can't create backup for local_ssh" }
397+ {
398+ "type" : "BackupError" ,
399+ "message" : "Can't create backup for local_ssh" ,
400+ "stage" : None ,
401+ }
398402 ]
399403 assert job ["child_jobs" ][2 ]["name" ] == "backup_store_folders"
400404 assert job ["child_jobs" ][2 ]["reference" ] == slug
@@ -425,11 +429,21 @@ async def test_api_backup_errors(
425429
426430 assert job ["name" ] == f"backup_manager_{ backup_type } _backup"
427431 assert job ["done" ] is True
428- assert job ["errors" ] == (
429- err := [{"type" : "HomeAssistantBackupError" , "message" : "Backup error" }]
430- )
432+ assert job ["errors" ] == [
433+ {
434+ "type" : "HomeAssistantBackupError" ,
435+ "message" : "Backup error" ,
436+ "stage" : "home_assistant" ,
437+ }
438+ ]
431439 assert job ["child_jobs" ][0 ]["name" ] == "backup_store_homeassistant"
432- assert job ["child_jobs" ][0 ]["errors" ] == err
440+ assert job ["child_jobs" ][0 ]["errors" ] == [
441+ {
442+ "type" : "HomeAssistantBackupError" ,
443+ "message" : "Backup error" ,
444+ "stage" : None ,
445+ }
446+ ]
433447 assert len (job ["child_jobs" ]) == 1
434448
435449
@@ -625,22 +639,28 @@ async def test_upload_download(
625639
626640@pytest .mark .usefixtures ("path_extern" , "tmp_supervisor_data" )
627641@pytest .mark .parametrize (
628- ("backup_type" , "inputs" ), [("full" , {}), ("partial" , {"folders" : ["ssl" ]})]
642+ ("backup_type" , "inputs" , "locations" ),
643+ [
644+ ("full" , {}, [None , ".cloud_backup" ]),
645+ ("full" , {}, [".cloud_backup" , None ]),
646+ ("partial" , {"folders" : ["ssl" ]}, [None , ".cloud_backup" ]),
647+ ("partial" , {"folders" : ["ssl" ]}, [".cloud_backup" , None ]),
648+ ],
629649)
630650async def test_backup_to_multiple_locations (
631651 api_client : TestClient ,
632652 coresys : CoreSys ,
633653 backup_type : str ,
634654 inputs : dict [str , Any ],
655+ locations : list [str | None ],
635656):
636657 """Test making a backup to multiple locations."""
637658 await coresys .core .set_state (CoreState .RUNNING )
638659 coresys .hardware .disk .get_disk_free_space = lambda x : 5000
639660
640661 resp = await api_client .post (
641662 f"/backups/new/{ backup_type } " ,
642- json = {"name" : "Multiple locations test" , "location" : [None , ".cloud_backup" ]}
643- | inputs ,
663+ json = {"name" : "Multiple locations test" , "location" : locations } | inputs ,
644664 )
645665 assert resp .status == 200
646666 result = await resp .json ()
@@ -658,6 +678,56 @@ async def test_backup_to_multiple_locations(
658678 assert coresys .backups .get (slug ).location is None
659679
660680
681+ @pytest .mark .usefixtures ("path_extern" , "tmp_supervisor_data" )
682+ @pytest .mark .parametrize (
683+ ("backup_type" , "inputs" ), [("full" , {}), ("partial" , {"folders" : ["ssl" ]})]
684+ )
685+ async def test_backup_to_multiple_locations_error_on_copy (
686+ api_client : TestClient ,
687+ coresys : CoreSys ,
688+ backup_type : str ,
689+ inputs : dict [str , Any ],
690+ ):
691+ """Test making a backup to multiple locations that fails during copy stage."""
692+ await coresys .core .set_state (CoreState .RUNNING )
693+ coresys .hardware .disk .get_disk_free_space = lambda x : 5000
694+
695+ with patch ("supervisor.backups.manager.copy" , side_effect = OSError ):
696+ resp = await api_client .post (
697+ f"/backups/new/{ backup_type } " ,
698+ json = {
699+ "name" : "Multiple locations test" ,
700+ "location" : [None , ".cloud_backup" ],
701+ }
702+ | inputs ,
703+ )
704+ assert resp .status == 200
705+ result = await resp .json ()
706+ assert result ["result" ] == "ok"
707+ slug = result ["data" ]["slug" ]
708+
709+ orig_backup = coresys .config .path_backup / f"{ slug } .tar"
710+ assert await coresys .run_in_executor (orig_backup .exists )
711+ assert coresys .backups .get (slug ).all_locations == {
712+ None : {"path" : orig_backup , "protected" : False , "size_bytes" : 10240 },
713+ }
714+ assert coresys .backups .get (slug ).location is None
715+
716+ resp = await api_client .get ("/jobs/info" )
717+ assert resp .status == 200
718+ result = await resp .json ()
719+ assert result ["data" ]["jobs" ][0 ]["name" ] == f"backup_manager_{ backup_type } _backup"
720+ assert result ["data" ]["jobs" ][0 ]["reference" ] == slug
721+ assert result ["data" ]["jobs" ][0 ]["done" ] is True
722+ assert result ["data" ]["jobs" ][0 ]["errors" ] == [
723+ {
724+ "type" : "BackupError" ,
725+ "message" : "Could not copy backup to .cloud_backup due to: " ,
726+ "stage" : "copy_additional_locations" ,
727+ }
728+ ]
729+
730+
661731@pytest .mark .parametrize (
662732 ("backup_type" , "inputs" ), [("full" , {}), ("partial" , {"folders" : ["ssl" ]})]
663733)
0 commit comments