Browse Source

UseCase, Tests, Commands complete and Commands registered

James 2 tháng trước cách đây
mục cha
commit
42192a293a

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

@@ -6,8 +6,4 @@
         <Nullable>enable</Nullable>
     </PropertyGroup>
 
-    <ItemGroup>
-      <Folder Include="Resources\Hardware\Server\Gpu\" />
-    </ItemGroup>
-
 </Project>

+ 26 - 0
RackPeek.Domain/Resources/Hardware/Server/Gpu/AddGpuUseCase.cs

@@ -0,0 +1,26 @@
+
+namespace RackPeek.Domain.Resources.Hardware.Server.Gpu;
+
+public class AddGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        string model,
+        int vram)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server) 
+            return;
+
+        server.Gpus ??= [];
+
+        server.Gpus.Add(new Models.Gpu
+        {
+            Model = model,
+            Vram = vram
+        });
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 21 - 0
RackPeek.Domain/Resources/Hardware/Server/Gpu/RemoveGpuUseCase.cs

@@ -0,0 +1,21 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Gpu;
+
+public class RemoveGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string serverName, int index)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server) 
+            return;
+
+        server.Gpus ??= [];
+
+        if (index < 0 || index >= server.Gpus.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "GPU index out of range.");
+
+        server.Gpus.RemoveAt(index);
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 27 - 0
RackPeek.Domain/Resources/Hardware/Server/Gpu/UpdateGpuUseCase.cs

@@ -0,0 +1,27 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Gpu;
+
+public class UpdateGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        int index,
+        string model,
+        int vram)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server) 
+            return;
+
+        server.Gpus ??= [];
+
+        if (index < 0 || index >= server.Gpus.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "GPU index out of range.");
+
+        var gpu = server.Gpus[index];
+        gpu.Model = model;
+        gpu.Vram = vram;
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 36 - 0
RackPeek/Commands/Server/Gpu/AddGpuUseCaseCommand.cs

@@ -0,0 +1,36 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Gpu;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Gpu;
+
+public class ServerGpuAddSettings : ServerNameSettings
+{
+    [CommandOption("--model <MODEL>")] 
+    public string Model { get; set; }
+
+    [CommandOption("--vram <VRAM>")] 
+    public int Vram { get; set; }
+}
+
+public class ServerGpuAddCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerGpuAddSettings>cd
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerGpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddGpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Vram);
+
+        AnsiConsole.MarkupLine($"[green]GPU added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Server/Gpu/RemoveGpuUseCaseCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Gpu;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Gpu;
+
+public class ServerGpuRemoveSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")] 
+    public int Index { get; set; }
+}
+
+public class ServerGpuRemoveCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerGpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerGpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveGpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]GPU {settings.Index} removed from '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 40 - 0
RackPeek/Commands/Server/Gpu/UpdateGpuUseCaseCommand.cs

@@ -0,0 +1,40 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Gpu;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Gpu;
+
+public class ServerGpuUpdateSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")] 
+    public int Index { get; set; }
+
+    [CommandOption("--model <MODEL>")] 
+    public string Model { get; set; }
+
+    [CommandOption("--vram <VRAM>")] 
+    public int Vram { get; set; }
+}
+
+public class ServerGpuUpdateCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerGpuUpdateSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerGpuUpdateSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateGpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index,
+            settings.Model,
+            settings.Vram);
+
+        AnsiConsole.MarkupLine($"[green]GPU {settings.Index} updated on '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 28 - 0
RackPeek/Program.cs

@@ -5,6 +5,7 @@ using RackPeek.Commands;
 using RackPeek.Commands.Server;
 using RackPeek.Commands.Server.Cpus;
 using RackPeek.Commands.Server.Drives;
+using RackPeek.Commands.Server.Gpu;
 using RackPeek.Commands.Server.Nics;
 using RackPeek.Commands.Switches;
 using RackPeek.Domain.Resources.Hardware;
@@ -12,6 +13,7 @@ using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Server;
 using RackPeek.Domain.Resources.Hardware.Server.Cpu;
 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.Switchs;
 using RackPeek.Spectre;
@@ -110,6 +112,11 @@ public static class CliBootstrap
         services.AddScoped<AddDrivesUseCase>();
         services.AddScoped<UpdateDriveUseCase>();
         services.AddScoped<RemoveDriveUseCase>();
+        
+        // GPU use cases
+        services.AddScoped<AddGpuUseCase>();
+        services.AddScoped<UpdateGpuUseCase>();
+        services.AddScoped<RemoveGpuUseCase>();
 
 
         // CPU commands
@@ -141,10 +148,17 @@ public static class CliBootstrap
         services.AddScoped<ServerNicAddCommand>();
         services.AddScoped<ServerNicUpdateCommand>();
         services.AddScoped<ServerNicRemoveCommand>();
+        
         // Drive commands
         services.AddScoped<ServerDriveAddCommand>();
         services.AddScoped<ServerDriveUpdateCommand>();
         services.AddScoped<ServerDriveRemoveCommand>();
+        
+        // GPU commands
+        services.AddScoped<ServerGpuAddCommand>();
+        services.AddScoped<ServerGpuUpdateCommand>();
+        services.AddScoped<ServerGpuRemoveCommand>();
+
 
 
         // Spectre bootstrap
@@ -216,6 +230,20 @@ public static class CliBootstrap
                         drive.AddCommand<ServerDriveRemoveCommand>("del")
                             .WithDescription("Remove a drive from a server");
                     });
