174 lines
6.4 KiB
C#
174 lines
6.4 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Local.Repositories;
|
|
using Omika.App.Local.Sqlite;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Test ciblé de la migration technique de la table <c>InterventionCache</c>.
|
|
///
|
|
/// La colonne de présentation <c>SiteAddressDisplay</c> a été ajoutée après que
|
|
/// la table existait déjà sur les terminaux. <c>CREATE TABLE IF NOT EXISTS</c>
|
|
/// 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).
|
|
/// </summary>
|
|
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<bool> 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;
|
|
}
|
|
}
|