Files
Omika/tests/Omika.Tests/Mobile/Local/LegacyTokenMigratorTests.cs
2026-10-23 18:46:50 +02:00

269 lines
8.9 KiB
C#

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Omika.App.Local.Abstractions;
using Omika.App.Local.Security;
namespace Omika.Tests.Mobile.Local;
/// <summary>
/// Tests de la migration idempotente et crash-safe de l'ancien token en clair
/// (<c>session.token</c>) vers un stockage sécurisé (<see cref="ISecureTokenStore"/>).
/// Aucun runtime Android/iOS requis : le store est simulé, le fichier legacy est
/// dans un répertoire temporaire.
/// </summary>
public class LegacyTokenMigratorTests
{
private static string NewTempDir()
{
string dir = Path.Combine(Path.GetTempPath(), "omika-migrate-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
return dir;
}
private static string LegacyPath(string dir) => Path.Combine(dir, "session.token");
private static void WriteLegacy(string dir, string token) => File.WriteAllText(LegacyPath(dir), token);
private static void DeleteDir(string dir)
{
try { Directory.Delete(dir, recursive: true); }
catch { /* best-effort */ }
}
[Fact]
public async Task Migrate_SecureAbsentLegacyAbsent_NoSession()
{
string dir = NewTempDir();
try
{
var store = new InMemoryStore();
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.NoSession, outcome);
Assert.Null(store.Value);
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_SecureAbsentLegacyPresent_MigratedEtLegacySupprime()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, "TOKEN_1");
var store = new InMemoryStore();
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.Migrated, outcome);
Assert.Equal("TOKEN_1", store.Value);
Assert.False(File.Exists(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_SecurePresentLegacyAbsent_SecureOnly()
{
string dir = NewTempDir();
try
{
var store = new InMemoryStore { Value = "TOKEN_SECURE" };
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.SecureOnly, outcome);
Assert.Equal("TOKEN_SECURE", store.Value);
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_SecurePresentLegacyPresentIdentique_SecurePrioritaireEtLegacySupprime()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, "TOKEN_1");
var store = new InMemoryStore { Value = "TOKEN_1" };
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.SecurePrioritaire, outcome);
Assert.Equal("TOKEN_1", store.Value);
Assert.False(File.Exists(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_SecurePresentLegacyPresentDifferent_SecurePrioritaireLegacyConserve()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, "TOKEN_ANCIEN");
var store = new InMemoryStore { Value = "TOKEN_SECURE" };
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
// Le sécurisé fait foi ; l'ancien n'est PAS supprimé (incohérent).
Assert.Equal(LegacyTokenMigrationOutcome.SecurePrioritaire, outcome);
Assert.Equal("TOKEN_SECURE", store.Value);
Assert.True(File.Exists(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_LegacyVide_TraiteCommeAbsentEtSupprime()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, " ");
var store = new InMemoryStore();
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.NoSession, outcome);
Assert.False(File.Exists(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_EcritureSecureEchoue_MigrationFailedLegacyConserve()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, "TOKEN_1");
var store = new InMemoryStore { ThrowOnSave = true };
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.MigrationFailed, outcome);
Assert.True(File.Exists(LegacyPath(dir)));
Assert.Equal("TOKEN_1", File.ReadAllText(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_VerificationEchoue_MigrationFailedLegacyConserve()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, "TOKEN_1");
// SaveAsync ne persiste rien : la relecture renvoie null ≠ TOKEN_1.
var store = new NonPersistingStore();
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
Assert.Equal(LegacyTokenMigrationOutcome.MigrationFailed, outcome);
Assert.True(File.Exists(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_Idempotent_DixAppelsNeChangentRien()
{
string dir = NewTempDir();
try
{
WriteLegacy(dir, "TOKEN_1");
var store = new InMemoryStore();
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var first = await migrator.MigrateAsync();
for (int i = 0; i < 9; i++)
{
await migrator.MigrateAsync();
}
Assert.Equal(LegacyTokenMigrationOutcome.Migrated, first);
Assert.Equal("TOKEN_1", store.Value);
Assert.False(File.Exists(LegacyPath(dir)));
// Après migration, un appel supplémentaire est stable (SecureOnly).
Assert.Equal(LegacyTokenMigrationOutcome.SecureOnly, await migrator.MigrateAsync());
}
finally { DeleteDir(dir); }
}
[Fact]
public async Task Migrate_CrashApresEcritureSecureAvantSuppressionLegacy_RecupereSansPerte()
{
string dir = NewTempDir();
try
{
// Simule le crash : le secure a été écrit mais le legacy subsiste.
WriteLegacy(dir, "TOKEN_1");
var store = new InMemoryStore { Value = "TOKEN_1" };
var migrator = new LegacyTokenMigrator(store, LegacyPath(dir));
var outcome = await migrator.MigrateAsync();
// SecurePrioritaire : secure lu, legacy identique supprimé, session intacte.
Assert.Equal(LegacyTokenMigrationOutcome.SecurePrioritaire, outcome);
Assert.Equal("TOKEN_1", store.Value);
Assert.False(File.Exists(LegacyPath(dir)));
}
finally { DeleteDir(dir); }
}
/// <summary>Store en mémoire qui persiste correctement.</summary>
private sealed class InMemoryStore : ISecureTokenStore
{
public string? Value { get; set; }
public bool ThrowOnSave { get; set; }
public Task SaveAsync(string token, CancellationToken cancellationToken = default)
{
if (ThrowOnSave)
{
throw new InvalidOperationException("save failed");
}
Value = token;
return Task.CompletedTask;
}
public Task<string?> GetAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(Value);
public Task ClearAsync(CancellationToken cancellationToken = default)
{
Value = null;
return Task.CompletedTask;
}
}
/// <summary>Store qui accepte l'écriture mais ne la persiste pas (simule une vérification échouée).</summary>
private sealed class NonPersistingStore : ISecureTokenStore
{
public Task SaveAsync(string token, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task<string?> GetAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<string?>(null);
public Task ClearAsync(CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
}