Procházet zdrojové kódy

Ups complete with tests, usecases and commands

James před 2 měsíci
rodič
revize
c22196a69d

+ 0 - 1
RackPeek.Domain/RackPeek.Domain.csproj

@@ -8,7 +8,6 @@
 
     <ItemGroup>
       <Folder Include="Resources\Hardware\Desktop\" />
-      <Folder Include="Resources\Hardware\Ups\" />
     </ItemGroup>
 
 </Project>

+ 20 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/AddUpsUseCase.cs

@@ -0,0 +1,20 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class AddUpsUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        var existing = await repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"UPS '{name}' already exists.");
+
+        var ups = new Ups
+        {
+            Name = name
+        };
+
+        await repository.AddAsync(ups);
+    }
+}

+ 14 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/DeleteUpsUseCase.cs

@@ -0,0 +1,14 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class DeleteUpsUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        if (await repository.GetByNameAsync(name) is not Ups ups)
+            throw new InvalidOperationException($"UPS '{name}' not found.");
+
+        await repository.DeleteAsync(name);
+    }
+}

+ 25 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/DescribeUpsUseCase.cs

@@ -0,0 +1,25 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public record UpsDescription(
+    string Name,
+    string? Model,
+    int? Va
+);
+
+public class DescribeUpsUseCase(IHardwareRepository repository)
+{
+    public async Task<UpsDescription?> ExecuteAsync(string name)
+    {
+        var ups = await repository.GetByNameAsync(name) as Ups;
+        if (ups == null)
+            return null;
+
+        return new UpsDescription(
+            ups.Name,
+            ups.Model,
+            ups.Va
+        );
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/GetUpsUnitUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class GetUpsUnitUseCase(IHardwareRepository repository)
+{
+    public async Task<Ups?> ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        return hardware as Ups;
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/GetUpsUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class GetUpsUseCase(IHardwareRepository repository)
+{
+    public async Task<IReadOnlyList<Ups>> ExecuteAsync()
+    {
+        var hardware = await repository.GetAllAsync();
+        return hardware.OfType<Ups>().ToList();
+    }
+}

+ 25 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/UpdateUpsUseCase.cs

@@ -0,0 +1,25 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class UpdateUpsUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? model = null,
+        int? va = null
+    )
+    {
+        var ups = await repository.GetByNameAsync(name) as Ups;
+        if (ups == null)
+            throw new InvalidOperationException($"UPS '{name}' not found.");
+
+        if (!string.IsNullOrWhiteSpace(model))
+            ups.Model = model;
+
+        if (va.HasValue)
+            ups.Va = va.Value;
+
+        await repository.UpdateAsync(ups);
+    }
+}

+ 29 - 0
RackPeek/Commands/Ups/UpsAddCommand.cs

@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] public string Name { get; set; } = default!;
+}
+
+public class UpsAddCommand(IServiceProvider provider)
+    : AsyncCommand<UpsAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddUpsUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]UPS '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 29 - 0
RackPeek/Commands/Ups/UpsDeleteCommand.cs

@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] public string Name { get; set; } = default!;
+}
+
+public class UpsDeleteCommand(IServiceProvider provider)
+    : AsyncCommand<UpsNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteUpsUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]UPS '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}

+ 39 - 0
RackPeek/Commands/Ups/UpsDescribeCommand.cs

