Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user