+                    server.AddBranch("gpu", gpu =>
+                    {
+                        gpu.SetDescription("Manage server GPUs");
+
+                        gpu.AddCommand<ServerGpuAddCommand>("add")
+                            .WithDescription("Add a GPU to a server");
+
+                        gpu.AddCommand<ServerGpuUpdateCommand>("set")
+                            .WithDescription("Update a GPU on a server");
+
+                        gpu.AddCommand<ServerGpuRemoveCommand>("del")
+                            .WithDescription("Remove a GPU from a server");
+                    });
+
                 });
 
                 config.AddBranch("switches", server =>

+ 0 - 4
RackPeek/RackPeek.csproj

@@ -23,8 +23,4 @@
         <ProjectReference Include="..\RackPeek.Domain\RackPeek.Domain.csproj"/>
     </ItemGroup>
 
-    <ItemGroup>
-      <Folder Include="Commands\Server\Gpu\" />
-    </ItemGroup>
-
 </Project>

+ 98 - 0
Tests/Hardware/AddGpuUseCaseTests.cs

@@ -0,0 +1,98 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Gpu;
+
+namespace Tests.Hardware;
+
+public class AddGpuUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Adds_gpu_when_server_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Gpus = new List<Gpu>()
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new AddGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "node01",
+            "RTX 4090",
+            24
+        );
+
+        // Assert
+        Assert.Single(server.Gpus);
+        Assert.Equal("RTX 4090", server.Gpus[0].Model);
+        Assert.Equal(24, server.Gpus[0].Vram);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Name == "node01" &&
+            s.Gpus.Count == 1 &&
+            s.Gpus[0].Model == "RTX 4090" &&
+            s.Gpus[0].Vram == 24
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Initializes_gpu_list_when_null()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Gpus = null
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new AddGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "node01",
+            "RTX 3080",
+            10
+        );
+
+        // Assert
+        Assert.NotNull(server.Gpus);
+        Assert.Single(server.Gpus);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Gpus != null &&
+            s.Gpus.Count == 1 &&
+            s.Gpus[0].Model == "RTX 3080"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_nothing_when_server_does_not_exist()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("node01")
+            .Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new AddGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "node01",
+            "RTX 4090",
+            24
+        );
+
+        // Assert
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}

+ 1 - 1
Tests/Hardware/RemoveDriveUseCaseTests.cs

@@ -22,7 +22,7 @@ public class RemoveDriveUseCaseTests
             }
         };
 
-        // ❗ FIXED: return the server, not null
+        
         repo.GetByNameAsync("node01").Returns(server);
 
         var sut = new RemoveDriveUseCase(repo);

+ 86 - 0
Tests/Hardware/RemoveGpuUseCaseTests.cs

@@ -0,0 +1,86 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Gpu;
+
+namespace Tests.Hardware;
+
+public class RemoveGpuUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Removes_gpu_when_index_is_valid()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Gpus = new List<Gpu>
+            {
+                new() { Model = "RTX 4090", Vram = 24 },
+                new() { Model = "RTX 3080", Vram = 10 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new RemoveGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync("node01", 0);
+
+        // Assert
+        Assert.Single(server.Gpus);
+        Assert.Equal("RTX 3080", server.Gpus[0].Model);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Name == "node01" &&
+            s.Gpus.Count == 1 &&
+            s.Gpus[0].Model == "RTX 3080"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_index_out_of_range()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Gpus = new List<Gpu>
+            {
+                new() { Model = "RTX 4090", Vram = 24 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new RemoveGpuUseCase(repo);
+
+        // Act & Assert
+        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
+            await sut.ExecuteAsync("node01", 1)
+        );
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_nothing_when_server_does_not_exist()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+
+        repo.GetByNameAsync("node01")
+            .Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new RemoveGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync("node01", 0);
+
+        // Assert
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}

+ 101 - 0
Tests/Hardware/UpdateGpuUseCaseTests.cs

@@ -0,0 +1,101 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Gpu;
+
+namespace Tests.Hardware;
+
+public class UpdateGpuUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Updates_gpu_when_index_is_valid()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Gpus = new List<Gpu>
+            {
+                new() { Model = "RTX 4090", Vram = 24 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new UpdateGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "node01",
+            0,
+            "RTX 3080",
+            10
+        );
+
+        // Assert
+        Assert.Equal("RTX 3080", server.Gpus[0].Model);
+        Assert.Equal(10, server.Gpus[0].Vram);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Name == "node01" &&
+            s.Gpus.Count == 1 &&
+            s.Gpus[0].Model == "RTX 3080" &&
+            s.Gpus[0].Vram == 10
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_index_out_of_range()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Gpus = new List<Gpu>
+            {
+                new() { Model = "RTX 4090", Vram = 24 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new UpdateGpuUseCase(repo);
+
+        // Act & Assert
+        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
+            await sut.ExecuteAsync(
+                "node01",
+                1,
+                "RTX 3080",
+                10
+            )
+        );
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_nothing_when_server_does_not_exist()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+
+        repo.GetByNameAsync("node01")
+            .Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new UpdateGpuUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            "node01",
+            0,
+            "RTX 3080",
+            10
+        );
+
+        // Assert
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}