|
| 1 | +/* |
| 2 | +The MIT License (MIT) |
| 3 | +
|
| 4 | +Copyright (c) 2007 - 2026 Microting A/S |
| 5 | +
|
| 6 | +Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | +of this software and associated documentation files (the "Software"), to deal |
| 8 | +in the Software without restriction. |
| 9 | +*/ |
| 10 | + |
| 11 | +namespace BackendConfiguration.Pn.Integration.Test; |
| 12 | + |
| 13 | +using BackendConfiguration.Pn.Infrastructure.Helpers; |
| 14 | +using Microsoft.EntityFrameworkCore; |
| 15 | +using Microsoft.Extensions.Logging.Abstractions; |
| 16 | +using Microting.eForm.Infrastructure.Data.Entities; |
| 17 | +using Microting.eForm.Infrastructure.Models; |
| 18 | + |
| 19 | +/// <summary> |
| 20 | +/// Part A regression coverage for the calendar translation LanguageId bug. The |
| 21 | +/// create/edit modal hardcoded the Danish source title's LanguageId to the frontend |
| 22 | +/// app-locale id 1, while target languages already carry the real SDK Languages.Id |
| 23 | +/// from GET /settings/languages. The SDK Languages table is customer-specific |
| 24 | +/// (Danish is NOT guaranteed to be id 1, and the ids are NOT guaranteed to be the |
| 25 | +/// contiguous set {1,2,3}), so a verbatim persist stores the Danish title under a |
| 26 | +/// language no reader queries, producing a blank event title. |
| 27 | +/// |
| 28 | +/// <see cref="AreaRuleLanguageHelper.RemapCommonTranslationLanguageIdsAsync"/> must |
| 29 | +/// be EXISTENCE-based: only an id that is ABSENT from the SDK Languages table is |
| 30 | +/// remapped (via the static app-locale map) to the SDK Languages.Id that carries the |
| 31 | +/// intended LanguageCode. Any id that EXISTS in SDK Languages is a real target id and |
| 32 | +/// must be left untouched — even when it is 2, 3 or 4 — and unresolvable translates |
| 33 | +/// are never dropped. |
| 34 | +/// |
| 35 | +/// These tests reproduce a SHIFTED-ID tenant (da=2, en-US=3, de-DE=4, NO id 1) to |
| 36 | +/// prove the safety property: a valid target id such as en-US=3 must NOT be reinterpreted |
| 37 | +/// via the static map (which historically mis-keyed it to de-DE), which was the corruption |
| 38 | +/// bug this change fixes. |
| 39 | +/// </summary> |
| 40 | +[Parallelizable(ParallelScope.Fixtures)] |
| 41 | +[TestFixture] |
| 42 | +public class CalendarTranslationLanguageIdRemapTests : TestBaseSetup |
| 43 | +{ |
| 44 | + /// <summary> |
| 45 | + /// Reproduces a shifted-id tenant: removes the default-seeded da/en-US/de-DE rows, |
| 46 | + /// consumes id 1 with a filler so it is ABSENT for the language codes, then re-inserts |
| 47 | + /// da, en-US and de-DE so they land on ascending ids > 1 (typically 2, 3, 4). Returns |
| 48 | + /// the resolved (daId, enUsId, deDeId). Guarantees no SDK Languages row has id 1. |
| 49 | + /// </summary> |
| 50 | + private async Task<(int DaId, int EnUsId, int DeDeId)> ForceShiftedIdTenant() |
| 51 | + { |
| 52 | + var codes = new[] { "da", "en-US", "de-DE" }; |
| 53 | + var existing = await MicrotingDbContext!.Languages |
| 54 | + .Where(x => codes.Contains(x.LanguageCode)) |
| 55 | + .ToListAsync(); |
| 56 | + if (existing.Count > 0) |
| 57 | + { |
| 58 | + MicrotingDbContext.Languages.RemoveRange(existing); |
| 59 | + await MicrotingDbContext.SaveChangesAsync(); |
| 60 | + } |
| 61 | + |
| 62 | + // Consume id 1 with a filler so that no language code resolves to id 1. |
| 63 | + await MicrotingDbContext.Languages.AddAsync( |
| 64 | + new Language { Name = "Filler", LanguageCode = "zz-filler", IsActive = false }); |
| 65 | + await MicrotingDbContext.SaveChangesAsync(); |
| 66 | + |
| 67 | + var da = new Language { Name = "Dansk", LanguageCode = "da", IsActive = true }; |
| 68 | + await MicrotingDbContext.Languages.AddAsync(da); |
| 69 | + await MicrotingDbContext.SaveChangesAsync(); |
| 70 | + |
| 71 | + var enUs = new Language { Name = "English", LanguageCode = "en-US", IsActive = true }; |
| 72 | + await MicrotingDbContext.Languages.AddAsync(enUs); |
| 73 | + await MicrotingDbContext.SaveChangesAsync(); |
| 74 | + |
| 75 | + var deDe = new Language { Name = "Deutsch", LanguageCode = "de-DE", IsActive = true }; |
| 76 | + await MicrotingDbContext.Languages.AddAsync(deDe); |
| 77 | + await MicrotingDbContext.SaveChangesAsync(); |
| 78 | + |
| 79 | + // The existence-based remap keys purely off whether the incoming id is present in |
| 80 | + // SDK Languages, so id 1 must be absent from the WHOLE table for app-locale id 1 to |
| 81 | + // be treated as the (absent) hardcoded source. The default seed put da on id 1, which |
| 82 | + // we deleted; auto-increment never reuses it, so id 1 is now free. |
| 83 | + var hasIdOne = await MicrotingDbContext.Languages.AnyAsync(x => x.Id == 1); |
| 84 | + Assert.That(hasIdOne, Is.False, |
| 85 | + "Test precondition: no SDK Languages row may sit on id 1 (shifted-id tenant)"); |
| 86 | + |
| 87 | + return (da.Id, enUs.Id, deDe.Id); |
| 88 | + } |
| 89 | + |
| 90 | + [Test] |
| 91 | + public async Task Remap_AbsentAppLocaleId1_IsRemappedToRealSdkDanishId() |
| 92 | + { |
| 93 | + var (daId, _, _) = await ForceShiftedIdTenant(); |
| 94 | + Assert.That(daId, Is.Not.EqualTo(1), |
| 95 | + "Test precondition: SDK Danish id must differ from app-locale id 1 to reproduce the bug"); |
| 96 | + |
| 97 | + var translates = new List<CommonTranslationsModel> |
| 98 | + { |
| 99 | + // Danish source: the frontend formerly hardcoded app-locale id 1, which is |
| 100 | + // ABSENT from this tenant's SDK Languages and must be remapped to da's real id. |
| 101 | + new() { LanguageId = 1, Name = "Tank 4 inspektion", Description = "Kontrollér ventiler." }, |
| 102 | + }; |
| 103 | + |
| 104 | + await AreaRuleLanguageHelper.RemapCommonTranslationLanguageIdsAsync( |
| 105 | + translates, MicrotingDbContext!, NullLogger.Instance); |
| 106 | + |
| 107 | + Assert.That(translates[0].LanguageId, Is.EqualTo(daId), |
| 108 | + "Absent app-locale id 1 must be remapped to the real SDK Danish Languages.Id"); |
| 109 | + Assert.That(translates[0].Name, Is.EqualTo("Tank 4 inspektion"), |
| 110 | + "Remap must not mutate the translation text"); |
| 111 | + } |
| 112 | + |
| 113 | + [Test] |
| 114 | + public async Task Remap_ValidEnUsSdkId_IsLeftUnchanged_NoCorruption() |
| 115 | + { |
| 116 | + // Regression guard for the corruption bug: a valid en-US SDK id collides with |
| 117 | + // the static app-locale map's en-US slot (2) only by accident; the OLD value-based |
| 118 | + // guard remapped any present id 1/2/3 whose SDK code mismatched the static code, |
| 119 | + // corrupting good targets. The existence-based guard must leave any present id alone. |
| 120 | + var (_, enUsId, deDeId) = await ForceShiftedIdTenant(); |
| 121 | + |
| 122 | + var translates = new List<CommonTranslationsModel> |
| 123 | + { |
| 124 | + new() { LanguageId = enUsId, Name = "English title" }, |
| 125 | + }; |
| 126 | + |
| 127 | + await AreaRuleLanguageHelper.RemapCommonTranslationLanguageIdsAsync( |
| 128 | + translates, MicrotingDbContext!, NullLogger.Instance); |
| 129 | + |
| 130 | + Assert.That(translates[0].LanguageId, Is.EqualTo(enUsId), |
| 131 | + "A valid SDK en-US id must be left unchanged — never reinterpreted via the static map"); |
| 132 | + Assert.That(translates[0].LanguageId, Is.Not.EqualTo(deDeId), |
| 133 | + "The corruption bug remapped a valid en-US id onto de-DE; this must not happen"); |
| 134 | + } |
| 135 | + |
| 136 | + [Test] |
| 137 | + public async Task Remap_ValidDeDeSdkId_IsLeftUnchanged() |
| 138 | + { |
| 139 | + var (_, _, deDeId) = await ForceShiftedIdTenant(); |
| 140 | + |
| 141 | + var translates = new List<CommonTranslationsModel> |
| 142 | + { |
| 143 | + new() { LanguageId = deDeId, Name = "Deutscher Titel" }, |
| 144 | + }; |
| 145 | + |
| 146 | + await AreaRuleLanguageHelper.RemapCommonTranslationLanguageIdsAsync( |
| 147 | + translates, MicrotingDbContext!, NullLogger.Instance); |
| 148 | + |
| 149 | + Assert.That(translates[0].LanguageId, Is.EqualTo(deDeId), |
| 150 | + "A valid SDK de-DE id must be left unchanged"); |
| 151 | + } |
| 152 | + |
| 153 | + [Test] |
| 154 | + public async Task Remap_MixedSourceAndTargets_OnShiftedTenant_OnlyAbsentIdRemapped() |
| 155 | + { |
| 156 | + var (daId, enUsId, deDeId) = await ForceShiftedIdTenant(); |
| 157 | + |
| 158 | + var translates = new List<CommonTranslationsModel> |
| 159 | + { |
| 160 | + // Danish source still arrives as the absent app-locale id 1. |
| 161 | + new() { LanguageId = 1, Name = "Dansk titel" }, |
| 162 | + // Targets already carry their true SDK ids (from getLanguages()). |
| 163 | + new() { LanguageId = enUsId, Name = "English title" }, |
| 164 | + new() { LanguageId = deDeId, Name = "Deutscher Titel" }, |
| 165 | + }; |
| 166 | + |
| 167 | + await AreaRuleLanguageHelper.RemapCommonTranslationLanguageIdsAsync( |
| 168 | + translates, MicrotingDbContext!, NullLogger.Instance); |
| 169 | + |
| 170 | + Assert.Multiple(() => |
| 171 | + { |
| 172 | + Assert.That(translates[0].LanguageId, Is.EqualTo(daId), |
| 173 | + "Absent Danish app-locale id 1 must be remapped to the SDK Danish id"); |
| 174 | + Assert.That(translates[1].LanguageId, Is.EqualTo(enUsId), |
| 175 | + "A translate that already carries a valid SDK en-US id must be left untouched"); |
| 176 | + Assert.That(translates[2].LanguageId, Is.EqualTo(deDeId), |
| 177 | + "A translate that already carries a valid SDK de-DE id must be left untouched"); |
| 178 | + }); |
| 179 | + } |
| 180 | + |
| 181 | + [Test] |
| 182 | + public async Task Remap_AbsentIdNotInStaticMap_IsLeftUnchanged_AndTranslationNotDropped() |
| 183 | + { |
| 184 | + await ForceShiftedIdTenant(); |
| 185 | + |
| 186 | + // 9999 is neither a known app-locale id nor a valid SDK id. |
| 187 | + var translates = new List<CommonTranslationsModel> |
| 188 | + { |
| 189 | + new() { LanguageId = 9999, Name = "Untouched" }, |
| 190 | + }; |
| 191 | + |
| 192 | + await AreaRuleLanguageHelper.RemapCommonTranslationLanguageIdsAsync( |
| 193 | + translates, MicrotingDbContext!, NullLogger.Instance); |
| 194 | + |
| 195 | + Assert.Multiple(() => |
| 196 | + { |
| 197 | + Assert.That(translates, Has.Count.EqualTo(1), |
| 198 | + "An unresolvable translate must never be dropped"); |
| 199 | + Assert.That(translates[0].LanguageId, Is.EqualTo(9999), |
| 200 | + "An absent id with no static-map entry must be left unchanged"); |
| 201 | + }); |
| 202 | + } |
| 203 | +} |
0 commit comments