2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2025-02-16 16:33:15 +01:00
// https://duplicati.com, hello@duplicati.com
2025-06-11 08:57:29 +02:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-02-16 16:33:15 +01:00
// Software is furnished to do so, subject to the following conditions:
2025-06-11 08:57:29 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-02-16 16:33:15 +01:00
// all copies or substantial portions of the Software.
2025-06-11 08:57:29 +02:00
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2020-08-23 14:25:34 -07:00
2019-01-17 19:52:41 -08:00
using System ;
2019-01-16 20:27:02 -08:00
using System.Collections.Generic ;
using System.IO ;
2025-06-11 08:58:32 +02:00
using System.Threading.Tasks ;
2024-06-07 15:56:43 +02:00
using Duplicati.Library.RestAPI ;
2025-02-16 16:33:15 +01:00
using Duplicati.Library.SQLiteHelper ;
2020-08-23 14:25:34 -07:00
using Duplicati.Server ;
using Duplicati.Server.Database ;
2020-08-23 20:25:11 -07:00
using Duplicati.Server.Serializable ;
using Duplicati.Server.Serialization ;
2020-08-23 14:25:34 -07:00
using Duplicati.Server.Serialization.Interface ;
2025-05-05 16:13:46 +02:00
using Duplicati.WebserverCore.Services ;
2020-08-23 14:25:34 -07:00
using NUnit.Framework ;
using Backup = Duplicati . Server . Database . Backup ;
2025-06-13 11:48:24 +02:00
using Assert = NUnit . Framework . Legacy . ClassicAssert ;
2019-01-16 20:27:02 -08:00
namespace Duplicati.UnitTest
{
2022-05-06 21:46:17 -07:00
public class ImportExportTests : BasicSetupHelper
2019-01-16 20:27:02 -08:00
{
private string serverDatafolder ;
[OneTimeSetUp]
public void Setup ()
{
this . serverDatafolder = Path . Combine ( Path . GetTempPath (), Path . GetRandomFileName ());
Directory . CreateDirectory ( this . serverDatafolder );
}
[OneTimeTearDown]
public void TearDown ()
{
try
{
Directory . Delete ( this . serverDatafolder , true );
}
catch
{
2019-01-17 19:21:32 -08:00
// If there's an exception, let the OS deal with cleaning up the temp folder.
2019-01-16 20:27:02 -08:00
}
}
2020-08-23 14:25:34 -07:00
private IBackup CreateBackup ( string name , string username , string password , Dictionary < string , string > metadata )
2020-08-23 14:24:27 -07:00
{
return new Backup
{
Description = "Mock Backup Description" ,
Filters = new IFilter [ 0 ],
ID = "1" ,
Metadata = metadata ,
Name = name ,
2024-06-07 15:56:43 +02:00
Settings = new [] { new Setting { Name = "passphrase" , Value = "12345" } },
Sources = new [] { "Mock Backup Source" },
Tags = new [] { "Tags" },
2020-08-23 14:24:27 -07:00
TargetURL = $"file:///mock_backup_target?auth-username={username}&auth-password={password}"
};
}
2020-08-23 20:25:11 -07:00
[Test]
[Category("ImportExport")]
[TestCase(true)]
[TestCase(false)]
public void ExportToJSONEncoding ( bool removePasswords )
{
2024-06-07 15:56:43 +02:00
Dictionary < string , string > advancedOptions = new Dictionary < string , string > { { "server-datafolder" , this . serverDatafolder } };
2020-08-23 20:25:11 -07:00
string usernameKey = "auth-username" ;
string passwordKey = "auth-password" ;
string username = @"user%40email.com" ;
string password = @"abcde12345!@#$%/\" ;
2024-06-07 15:56:43 +02:00
IBackup backup = this . CreateBackup ( "backup" , username , password , new Dictionary < string , string >());
2020-08-23 20:25:11 -07:00
if ( removePasswords )
{
2024-06-07 15:56:43 +02:00
BackupImportExportHandler . RemovePasswords ( backup );
2020-08-23 20:25:11 -07:00
}
Assert . That ( backup . TargetURL , Does . Contain ( $"{usernameKey}={username}" ));
if ( removePasswords )
{
Assert . That ( backup . TargetURL , Does . Not . Contain ( passwordKey ));
Assert . That ( backup . TargetURL , Does . Not . Contain ( password ));
}
else
{
Assert . That ( backup . TargetURL , Does . Contain ( passwordKey ));
Assert . That ( backup . TargetURL , Does . Contain ( password ));
}
byte [] jsonByteArray ;
2025-07-10 21:03:16 +02:00
using ( var con = Program . GetDatabaseConnection ( new ApplicationSettings (), advancedOptions , true , false ))
2025-05-05 16:13:46 +02:00
jsonByteArray = BackupImportExportHandler . ExportToJSON ( con , backup , null );
2020-08-23 20:25:11 -07:00
// The username should not have the '%40' converted to '@' since the import code
// cannot handle it (see issue #3619).
string json = System . Text . Encoding . Default . GetString ( jsonByteArray );
Assert . That ( json , Does . Not . Contain ( "user@email.com" ));
ImportExportStructure importedConfiguration = Serializer . Deserialize < ImportExportStructure >( new StreamReader ( new MemoryStream ( jsonByteArray )));
Assert . AreEqual ( backup . Description , importedConfiguration . Backup . Description );
Assert . AreEqual ( backup . Name , importedConfiguration . Backup . Name );
Assert . AreEqual ( backup . TargetURL , importedConfiguration . Backup . TargetURL );
}
2019-01-16 20:27:02 -08:00
[Test]
[Category("ImportExport")]
2025-06-11 08:58:32 +02:00
public async Task RoundTrip ()
2019-01-16 20:27:02 -08:00
{
2025-02-16 16:33:15 +01:00
var metadata = new Dictionary < string , string > { { "SourceFilesCount" , "1" } };
var advancedOptions = new Dictionary < string , string > { { "server-datafolder" , this . serverDatafolder } };
var dbpath = Path . Combine ( this . serverDatafolder , Library . AutoUpdater . DataFolderManager . SERVER_DATABASE_FILENAME );
if ( File . Exists ( dbpath ))
File . Delete ( dbpath );
2025-06-12 08:34:29 +02:00
var con = await SQLiteLoader . LoadConnectionAsync (). ConfigureAwait ( false );
await SQLiteLoader . OpenDatabaseAsync ( con , dbpath , null ). ConfigureAwait ( false );
2025-04-25 12:30:03 +02:00
DatabaseUpgrader . UpgradeDatabase ( con , dbpath , typeof ( Library . RestAPI . Database . DatabaseSchemaMarker ));
2025-02-16 16:33:15 +01:00
2025-05-05 16:13:46 +02:00
using ( var connection = new Connection ( con , true , null , this . serverDatafolder , null ))
2019-01-16 20:27:02 -08:00
{
2019-01-17 19:41:55 -08:00
// Unencrypted file, don't import metadata.
2019-01-16 20:27:02 -08:00
string unencryptedWithoutMetadata = Path . Combine ( this . serverDatafolder , Path . GetRandomFileName ());
2025-02-16 16:33:15 +01:00
File . WriteAllBytes ( unencryptedWithoutMetadata , BackupImportExportHandler . ExportToJSON ( connection , this . CreateBackup ( "unencrypted without metadata" , "user" , "password" , metadata ), null ));
BackupImportExportHandler . ImportBackup ( connection , unencryptedWithoutMetadata , false , () => null );
Assert . AreEqual ( 1 , connection . Backups . Length );
Assert . AreEqual ( 0 , connection . Backups [ 0 ]. Metadata . Count );
2019-01-16 20:27:02 -08:00
2019-01-17 19:41:55 -08:00
// Unencrypted file, import metadata.
2019-01-16 20:27:02 -08:00
string unencryptedWithMetadata = Path . Combine ( this . serverDatafolder , Path . GetRandomFileName ());
2025-02-16 16:33:15 +01:00
File . WriteAllBytes ( unencryptedWithMetadata , BackupImportExportHandler . ExportToJSON ( connection , this . CreateBackup ( "unencrypted with metadata" , "user" , "password" , metadata ), null ));
BackupImportExportHandler . ImportBackup ( connection , unencryptedWithMetadata , true , () => null );
Assert . AreEqual ( 2 , connection . Backups . Length );
Assert . AreEqual ( metadata . Count , connection . Backups [ 1 ]. Metadata . Count );
2019-01-16 20:27:02 -08:00
2019-01-17 19:41:55 -08:00
// Encrypted file, don't import metadata.
2019-01-16 20:27:02 -08:00
string encryptedWithoutMetadata = Path . Combine ( this . serverDatafolder , Path . GetRandomFileName ());
string passphrase = "abcde" ;
2025-02-16 16:33:15 +01:00
File . WriteAllBytes ( encryptedWithoutMetadata , BackupImportExportHandler . ExportToJSON ( connection , this . CreateBackup ( "encrypted without metadata" , "user" , "password" , metadata ), passphrase ));
BackupImportExportHandler . ImportBackup ( connection , encryptedWithoutMetadata , false , () => passphrase );
Assert . AreEqual ( 3 , connection . Backups . Length );
Assert . AreEqual ( 0 , connection . Backups [ 2 ]. Metadata . Count );
2019-01-16 20:27:02 -08:00
2019-01-17 19:41:55 -08:00
// Encrypted file, import metadata.
2019-01-16 20:27:02 -08:00
string encryptedWithMetadata = Path . Combine ( this . serverDatafolder , Path . GetRandomFileName ());
2025-02-16 16:33:15 +01:00
File . WriteAllBytes ( encryptedWithMetadata , BackupImportExportHandler . ExportToJSON ( connection , this . CreateBackup ( "encrypted with metadata" , "user" , "password" , metadata ), passphrase ));
BackupImportExportHandler . ImportBackup ( connection , encryptedWithMetadata , true , () => passphrase );
Assert . AreEqual ( 4 , connection . Backups . Length );
Assert . AreEqual ( metadata . Count , connection . Backups [ 3 ]. Metadata . Count );
2019-01-17 19:52:41 -08:00
// Encrypted file, incorrect passphrase.
2025-02-16 16:33:15 +01:00
Assert . Throws ( Is . InstanceOf < Exception >(), () => BackupImportExportHandler . ImportBackup ( connection , encryptedWithMetadata , true , () => passphrase + " " ));
2019-01-16 20:27:02 -08:00
}
}
}
2020-08-23 14:25:34 -07:00
}