269 lines
8.9 KiB
C#
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;
|
|
}
|
|
}
|