using Microsoft.Data.Sqlite;
using Omika.App.Local.Models;
using Omika.App.Local.Repositories;
using Omika.App.Local.Sqlite;
namespace Omika.Tests.Mobile.Local;
///
/// Test ciblé de la migration technique de la table InterventionCache.
///
/// La colonne de présentation SiteAddressDisplay a été ajoutée après que
/// la table existait déjà sur les terminaux. CREATE TABLE IF NOT EXISTS
/// n'ajoute pas de colonne à une table existante : ce test simule une base
/// « ancien schéma » (sans la colonne) et vérifie que l'initialisation ajoute
/// la colonne sans détruire les lignes (favoris + dernière recherche).
///
public class InterventionCacheMigrationTests : IDisposable
{
private readonly LocalDatabase _database;
private readonly InterventionCacheRepository _repository;
public InterventionCacheMigrationTests()
{
_database = new LocalDatabase(new LocalDatabaseOptions());
// IMPORTANT : on ne lance PAS InitializeSchemaAsync immédiatement.
// On crée d'abord l'ANCIEN schéma (sans SiteAddressDisplay) pour
// reproduire une base déjà déployée avant l'ajout de la colonne.
_repository = new InterventionCacheRepository(_database);
}
public void Dispose()
{
_database.Dispose();
GC.SuppressFinalize(this);
}
[Fact]
public async Task Migration_AjouteColonne_PreserveLignes_EtFlags()
{
// 1. Ancien schéma : InterventionCache SANS SiteAddressDisplay.
await _database.ExecuteNonQueryAsync("""
CREATE TABLE "InterventionCache" (
"Id" TEXT NOT NULL PRIMARY KEY,
"PayloadJson" TEXT NOT NULL,
"IsPinned" INTEGER NOT NULL DEFAULT 0,
"IsInLastSearch" INTEGER NOT NULL DEFAULT 0,
"SearchOrder" INTEGER NULL,
"NbRendezVous" INTEGER NOT NULL DEFAULT 0,
"SiteNom" TEXT NULL,
"ClientNom" TEXT NULL,
"ContactNom" TEXT NULL,
"UpdatedAtUtc" TEXT NOT NULL
);
""");
// 2. Une ligne existante : favorite + dernière recherche.
Guid id = Guid.NewGuid();
await _database.ExecuteNonQueryAsync("""
INSERT INTO "InterventionCache"
("Id", "PayloadJson", "IsPinned", "IsInLastSearch", "SearchOrder", "NbRendezVous", "SiteNom", "ClientNom", "ContactNom", "UpdatedAtUtc")
VALUES
(@Id, @PayloadJson, 1, 1, 0, 0, 'Site', 'Client', 'Contact', @UpdatedAtUtc)
""",
[
Param("@Id", id.ToString()),
Param("@PayloadJson", "{\"Id\":\"" + id + "\",\"Titre\":\"Intervention A\"}"),
Param("@UpdatedAtUtc", DateTimeOffset.UtcNow.ToString("O"))
]);
// 3. Initialisation/migration complète.
await _database.InitializeSchemaAsync();
// 4. La colonne canonique est présente.
Assert.True(await HasColumnAsync("InterventionCache", "SiteAddressDisplay"),
"SiteAddressDisplay doit être ajoutée par la migration.");
// 5. La ligne est toujours présente et ses flags sont préservés.
var pinned = await _repository.GetPinnedAsync();
Assert.Single(pinned);
Assert.Equal(id, pinned[0].Id);
Assert.True(pinned[0].IsPinned);
Assert.True(pinned[0].IsInLastSearch);
var lastSearch = await _repository.GetLastSearchAsync();
Assert.Single(lastSearch);
Assert.Equal(id, lastSearch[0].Id);
}
[Fact]
public async Task Migration_EstIdempotente()
{
await _database.InitializeSchemaAsync();
await _database.InitializeSchemaAsync();
Assert.True(await HasColumnAsync("InterventionCache", "SiteAddressDisplay"));
}
[Fact]
public async Task Typo_SitesAddressDisplay_EstAbsent()
{
await CreateOldSchemaAsync();
await _database.InitializeSchemaAsync();
Assert.False(await HasColumnAsync("InterventionCache", "SitesAddressDisplay"),
"Le typo au pluriel SitesAddressDisplay ne doit jamais réapparaître.");
Assert.True(await HasColumnAsync("InterventionCache", "SiteAddressDisplay"));
}
[Fact]
public async Task Lecture_ColonneNull_NeProvoquePasException()
{
await _database.InitializeSchemaAsync();
// Upsert sans adresse (NULL) puis relecture : aucune exception.
var entity = new CachedIntervention
{
Id = Guid.NewGuid(),
PayloadJson = "{\"Id\":\"" + Guid.NewGuid() + "\",\"Titre\":\"Sans adresse\"}",
IsPinned = true,
IsInLastSearch = true,
SiteAddressDisplay = null
};
await _repository.UpsertAsync(entity);
var read = await _repository.GetByIdAsync(entity.Id);
Assert.NotNull(read);
Assert.Null(read!.SiteAddressDisplay);
Assert.True(read.IsPinned);
}
private async Task CreateOldSchemaAsync()
{
await _database.ExecuteNonQueryAsync("""
CREATE TABLE "InterventionCache" (
"Id" TEXT NOT NULL PRIMARY KEY,
"PayloadJson" TEXT NOT NULL,
"IsPinned" INTEGER NOT NULL DEFAULT 0,
"IsInLastSearch" INTEGER NOT NULL DEFAULT 0,
"SearchOrder" INTEGER NULL,
"NbRendezVous" INTEGER NOT NULL DEFAULT 0,
"SiteNom" TEXT NULL,
"ClientNom" TEXT NULL,
"ContactNom" TEXT NULL,
"UpdatedAtUtc" TEXT NOT NULL
);
""");
}
private async Task HasColumnAsync(string table, string column)
{
await using var command = _database.CreateCommand();
command.CommandText = $"PRAGMA table_info(\"{table}\")";
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
string name = reader.GetString(reader.GetOrdinal("name"));
if (string.Equals(name, column, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static SqliteParameter Param(string name, object? value)
{
var parameter = new SqliteParameter { ParameterName = name };
parameter.Value = value ?? DBNull.Value;
return parameter;
}
}