@@ -0,0 +1,39 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsDescribeCommand(IServiceProvider provider)
+    : AsyncCommand<UpsNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeUpsUseCase>();
+
+        var ups = await useCase.ExecuteAsync(settings.Name);
+
+        if (ups == null)
+        {
+            AnsiConsole.MarkupLine($"[red]UPS '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid()
+            .AddColumn()
+            .AddColumn();
+
+        grid.AddRow("Name:", ups.Name);
+        grid.AddRow("Model:", ups.Model ?? "Unknown");
+        grid.AddRow("VA:", ups.Va?.ToString() ?? "Unknown");
+
+        AnsiConsole.Write(new Panel(grid).Header("UPS").Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Ups/UpsGetByNameCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsGetByNameCommand(IServiceProvider provider)
+    : AsyncCommand<UpsNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeUpsUseCase>();
+
+        var ups = await useCase.ExecuteAsync(settings.Name);
+
+        if (ups == null)
+        {
+            AnsiConsole.MarkupLine($"[red]UPS '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine(
+            $"[green]{ups.Name}[/]  Model: {ups.Model ?? "Unknown"}, VA: {ups.Va?.ToString() ?? "Unknown"}");
+
+        return 0;
+    }
+}

+ 42 - 0
RackPeek/Commands/Ups/UpsGetCommand.cs

@@ -0,0 +1,42 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Reports;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsGetCommand(IServiceProvider provider)
+    : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpsHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.UpsUnits.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No UPS units found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("VA");
+
+        foreach (var ups in report.UpsUnits)
+            table.AddRow(
+                ups.Name,
+                ups.Model,
+                ups.Va.ToString()
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 31 - 0
RackPeek/Commands/Ups/UpsSetCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Server;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Ups;
+
+public class UpsSetSettings : ServerNameSettings
+{
+    [CommandOption("--model")] public string? Model { get; set; }
+    [CommandOption("--va")] public int? Va { get; set; }
+}
+
+public class UpsSetCommand(IServiceProvider provider)
+    : AsyncCommand<UpsSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        UpsSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateUpsUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name, settings.Model, settings.Va);
+
+        AnsiConsole.MarkupLine($"[green]UPS '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 43 - 1
RackPeek/Program.cs

@@ -10,6 +10,7 @@ using RackPeek.Commands.Server.Gpu;
 using RackPeek.Commands.Server.Nics;
 using RackPeek.Commands.Switches;
 using RackPeek.Commands.Systems;
+using RackPeek.Commands.Ups;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.AccessPoints;
 using RackPeek.Domain.Resources.Hardware.Reports;
@@ -19,6 +20,7 @@ using RackPeek.Domain.Resources.Hardware.Server.Drive;
 using RackPeek.Domain.Resources.Hardware.Server.Gpu;
 using RackPeek.Domain.Resources.Hardware.Server.Nic;
 using RackPeek.Domain.Resources.Hardware.Switches;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
 using RackPeek.Domain.Resources.SystemResources;
 using RackPeek.Domain.Resources.SystemResources.UseCases;
 using RackPeek.Spectre;
@@ -202,9 +204,23 @@ public static class CliBootstrap
         services.AddScoped<AccessPointGetCommand>();
         services.AddScoped<AccessPointSetCommand>();
 
+        // UPS use cases
+        services.AddScoped<AddUpsUseCase>();
+        services.AddScoped<DeleteUpsUseCase>();
+        services.AddScoped<GetUpsUnitUseCase>();
+        services.AddScoped<GetUpsUseCase>();
+        services.AddScoped<UpdateUpsUseCase>();
+        services.AddScoped<DescribeUpsUseCase>();
+
+        // UPS commands
+        services.AddScoped<UpsAddCommand>();
+        services.AddScoped<UpsDeleteCommand>();
+        services.AddScoped<UpsDescribeCommand>();
+        services.AddScoped<UpsGetByNameCommand>();
+        services.AddScoped<UpsGetCommand>();
+        services.AddScoped<UpsSetCommand>();
 
         
-        
 
         
         
@@ -373,6 +389,32 @@ public static class CliBootstrap
                     ap.AddCommand<AccessPointDeleteCommand>("del")
                         .WithDescription("Delete an access point");
                 });
+                
+                config.AddBranch("ups", ups =>
+                {
+                    ups.SetDescription("Manage UPS units");
+
+                    ups.AddCommand<UpsReportCommand>("summary")
+                        .WithDescription("Show UPS hardware report");
+
+                    ups.AddCommand<UpsAddCommand>("add")
+                        .WithDescription("Add a new UPS");
+
+                    ups.AddCommand<UpsGetCommand>("list")
+                        .WithDescription("List UPS units");
+
+                    ups.AddCommand<UpsGetByNameCommand>("get")
+                        .WithDescription("Get a UPS by name");
+
+                    ups.AddCommand<UpsDescribeCommand>("describe")
+                        .WithDescription("Show detailed information about a UPS");
+
+                    ups.AddCommand<UpsSetCommand>("set")
+                        .WithDescription("Update UPS properties");
+
+                    ups.AddCommand<UpsDeleteCommand>("del")
+                        .WithDescription("Delete a UPS");
+                });
 
                 
                 // ----------------------------

+ 0 - 1
RackPeek/RackPeek.csproj

@@ -25,7 +25,6 @@
 
     <ItemGroup>
       <Folder Include="Commands\Desktop\" />
-      <Folder Include="Commands\Ups\" />
     </ItemGroup>
 
 </Project>

+ 40 - 0
Tests/Hardware/Ups/AddUpsUseCaseTests.cs

@@ -0,0 +1,40 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class AddUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Adds_new_ups_when_not_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new AddUpsUseCase(repo);
+
+        await sut.ExecuteAsync("ups01");
+
+        await repo.Received(1).AddAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Ups>(u =>
+            u.Name == "ups01"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_ups_already_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" });
+
+        var sut = new AddUpsUseCase(repo);
+
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ups01")
+        );
+
+        Assert.Equal("UPS 'ups01' already exists.", ex.Message);
+        await repo.DidNotReceive().AddAsync(Arg.Any<RackPeek.Domain.Resources.Hardware.Models.Ups>());
+    }
+}

+ 38 - 0
Tests/Hardware/Ups/DeleteUpsUseCaseTests.cs

