Fixed SaveDb for CachingFileStorage and target file not existing

-> + Tests
First, very simple implementation of ChangingFileStorage in KP2A App (preliminary)
This commit is contained in:
Philipp Crocoll 2013-08-08 20:40:02 +02:00
parent 289e10e1c4
commit a671c4f241
8 changed files with 246 additions and 16 deletions

View File

@ -20,7 +20,7 @@ namespace keepass2android.Io
/// </summary>
/// <param name="ioc">The file which we tried to write</param>
/// <param name="e">The exception why the remote file couldn't be updated</param>
void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e);
void CouldntSaveToRemote(IOConnectionInfo ioc, Exception ex);
/// <summary>
/// Called when only the local file could be opened during an open operation.

View File

@ -99,12 +99,10 @@ namespace keepass2android
}
if (
(_streamForOrigFile != null)
|| fileStorage.CheckForFileChangeFast(ioc, _app.GetDb().LastFileVersion) //first try to use the fast change detection
|| (FileHashChanged(ioc, _app.GetDb().KpDatabase.HashOfFileOnDisk)) //if that fails, hash the file and compare:
|| (FileHashChanged(ioc, _app.GetDb().KpDatabase.HashOfFileOnDisk) == FileHashChange.Changed) //if that fails, hash the file and compare:
)
{
@ -243,14 +241,26 @@ namespace keepass2android
_app.GetDb().LastFileVersion = fileStorage.GetCurrentFileVersionFast(ioc);
}
public byte[] HashFile(IOConnectionInfo iocFile)
public byte[] HashOriginalFile(IOConnectionInfo iocFile)
{
if (iocFile == null) { Debug.Assert(false); return null; } // Assert only
Stream sIn;
try
{
sIn = _app.GetFileStorage(iocFile).OpenFileForRead(iocFile);
IFileStorage fileStorage = _app.GetFileStorage(iocFile);
CachingFileStorage cachingFileStorage = fileStorage as CachingFileStorage;
if (cachingFileStorage != null)
{
string hash;
cachingFileStorage.GetRemoteDataAndHash(iocFile, out hash);
return MemUtil.HexStringToByteArray(hash);
}
else
{
sIn = fileStorage.OpenFileForRead(iocFile);
}
if (sIn == null) throw new FileNotFoundException();
}
catch (Exception) { return null; }
@ -267,10 +277,20 @@ namespace keepass2android
return pbHash;
}
private bool FileHashChanged(IOConnectionInfo ioc, byte[] hashOfFileOnDisk)
enum FileHashChange
{
Equal,
Changed,
FileNotAvailable
}
private FileHashChange FileHashChanged(IOConnectionInfo ioc, byte[] hashOfFileOnDisk)
{
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.CheckingTargetFileForChanges));
return !MemUtil.ArraysEqual(HashFile(ioc), hashOfFileOnDisk);
byte[] fileHash = HashOriginalFile(ioc);
if (fileHash == null)
return FileHashChange.FileNotAvailable;
return MemUtil.ArraysEqual(fileHash, hashOfFileOnDisk) ? FileHashChange.Equal : FileHashChange.Changed;
}

View File

@ -71,6 +71,7 @@
<Compile Include="TestLoadDbCredentials.cs" />
<Compile Include="TestCachingFileStorage.cs" />
<Compile Include="TestSaveDb.cs" />
<Compile Include="TestSaveDbCached.cs" />
<Compile Include="TestSynchronizeCachedDatabase.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -19,8 +19,10 @@ namespace Kp2aUnitTests
TestRunner runner = new TestRunner();
// Run all tests from this assembly
runner.AddTests(Assembly.GetExecutingAssembly());
//runner.AddTests(new List<Type> { typeof(TestSynchronizeCachedDatabase) });
//runner.AddTests(new List<Type> { typeof(TestLoadDb) });}}
//runner.AddTests(new List<Type> { typeof(TestSaveDbCached) });
//runner.AddTests(typeof(TestSaveDbCached).GetMethod("TestLoadEditSaveWhenModified"));
//runner.AddTests(new List<Type> { typeof(TestSaveDb) });
//runner.AddTests(new List<Type> { typeof(TestCachingFileStorage) });
//runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote"));
//runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly"));

View File

@ -191,8 +191,6 @@ namespace Kp2aUnitTests
Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
Assert.IsFalse(_testCacheSupervisor.RestoredRemoteCalled);
}

View File

