elsa-core/test/unit/Elsa.Persistence.EFCore.UnitTests/LabelNormalizedNameUniquenessTests.cs
Sipke Schoorstra 4b15b0166d
fix(labels): enforce per-tenant uniqueness on NormalizedName (#8112)
* fix(labels): enforce per-tenant uniqueness on NormalizedName

Finish the unused Label.NormalizedName contract the same way Secrets
does: unique (TenantId, NormalizedName) in EF, fail-closed Memory saves,
and keep NormalizedName in sync with Name.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* fix(labels): clone Memory label reads so a rejected rename cannot persist

Find/List handed out live store refs. Labels.Update mutates Name (and
NormalizedName) on that instance before Save; a uniqueness rejection
then left the stored row already renamed. Clone-on-read matches Memory
identity stores from #8108.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* fix(labels): stamp null TenantId, fail-loud on leftover duplicates, cap names at 255

Follow the Architect steer for Greptile P1s: do not auto-delete duplicate
labels; UPDATE Labels SET TenantId = '' WHERE TenantId IS NULL on every
provider before CreateIndex; keep SQL Server/Oracle filtered unique
indexes; HasMaxLength(255) on Name and NormalizedName with no silent
truncate.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* fix(labels): fail-loud preflight for leftover keys and over-length names

No silent dedupe: every provider lists leftover (TenantId, NormalizedName)
keys and aborts before CreateIndex. SQL Server/Oracle keep their filtered
unique indexes. PostgreSQL/SQLite keep provider column types. Providers
that narrow Name/NormalizedName to 255 preflight over-length Ids first.
Memory uniqueness semantics are unchanged.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* test(labels): match Oracle filtered-index escape in migration assertion

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-13 16:16:01 +02:00

41 lines
1.5 KiB
C#

using Elsa.Labels.Entities;
using Elsa.Persistence.EFCore.Modules.Labels;
using Microsoft.EntityFrameworkCore;
namespace Elsa.Persistence.EFCore.UnitTests;
/// <summary>
/// The Labels unique index must be per tenant, matching Secrets'
/// <c>IX_Secret_TenantId_NormalizedName</c>. A global unique index would make
/// the name a shared resource: the second tenant could not create "urgent".
/// </summary>
public class LabelNormalizedNameUniquenessTests
{
[Fact]
public void Label_HasUniqueIndex_OnTenantIdAndNormalizedName()
{
var builder = new ModelBuilder();
new Configurations().Configure(builder.Entity<Label>());
var index = builder.Model
.FindEntityType(typeof(Label))!
.GetIndexes()
.Single(x => x.IsUnique);
Assert.Equal(["TenantId", "NormalizedName"], index.Properties.Select(x => x.Name));
Assert.Equal("IX_Label_TenantId_NormalizedName", index.GetDatabaseName());
}
[Fact]
public void Label_NameAndNormalizedName_HaveSharedMaxLength()
{
var builder = new ModelBuilder();
new Configurations().Configure(builder.Entity<Label>());
var entity = builder.Model.FindEntityType(typeof(Label))!;
Assert.Equal(Label.NameMaxLength, entity.FindProperty(nameof(Label.Name))!.GetMaxLength());
Assert.Equal(Label.NameMaxLength, entity.FindProperty(nameof(Label.NormalizedName))!.GetMaxLength());
Assert.Equal(255, Label.NameMaxLength);
}
}