@@ -0,0 +1,38 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class DeleteUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Deletes_ups_when_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" });
+
+        var sut = new DeleteUpsUseCase(repo);
+
+        await sut.ExecuteAsync("ups01");
+
+        await repo.Received(1).DeleteAsync("ups01");
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_ups_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new DeleteUpsUseCase(repo);
+
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ups01")
+        );
+
+        Assert.Equal("UPS 'ups01' not found.", ex.Message);
+        await repo.DidNotReceive().DeleteAsync(Arg.Any<string>());
+    }
+}

+ 43 - 0
Tests/Hardware/Ups/DescribeUpsUseCaseTests.cs

@@ -0,0 +1,43 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class DescribeUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_null_when_ups_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new DescribeUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync("ups01");
+
+        Assert.Null(result);
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_description_when_ups_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(new RackPeek.Domain.Resources.Hardware.Models.Ups
+        {
+            Name = "ups01",
+            Model = "APC-1500",
+            Va = 1500
+        });
+
+        var sut = new DescribeUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync("ups01");
+
+        Assert.NotNull(result);
+        Assert.Equal("ups01", result.Name);
+        Assert.Equal("APC-1500", result.Model);
+        Assert.Equal(1500, result.Va);
+    }
+}

+ 37 - 0
Tests/Hardware/Ups/GetUpsUnitUseCaseTests.cs

@@ -0,0 +1,37 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class GetUpsUnitUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_ups_when_it_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var ups = new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" };
+        repo.GetByNameAsync("ups01").Returns(ups);
+
+        var sut = new GetUpsUnitUseCase(repo);
+
+        var result = await sut.ExecuteAsync("ups01");
+
+        Assert.NotNull(result);
+        Assert.Same(ups, result);
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_null_when_hardware_is_not_ups()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("node01").Returns(new Server { Name = "node01" });
+
+        var sut = new GetUpsUnitUseCase(repo);
+
+        var result = await sut.ExecuteAsync("node01");
+
+        Assert.Null(result);
+    }
+}

+ 44 - 0
Tests/Hardware/Ups/GetUpsUseCaseTests.cs

@@ -0,0 +1,44 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class GetUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Returns_only_ups_units()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns([
+            new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups01" },
+            new Server { Name = "node01" },
+            new RackPeek.Domain.Resources.Hardware.Models.Ups { Name = "ups02" }
+        ]);
+
+        var sut = new GetUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync();
+
+        Assert.Equal(2, result.Count);
+        Assert.All(result, u => Assert.IsType<RackPeek.Domain.Resources.Hardware.Models.Ups>(u));
+        Assert.Contains(result, u => u.Name == "ups01");
+        Assert.Contains(result, u => u.Name == "ups02");
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Returns_empty_list_when_no_ups_exist()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns([
+            new Server { Name = "node01" }
+        ]);
+
+        var sut = new GetUpsUseCase(repo);
+
+        var result = await sut.ExecuteAsync();
+
+        Assert.Empty(result);
+    }
+}

+ 78 - 0
Tests/Hardware/Ups/UpdateUpsUseCaseTests.cs

@@ -0,0 +1,78 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+namespace Tests.Hardware.Ups;
+
+public class UpdateUpsUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Throws_when_ups_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new UpdateUpsUseCase(repo);
+
+        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
+            await sut.ExecuteAsync("ups01")
+        );
+
+        Assert.Equal("UPS 'ups01' not found.", ex.Message);
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<RackPeek.Domain.Resources.Hardware.Models.Ups>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Updates_only_provided_fields()
+    {
+        var existing = new RackPeek.Domain.Resources.Hardware.Models.Ups
+        {
+            Name = "ups01",
+            Model = "OldModel",
+            Va = 1000
+        };
+
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(existing);
+
+        var sut = new UpdateUpsUseCase(repo);
+
+        await sut.ExecuteAsync(
+            "ups01",
+            model: "NewModel",
+            va: 1500
+        );
+
+        await repo.Received(1).UpdateAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Ups>(u =>
+            u.Name == "ups01" &&
+            u.Model == "NewModel" &&
+            u.Va == 1500
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_not_update_model_when_empty_or_whitespace()
+    {
+        var existing = new RackPeek.Domain.Resources.Hardware.Models.Ups
+        {
+            Name = "ups01",
+            Model = "KeepMe",
+            Va = 1000
+        };
+
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("ups01").Returns(existing);
+
+        var sut = new UpdateUpsUseCase(repo);
+
+        await sut.ExecuteAsync(
+            "ups01",
+            model: "   "
+        );
+
+        await repo.Received(1).UpdateAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Ups>(u =>
+            u.Model == "KeepMe"
+        ));
+    }
+}

+ 0 - 1
Tests/Tests.csproj

@@ -26,7 +26,6 @@
 
     <ItemGroup>
       <Folder Include="Hardware\Desktop\" />
-      <Folder Include="Hardware\Ups\" />
     </ItemGroup>
 
 </Project>