@ -126,6 +126,35 @@ namespace Kp2aUnitTests
}
[TestMethod]
public void TestLoadEditSaveWithWriteBecauseTargetNotExists()
{
//create the default database:
IKp2aApp app = SetupAppWithDefaultDatabase();
//save it and reload it so we have a base version
SaveDatabase(app);
app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//modify the database by adding a group:
app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true);
//delete the file:
File.Delete(DefaultFilename);
//save the database:
SaveDatabase(app);
//make sure no question was asked
Assert.AreEqual(null, ((TestKp2aApp)app).LastYesNoCancelQuestionTitle);
//load database to a new app instance:
IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//ensure the file was saved:
AssertDatabasesAreEqual(app.GetDb().KpDatabase, resultApp.GetDb().KpDatabase);
}
[TestMethod]
public void TestLoadEditSaveWithSyncOverwriteBecauseOfNoCheck()
{

View File

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using KeePassLib;
using KeePassLib.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using keepass2android;
using keepass2android.Io;
namespace Kp2aUnitTests
{
[TestClass]
class TestSaveDbCached: TestBase
{
private TestCacheSupervisor _testCacheSupervisor = new TestCacheSupervisor();
private TestFileStorage _testFileStorage = new TestFileStorage();
protected override TestKp2aApp CreateTestKp2aApp()
{
TestKp2aApp app = base.CreateTestKp2aApp();
app.FileStorage = new CachingFileStorage(_testFileStorage, "/mnt/sdcard/kp2atest/cache/", _testCacheSupervisor);
return app;
}
[TestMethod]
public void TestLoadEditSave()
{
//create the default database:
IKp2aApp app = SetupAppWithDefaultDatabase();
IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename });
//save it and reload it so we have a base version
SaveDatabase(app);
app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//modify the database by adding a group:
app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true);
//save the database again:
SaveDatabase(app);
Assert.IsNull(((TestKp2aApp)app).LastYesNoCancelQuestionTitle);
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
//load database to a new app instance:
IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//ensure the change was saved:
AssertDatabasesAreEqual(app.GetDb().KpDatabase, resultApp.GetDb().KpDatabase);
}
[TestMethod]
public void TestLoadEditSaveWhenDeleted()
{
//create the default database:
IKp2aApp app = SetupAppWithDefaultDatabase();
IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename });
//save it and reload it so we have a base version
SaveDatabase(app);
app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//delete the file:
File.Delete(DefaultFilename);
//modify the database by adding a group:
app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true);
//save the database again:
SaveDatabase(app);
Assert.IsNull(((TestKp2aApp) app).LastYesNoCancelQuestionTitle);
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
//load database to a new app instance:
IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//ensure the change was saved:
AssertDatabasesAreEqual(app.GetDb().KpDatabase, resultApp.GetDb().KpDatabase);
}
[TestMethod]
public void TestLoadEditSaveWhenModified()
{
//create the default database:
IKp2aApp app = SetupAppWithDefaultDatabase();
IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename });
//save it and reload it so we have a base version
SaveDatabase(app);
app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups)
Kp2aLog.Log("app c: " + group.Name);
//load once more:
var app2 = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
//modifiy once:
PwGroup group2 = new PwGroup(true, true, "TestGroup2", PwIcon.Apple);
app2.GetDb().KpDatabase.RootGroup.AddGroup(group2, true);
foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups)
Kp2aLog.Log("app b: " + group.Name);
SaveDatabase(app2);
foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups)
Kp2aLog.Log("app d: " + group.Name);
Assert.IsNull(((TestKp2aApp)app).LastYesNoCancelQuestionTitle);
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
//modify the database by adding a group:
PwGroup group1 = new PwGroup(true, true, "TestGroup", PwIcon.Apple);
app.GetDb().KpDatabase.RootGroup.AddGroup(group1, true);
foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups)
Kp2aLog.Log("app a: " + group.Name);
//save the database again:
SaveDatabase(app);
Assert.AreEqual(((TestKp2aApp)app).LastYesNoCancelQuestionTitle, UiStringKey.TitleSyncQuestion);
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
//load database to a new app instance:
IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
app2.GetDb().KpDatabase.RootGroup.AddGroup(group1, true);
foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups)
Kp2aLog.Log("app: "+group.Name);
foreach (var group in resultApp.GetDb().KpDatabase.RootGroup.Groups)
Kp2aLog.Log("resultApp: " + group.Name);
//ensure the change was saved:
AssertDatabasesAreEqual(app2.GetDb().KpDatabase, resultApp.GetDb().KpDatabase);
}
}
}

View File

@ -20,6 +20,7 @@ using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Widget;
using KeePassLib.Serialization;
using Android.Preferences;
using keepass2android.Io;
@ -57,7 +58,7 @@ namespace keepass2android
/// <summary>
/// Main implementation of the IKp2aApp interface for usage in the real app.
/// </summary>
public class Kp2aApp: IKp2aApp
public class Kp2aApp: IKp2aApp, ICacheSupervisor
{
public bool IsShutdown()
{
@ -253,7 +254,13 @@ namespace keepass2android
public IFileStorage GetFileStorage(IOConnectionInfo iocInfo)
{
if (iocInfo.IsLocalFile())
return new BuiltInFileStorage();
else
{
//todo: check if desired
return new CachingFileStorage(new BuiltInFileStorage(), Application.Context.CacheDir.Path, this);
}
}
public void TriggerReload(Context ctx)
@ -294,6 +301,31 @@ namespace keepass2android
_db = new Database(new DrawableFactory(), this);
return _db;
}
void ShowToast(string message)
{
var handler = new Handler(Looper.MainLooper);
handler.Post(() => { Toast.MakeText(Application.Context, message, ToastLength.Long).Show(); });
}
public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e)
{
//TODO use resource strings
ShowToast("Couldn't save to remote: "+e.Message+". Save again or use Sync menu when remote connection is available again.");
}
//todo: test changes in SaveDb with Cache: Save without conflict, save with conflict
//add test?
public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex)
{
ShowToast("Couldn't open from remote: " + ex.Message+". Loaded file from local cache. You can still make changes in the database and sync them later.");
}
public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc)
{
ShowToast("Opened local file due to conflict with changes in remote file. Use Synchronize menu to merge.");
}
}