577 lines
26 KiB
C#
577 lines
26 KiB
C#
internal static partial class Program
|
|
{
|
|
static Task TestVaultCryptoRoundtripAsync()
|
|
{
|
|
var crypto = new VaultCryptoService();
|
|
var plaintext = "sample vault payload";
|
|
var payload = crypto.EncryptData(System.Text.Encoding.UTF8.GetBytes(plaintext), "vault-pass-123");
|
|
|
|
Assert(payload.Length == VaultCryptoService.SaltSize + VaultCryptoService.NonceSize + VaultCryptoService.TagSize + plaintext.Length, "Vault payload length should match salt+nonce+tag+ciphertext layout.");
|
|
var decrypted = crypto.DecryptData(payload, "vault-pass-123");
|
|
Assert(System.Text.Encoding.UTF8.GetString(decrypted) == plaintext, "Vault roundtrip decrypt should return original plaintext.");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultCryptoDecryptsPythonFixtureAsync()
|
|
{
|
|
var crypto = new VaultCryptoService();
|
|
|
|
var payload = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODwABAgMEBQYHCAkKC6AErhDEMERBl7OFkG4L4oZ2JZckS0VzhxaZoVLckF7VXE+NIYXILsJ8f1I=");
|
|
var expectedPlaintext = Convert.FromBase64String("dmF1bHQgcGF5bG9hZCBleGFtcGxlCmxpbmUy");
|
|
var decrypted = crypto.DecryptData(payload, "vault-pass-123");
|
|
|
|
Assert(decrypted.SequenceEqual(expectedPlaintext), "C# decrypt should match Python-generated payload plaintext.");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultKeyDerivationMatchesPythonAsync()
|
|
{
|
|
var crypto = new VaultCryptoService();
|
|
var salt = Enumerable.Range(0, VaultCryptoService.SaltSize).Select(i => (byte)i).ToArray();
|
|
var key = crypto.DeriveKey("vault-pass-123", salt);
|
|
var expectedKeyHex = "b29f523f28bf178f6815c6ca9ee2a588d79b3bd9a822c92a2f0dde5bc853bb52";
|
|
var actualKeyHex = Convert.ToHexString(key).ToLowerInvariant();
|
|
|
|
Assert(actualKeyHex == expectedKeyHex, "Derived key should match Python PBKDF2 fixture key.");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultMonthlyFilenameParityAsync()
|
|
{
|
|
IVaultStorageService vaultStorage = new VaultStorageService(new VaultCryptoService());
|
|
var name = vaultStorage.GetMonthlyVaultFileName(new DateTime(2026, 2, 7));
|
|
Assert(name == "2026-02.vault", "Monthly vault filename must match yyyy-MM.vault format.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultLoadClearsAndExtractsAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dataDir, "old_file.md"), "stale");
|
|
|
|
var zipBytes = CreateZipBytes(new Dictionary<string, string>
|
|
{
|
|
["2026-02-01.md"] = "hello from vault"
|
|
});
|
|
var crypto = new VaultCryptoService();
|
|
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
|
|
File.WriteAllBytes(Path.Combine(vaultDir, "2026-02.vault"), encrypted);
|
|
|
|
IVaultStorageService storage = new VaultStorageService(crypto);
|
|
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
|
|
|
|
Assert(ok, "Expected vault load success with correct password.");
|
|
Assert(!File.Exists(Path.Combine(dataDir, "old_file.md")), "Data directory should be cleared before extraction.");
|
|
var extractedPath = Path.Combine(dataDir, "2026-02-01.md");
|
|
Assert(File.Exists(extractedPath), "Expected markdown file extracted from vault archive.");
|
|
Assert(File.ReadAllText(extractedPath) == "hello from vault", "Extracted file content mismatch.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultLoadWrongPasswordPreservesVaultAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
var zipBytes = CreateZipBytes(new Dictionary<string, string>
|
|
{
|
|
["2026-02-01.md"] = "hello from vault"
|
|
});
|
|
var crypto = new VaultCryptoService();
|
|
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
|
|
var vaultPath = Path.Combine(vaultDir, "2026-02.vault");
|
|
File.WriteAllBytes(vaultPath, encrypted);
|
|
var before = File.ReadAllBytes(vaultPath);
|
|
|
|
IVaultStorageService storage = new VaultStorageService(crypto);
|
|
var ok = storage.LoadAllVaults("wrong-password", vaultDir, dataDir);
|
|
var after = File.ReadAllBytes(vaultPath);
|
|
|
|
Assert(!ok, "Expected vault load failure with wrong password.");
|
|
Assert(before.SequenceEqual(after), "Vault file bytes should remain unchanged on wrong password.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultLoadLegacyInitVaultHandlingAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
var legacyPath = Path.Combine(vaultDir, "_init_vault.vault");
|
|
File.WriteAllBytes(legacyPath, [1, 2, 3, 4]);
|
|
|
|
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
|
|
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
|
|
|
|
Assert(ok, "Legacy-only vault directory should still be treated as successful load state.");
|
|
Assert(!File.Exists(legacyPath), "Legacy _init_vault.vault should be removed during load.");
|
|
Assert(Directory.Exists(dataDir), "Data directory should exist after load workflow.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultCurrentMonthSaveOptimizedAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb one");
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two");
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan one");
|
|
|
|
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
|
|
var now = new DateTime(2026, 2, 22, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
var firstSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
|
|
Assert(firstSaved, "Expected first current-month save to write vault data.");
|
|
|
|
var febVaultPath = Path.Combine(vaultDir, "2026-02.vault");
|
|
var janVaultPath = Path.Combine(vaultDir, "2026-01.vault");
|
|
Assert(File.Exists(febVaultPath), "Expected current-month vault file to be created.");
|
|
Assert(!File.Exists(janVaultPath), "Current-month save should not write non-current month vault files.");
|
|
|
|
var entries = ReadVaultEntryTexts(febVaultPath, "vault-pass-123");
|
|
Assert(entries.Count == 2, "Current-month vault should include only current-month markdown files.");
|
|
Assert(entries.ContainsKey("2026-02-01.md"), "Missing first current-month entry in vault archive.");
|
|
Assert(entries.ContainsKey("2026-02-18.md"), "Missing second current-month entry in vault archive.");
|
|
Assert(!entries.ContainsKey("2026-01-31.md"), "Current-month vault must not include previous-month files.");
|
|
|
|
var beforeSkipBytes = File.ReadAllBytes(febVaultPath);
|
|
var secondSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
|
|
var afterSkipBytes = File.ReadAllBytes(febVaultPath);
|
|
Assert(!secondSaved, "Expected unchanged current-month save to skip write.");
|
|
Assert(beforeSkipBytes.SequenceEqual(afterSkipBytes), "Vault bytes should remain unchanged when save is skipped.");
|
|
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two changed");
|
|
var thirdSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
|
|
Assert(thirdSaved, "Expected save to run after current-month file change.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultRebuildAllVaultsAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan body");
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb body");
|
|
File.WriteAllText(Path.Combine(dataDir, "not-a-journal.md"), "should be ignored");
|
|
|
|
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
|
|
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
|
|
|
|
var janVaultPath = Path.Combine(vaultDir, "2026-01.vault");
|
|
var febVaultPath = Path.Combine(vaultDir, "2026-02.vault");
|
|
Assert(File.Exists(janVaultPath), "Expected January vault from rebuild flow.");
|
|
Assert(File.Exists(febVaultPath), "Expected February vault from rebuild flow.");
|
|
|
|
var janEntries = ReadVaultEntryTexts(janVaultPath, "vault-pass-123");
|
|
var febEntries = ReadVaultEntryTexts(febVaultPath, "vault-pass-123");
|
|
|
|
Assert(janEntries.Count == 1 && janEntries.ContainsKey("2026-01-31.md"), "January vault contents mismatch.");
|
|
Assert(febEntries.Count == 1 && febEntries.ContainsKey("2026-02-01.md"), "February vault contents mismatch.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultClearDataDirectoryAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(dataDir);
|
|
Directory.CreateDirectory(Path.Combine(dataDir, "nested"));
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "decrypted content");
|
|
File.WriteAllText(Path.Combine(dataDir, "journal_cache.db"), "cache");
|
|
File.WriteAllText(Path.Combine(dataDir, "nested", "tmp.txt"), "temp");
|
|
|
|
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
|
|
storage.ClearDataDirectory(dataDir);
|
|
|
|
Assert(Directory.Exists(dataDir), "Data directory should be recreated after cleanup.");
|
|
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after cleanup.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static async Task TestEntryVaultLoadAllEmptyAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
|
|
try
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.load_all",
|
|
payload = new
|
|
{
|
|
password = "vault-pass-123",
|
|
vaultDirectory = vaultDir,
|
|
dataDirectory = dataDir,
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for empty vault directory load.");
|
|
Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault directory.");
|
|
Assert(Directory.Exists(dataDir), "Expected data directory to be created by load workflow.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestEntryVaultClearDataDirectoryAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(dataDir);
|
|
File.WriteAllText(Path.Combine(dataDir, "tmp.md"), "x");
|
|
|
|
try
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.clear_data_directory",
|
|
payload = new
|
|
{
|
|
dataDirectory = dataDir,
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for clear_data_directory.");
|
|
Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected clear_data_directory result=true.");
|
|
Assert(Directory.Exists(dataDir), "Expected data directory to exist after clear.");
|
|
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Expected data directory to be empty after clear.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static Task TestSidecarVaultCliLoadAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
|
|
try
|
|
{
|
|
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
|
|
var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
|
|
|
|
Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory.");
|
|
Assert(Directory.Exists(dataDir), "Expected data directory to be created by vault load CLI command.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestSidecarVaultCliSaveAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-22.md"), "entry body");
|
|
|
|
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
|
|
var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
|
|
|
|
Assert(exitCode == 0, "Expected vault save CLI command to succeed.");
|
|
Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault file to be written by save CLI command.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultCustomEntryRoundtripAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
// Create both date-named and custom-named entries
|
|
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "date entry");
|
|
File.WriteAllText(Path.Combine(dataDir, "My Custom Entry.md"), "custom entry body");
|
|
File.WriteAllText(Path.Combine(dataDir, "Work Notes.md"), "work notes body");
|
|
|
|
// Rebuild vaults (simulates app close)
|
|
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
|
|
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
|
|
|
|
// Verify custom vault was created
|
|
var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault");
|
|
Assert(File.Exists(customVaultPath), "Expected _custom_entries.vault to be created.");
|
|
Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault for date entry.");
|
|
|
|
// Clear data directory (simulates app close step 2)
|
|
storage.ClearDataDirectory(dataDir);
|
|
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after clear.");
|
|
|
|
// Load vaults (simulates app restart)
|
|
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
|
|
Assert(ok, "Expected vault load to succeed.");
|
|
|
|
// Verify all entries are restored
|
|
Assert(File.Exists(Path.Combine(dataDir, "2026-02-01.md")), "Date entry should be restored from vault.");
|
|
Assert(File.Exists(Path.Combine(dataDir, "My Custom Entry.md")), "Custom entry should be restored from vault.");
|
|
Assert(File.Exists(Path.Combine(dataDir, "Work Notes.md")), "Second custom entry should be restored from vault.");
|
|
Assert(File.ReadAllText(Path.Combine(dataDir, "My Custom Entry.md")) == "custom entry body", "Custom entry content mismatch.");
|
|
Assert(File.ReadAllText(Path.Combine(dataDir, "Work Notes.md")) == "work notes body", "Second custom entry content mismatch.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static async Task TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var vaultDir = Path.Combine(root, "vault");
|
|
var dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
try
|
|
{
|
|
var zipBytes = CreateZipBytes(new Dictionary<string, string>
|
|
{
|
|
["2026-02-01.md"] = "hello from vault"
|
|
});
|
|
var crypto = new VaultCryptoService();
|
|
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
|
|
File.WriteAllBytes(Path.Combine(vaultDir, "2026-02.vault"), encrypted);
|
|
|
|
var entry = NewEntry();
|
|
|
|
var loadRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.load_all",
|
|
payload = new
|
|
{
|
|
password = "wrong-password",
|
|
vaultDirectory = vaultDir,
|
|
dataDirectory = dataDir
|
|
}
|
|
});
|
|
var loadResponse = await entry.HandleCommandAsync(loadRequest);
|
|
using var loadDoc = JsonDocument.Parse(loadResponse);
|
|
Assert(loadDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected vault.load_all response envelope to be ok=true.");
|
|
Assert(!loadDoc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=false with wrong password.");
|
|
|
|
var listRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "lists.list"
|
|
});
|
|
var listResponse = await entry.HandleCommandAsync(listRequest);
|
|
using var listDoc = JsonDocument.Parse(listResponse);
|
|
Assert(!listDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected lists.list to fail while locked.");
|
|
var error = listDoc.RootElement.GetProperty("error").GetString() ?? "";
|
|
Assert(error.Contains("database is locked", StringComparison.OrdinalIgnoreCase), "Expected locked-session error after failed vault.load_all.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestEntryTemplateSaveAutoSyncsVaultAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-entry-vault-sync-smoke", Guid.NewGuid().ToString("N"));
|
|
var projectRoot = Path.Combine(root, "project");
|
|
var appDirectory = Path.Combine(projectRoot, "journal");
|
|
var vaultDir = Path.Combine(appDirectory, "vault");
|
|
var dataDir = Path.Combine(appDirectory, "data");
|
|
Directory.CreateDirectory(vaultDir);
|
|
Directory.CreateDirectory(dataDir);
|
|
|
|
var previousProjectRoot = Environment.GetEnvironmentVariable("JOURNAL_PROJECT_ROOT");
|
|
var previousDataDir = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR");
|
|
var previousVaultDir = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR");
|
|
Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", projectRoot);
|
|
Environment.SetEnvironmentVariable("JOURNAL_DATA_DIR", dataDir);
|
|
Environment.SetEnvironmentVariable("JOURNAL_VAULT_DIR", vaultDir);
|
|
|
|
try
|
|
{
|
|
var entry = NewEntry();
|
|
var password = "vault-pass-123";
|
|
|
|
var unlockRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.load_all",
|
|
payload = new
|
|
{
|
|
password,
|
|
vaultDirectory = vaultDir,
|
|
dataDirectory = dataDir
|
|
}
|
|
});
|
|
var unlockResponse = await entry.HandleCommandAsync(unlockRequest);
|
|
using var unlockDoc = JsonDocument.Parse(unlockResponse);
|
|
Assert(unlockDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected vault.load_all envelope to succeed.");
|
|
Assert(unlockDoc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault.");
|
|
|
|
var saveTemplateRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "templates.save",
|
|
payload = new
|
|
{
|
|
name = "Weekly Review",
|
|
content = "## Wins\n- shipped feature",
|
|
dataDirectory = dataDir
|
|
}
|
|
});
|
|
var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest);
|
|
using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse);
|
|
Assert(saveTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected templates.save to succeed.");
|
|
|
|
var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault");
|
|
Assert(File.Exists(customVaultPath), "Expected template save to auto-sync custom entries vault.");
|
|
|
|
var entries = ReadVaultEntryTexts(customVaultPath, password);
|
|
Assert(entries.ContainsKey("Weekly Review.template.md"), "Expected template file in custom vault archive.");
|
|
|
|
var clearRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.clear_data_directory",
|
|
payload = new
|
|
{
|
|
dataDirectory = dataDir
|
|
}
|
|
});
|
|
var clearResponse = await entry.HandleCommandAsync(clearRequest);
|
|
using var clearDoc = JsonDocument.Parse(clearResponse);
|
|
Assert(clearDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected vault.clear_data_directory to succeed.");
|
|
|
|
var reloadResponse = await entry.HandleCommandAsync(unlockRequest);
|
|
using var reloadDoc = JsonDocument.Parse(reloadResponse);
|
|
Assert(reloadDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected second vault.load_all envelope to succeed.");
|
|
Assert(reloadDoc.RootElement.GetProperty("data").GetBoolean(), "Expected second vault.load_all data=true.");
|
|
|
|
var restoredTemplatePath = Path.Combine(dataDir, "Weekly Review.template.md");
|
|
Assert(File.Exists(restoredTemplatePath), "Expected template to be restored from vault after reload.");
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", previousProjectRoot);
|
|
Environment.SetEnvironmentVariable("JOURNAL_DATA_DIR", previousDataDir);
|
|
Environment.SetEnvironmentVariable("JOURNAL_VAULT_DIR", previousVaultDir);
|
|
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|