File: Model\ResourceTagStoreTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Tests.Shared.DashboardModel;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Model;
 
public sealed class ResourceTagStoreTests(ITestOutputHelper testOutputHelper) : IDisposable
{
    private readonly TemporaryWorkspace _workspace = TemporaryWorkspace.Create(testOutputHelper);
 
    [Fact]
    public void AppHostTagsAreMergedWithoutPersistingDeclaredTags()
    {
        var store = CreateStore();
        store.AddTag("api", "backend");
        store.AddTag("api", "Personal");
        var name = KnownProperties.Resource.TraitPrefix + KnownResourceTraits.DashboardTag;
        var resource = ModelTestHelpers.CreateResource("api", properties: new()
        {
            [name] = new(name, Value.ForList(Value.ForString("Backend"), Value.ForString("API")), false, null, 0, null, false)
        });
 
        Assert.Equal(["Backend", "API", "Personal"], store.GetTagsByResource([resource])["api"]);
        Assert.Equal(["API", "Backend", "Personal"], store.GetAllTags([resource]));
        Assert.Equal(["backend", "Personal"], store.GetTags("api"));
        Assert.Equal(["backend", "Personal"], store.GetTagsByResource([])["api"]);
    }
 
    [Fact]
    public void AddTag_AddsTagsInOrderAndRaisesChanged()
    {
        var store = CreateStore();
        var changedCount = 0;
        store.Changed += () => changedCount++;
 
        Assert.True(store.AddTag("api", "backend"));
        Assert.True(store.AddTag("api", "core"));
        Assert.True(store.AddTag("worker", "backend"));
 
        Assert.Equal(["backend", "core"], store.GetTags("api"));
        Assert.Equal(["backend"], store.GetTags("worker"));
        Assert.Equal(["backend", "core"], store.GetAllTags());
        Assert.Equal(3, changedCount);
    }
 
    [Fact]
    public void AddTag_ReusesCasingOfExistingTag()
    {
        var store = CreateStore();
 
        Assert.True(store.AddTag("api", "Backend"));
        Assert.True(store.AddTag("worker", "  backend "));
        Assert.False(store.AddTag("api", "BACKEND"));
 
        Assert.Equal(["Backend"], store.GetTags("api"));
        Assert.Equal(["Backend"], store.GetTags("worker"));
        Assert.Equal(["Backend"], store.GetAllTags());
    }
 
    [Fact]
    public void AddTag_EmptyTagIsIgnored()
    {
        var store = CreateStore();
        var changedCount = 0;
        store.Changed += () => changedCount++;
 
        Assert.False(store.AddTag("api", "   "));
 
        Assert.Equal([], store.GetTags("api"));
        Assert.Equal(0, changedCount);
    }
 
    [Fact]
    public void RemoveTag_RemovesTagIgnoringCase()
    {
        var store = CreateStore();
        store.AddTag("api", "backend");
        store.AddTag("api", "core");
 
        Assert.True(store.RemoveTag("api", "BACKEND"));
        Assert.False(store.RemoveTag("api", "backend"));
        Assert.False(store.RemoveTag("worker", "core"));
 
        Assert.Equal(["core"], store.GetTags("api"));
        Assert.Equal(["core"], store.GetAllTags());
    }
 
    [Fact]
    public void Tags_PersistAcrossStoreInstances()
    {
        var databasePath = Path.Combine(_workspace.Path, DashboardSettingsDatabase.DatabaseFileName);
 
        var store = new ResourceTagStore(databasePath, NullLogger.Instance);
        store.AddTag("api", "backend");
        store.AddTag("api", "core");
        store.AddTag("worker", "backend");
        store.RemoveTag("api", "core");
 
        var reloadedStore = new ResourceTagStore(databasePath, NullLogger.Instance);
 
        var tagsByResource = reloadedStore.GetTagsByResource();
        Assert.Equal(["api", "worker"], tagsByResource.Keys.Order(StringComparer.Ordinal));
        Assert.Equal(["backend"], tagsByResource["api"]);
        Assert.Equal(["backend"], tagsByResource["worker"]);
    }
 
    [Fact]
    public void Tags_WithoutDatabaseAreKeptInMemory()
    {
        var store = new ResourceTagStore(databasePath: null, NullLogger.Instance);
 
        Assert.True(store.AddTag("api", "backend"));
 
        Assert.Null(store.DatabasePath);
        Assert.Equal(["backend"], store.GetTags("api"));
    }
 
    [Fact]
    public void GetDatabasePath_NoPersistenceReturnsNull()
    {
        var options = new DashboardOptions();
        options.Data.PersistenceMode = DashboardPersistenceMode.None;
 
        Assert.Null(DashboardSettingsDatabase.GetDatabasePath(options));
    }
 
    [Theory]
    [InlineData(null, null)]
    [InlineData("   ", null)]
    [InlineData("backend", "backend")]
    [InlineData("  my   tag\t ", "my tag")]
    public void NormalizeTag_TrimsAndCollapsesWhitespace(string? tag, string? expected)
    {
        Assert.Equal(expected, ResourceTagStore.NormalizeTag(tag));
    }
 
    [Fact]
    public void NormalizeTag_TruncatesLongTags()
    {
        var normalized = ResourceTagStore.NormalizeTag(new string('a', ResourceTagStore.MaxTagLength + 10));
 
        Assert.Equal(new string('a', ResourceTagStore.MaxTagLength), normalized);
    }
 
    private ResourceTagStore CreateStore() =>
        new(Path.Combine(_workspace.Path, DashboardSettingsDatabase.DatabaseFileName), NullLogger.Instance);
 
    public void Dispose()
    {
        _workspace.Dispose();
    }
}