Sfoglia il codice sorgente

Merge branch 'main' into Manage-Switch-Through-Cli

Tim Jones 5 mesi fa
parent
commit
a5c8236072

+ 27 - 0
RackPeek.Domain/Resources/Hardware/Server/Drive/AddDriveUseCase.cs

@@ -0,0 +1,27 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Drive;
+
+public class AddDrivesUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        string type,
+        int size)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server)
+        {
+            return;
+        }
+
+        server.Drives ??= [];
+
+        server.Drives.Add(new Models.Drive
+        {
+            Type = type,
+            Size = size
+        });
+        
+        await repository.UpdateAsync(server);
+    }
+}

+ 15 - 0
RackPeek.Domain/Resources/Hardware/Server/Drive/RemoveDriveUseCase.cs

@@ -0,0 +1,15 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Drive;
+
+public class RemoveDriveUseCase(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.Drives ??= [];
+        if (index < 0 || index >= server.Drives.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "Drive index out of range.");
+        server.Drives.RemoveAt(index);
+        await repository.UpdateAsync(server);
+    }
+}

+ 21 - 0
RackPeek.Domain/Resources/Hardware/Server/Drive/UpdateDriveUseCase.cs

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

+ 27 - 0
RackPeek.Domain/Resources/Hardware/Server/Nic/AddNicUseCase.cs

@@ -0,0 +1,27 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Nic;
+
+public class AddNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        string type,
+        int speed,
+        int ports)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server)
+            return;
+
+        server.Nics ??= [];
+
+        server.Nics.Add(new Models.Nic
+        {
+            Type = type,
+            Speed = speed,
+            Ports = ports
+        });
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 21 - 0
RackPeek.Domain/Resources/Hardware/Server/Nic/RemoveNicUseCase.cs

@@ -0,0 +1,21 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Nic;
+
+public class RemoveNicUseCase(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.Nics ??= [];
+
+        if (index < 0 || index >= server.Nics.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "NIC index out of range.");
+
+        server.Nics.RemoveAt(index);
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 29 - 0
RackPeek.Domain/Resources/Hardware/Server/Nic/UpdateNicUseCase.cs

@@ -0,0 +1,29 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Nic;
+
+public class UpdateNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        int index,
+        string type,
+        int speed,
+        int ports)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server)
+            return;
+
+        server.Nics ??= [];
+
+        if (index < 0 || index >= server.Nics.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "NIC index out of range.");
+
+        var nic = server.Nics[index];
+        nic.Type = type;
+        nic.Speed = speed;
+        nic.Ports = ports;
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 36 - 0
RackPeek/Commands/Server/Drives/ServerDriveAddCommand.cs

@@ -0,0 +1,36 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Drive;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Drives;
+
+public class ServerDriveAddSettings : ServerNameSettings
+{
+    [CommandOption("--type <TYPE>")]
+    public string Type { get; set; }
+
+    [CommandOption("--size <SIZE>")]
+    public int Size { get; set; }
+}
+
+public class ServerDriveAddCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerDriveAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerDriveAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDrivesUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Type,
+            settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Server/Drives/ServerDriveRemoveCommand.cs

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

+ 40 - 0
RackPeek/Commands/Server/Drives/ServerDriveUpdateCommand.cs

@@ -0,0 +1,40 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Drive;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Drives;
+
+public class ServerDriveUpdateSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")]
+    public int Index { get; set; }
+
+    [CommandOption("--type <TYPE>")]
+    public string Type { get; set; }
+
+    [CommandOption("--size <SIZE>")]
+    public int Size { get; set; }
+}
+
+public class ServerDriveUpdateCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerDriveUpdateSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerDriveUpdateSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDriveUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index,
+            settings.Type,
+            settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive {settings.Index} updated on '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 40 - 0
RackPeek/Commands/Server/Nics/ServerNicAddCommand.cs

@@ -0,0 +1,40 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Nic;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Nics;
+
+public class ServerNicAddSettings : ServerNameSettings
+{
+    [CommandOption("--type <TYPE>")]
+    public string Type { get; set; }
+
+    [CommandOption("--speed <SPEED>")]
+    public int Speed { get; set; }
+
+    [CommandOption("--ports <PORTS>")]
+    public int Ports { get; set; }
+}
+
+public class ServerNicAddCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerNicAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerNicAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddNicUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Type,
+            settings.Speed,
+            settings.Ports);
+
+        AnsiConsole.MarkupLine($"[green]NIC added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Server/Nics/ServerNicRemoveCommand.cs

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

+ 44 - 0
RackPeek/Commands/Server/Nics/ServerNicUpdateCommand.cs

@@ -0,0 +1,44 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Nic;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Nics;
+
+public class ServerNicUpdateSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")]
+    public int Index { get; set; }
+
+    [CommandOption("--type <TYPE>")]
+    public string Type { get; set; }
+
+    [CommandOption("--speed <SPEED>")]
+    public int Speed { get; set; }
+
+    [CommandOption("--ports <PORTS>")]
+    public int Ports { get; set; }
+}
+
+public class ServerNicUpdateCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerNicUpdateSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerNicUpdateSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateNicUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index,
+            settings.Type,
+            settings.Speed,
+            settings.Ports);
+
+        AnsiConsole.MarkupLine($"[green]NIC {settings.Index} updated on '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 58 - 0
RackPeek/Program.cs

@@ -12,6 +12,16 @@ using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Server;
 using RackPeek.Domain.Resources.Hardware.Server;
 using RackPeek.Domain.Resources.Hardware.Server.Cpu;
 using RackPeek.Domain.Resources.Hardware.Server.Cpu;
 using RackPeek.Domain.Resources.Hardware.Switchs;
 using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Commands.Server.Nics;
+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.Nic;
+using RackPeek.Commands.Server.Drives;
+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.Yaml;
 using RackPeek.Yaml;
 
 
 namespace RackPeek;
 namespace RackPeek;
@@ -87,6 +97,12 @@ public static class Program
         services.AddScoped<AddCpuUseCase>();
         services.AddScoped<AddCpuUseCase>();
         services.AddScoped<UpdateCpuUseCase>();
         services.AddScoped<UpdateCpuUseCase>();
         services.AddScoped<RemoveCpuUseCase>();
         services.AddScoped<RemoveCpuUseCase>();
+        
+        // Drive use cases
+        services.AddScoped<AddDrivesUseCase>();
+        services.AddScoped<UpdateDriveUseCase>();
+        services.AddScoped<RemoveDriveUseCase>();
+
 
 
         // CPU commands
         // CPU commands
         services.AddScoped<ServerCpuAddCommand>();
         services.AddScoped<ServerCpuAddCommand>();
@@ -108,6 +124,21 @@ public static class Program
         services.AddScoped<GetSwitchesUseCase>();
         services.AddScoped<GetSwitchesUseCase>();
         services.AddScoped<UpdateSwitchUseCase>();
         services.AddScoped<UpdateSwitchUseCase>();
 
 
+        // NIC use cases
+        services.AddScoped<AddNicUseCase>();
+        services.AddScoped<UpdateNicUseCase>();
+        services.AddScoped<RemoveNicUseCase>();
+        
+        // NIC commands
+        services.AddScoped<ServerNicAddCommand>();
+        services.AddScoped<ServerNicUpdateCommand>();
+        services.AddScoped<ServerNicRemoveCommand>();
+        // Drive commands
+        services.AddScoped<ServerDriveAddCommand>();
+        services.AddScoped<ServerDriveUpdateCommand>();
+        services.AddScoped<ServerDriveRemoveCommand>();
+
+        
         // Spectre bootstrap
         // Spectre bootstrap
         var registrar = new TypeRegistrar(services);
         var registrar = new TypeRegistrar(services);
         var app = new CommandApp(registrar);
         var app = new CommandApp(registrar);
@@ -154,6 +185,33 @@ public static class Program
                     cpu.AddCommand<ServerCpuRemoveCommand>("del")
                     cpu.AddCommand<ServerCpuRemoveCommand>("del")
                         .WithDescription("Remove a CPU from a server");
                         .WithDescription("Remove a CPU from a server");
                 });
                 });
+                
+                server.AddBranch("nic", nic =>
+                {
+                    nic.SetDescription("Manage server NICs");
+
+                    nic.AddCommand<ServerNicAddCommand>("add")
+                        .WithDescription("Add a NIC to a server");
+
+                    nic.AddCommand<ServerNicUpdateCommand>("set")
+                        .WithDescription("Update a NIC on a server");
+
+                    nic.AddCommand<ServerNicRemoveCommand>("del")
+                        .WithDescription("Remove a NIC from a server");
+                server.AddBranch("drive", drive =>
+                {
+                    drive.SetDescription("Manage server drives");
+
+                    drive.AddCommand<ServerDriveAddCommand>("add")
+                        .WithDescription("Add a drive to a server");
+
+                    drive.AddCommand<ServerDriveUpdateCommand>("set")
+                        .WithDescription("Update a drive on a server");
+
+                    drive.AddCommand<ServerDriveRemoveCommand>("del")
+                        .WithDescription("Remove a drive from a server");
+                });
+
             });
             });
 
 
             config.AddBranch("switches", server =>
             config.AddBranch("switches", server =>

+ 96 - 0
Tests/Hardware/AddDriveUseCaseTests.cs

@@ -0,0 +1,96 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Server.Drive;
+
+namespace Tests.Hardware;
+
+public class AddDrivesUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Adds_drive_when_server_exists()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new RackPeek.Domain.Resources.Hardware.Models.Server
+        {
+            Name = "node01",
+            Drives = new List<RackPeek.Domain.Resources.Hardware.Models.Drive>()
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new AddDrivesUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            serverName: "node01",
+            type: "NVMe",
+            size: 2000
+        );
+
+        // Assert
+        Assert.Single(server.Drives);
+        Assert.Equal("NVMe", server.Drives[0].Type);
+        Assert.Equal(2000, server.Drives[0].Size);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Server>(s =>
+            s.Name == "node01" &&
+            s.Drives.Count == 1 &&
+            s.Drives[0].Type == "NVMe" &&
+            s.Drives[0].Size == 2000
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Initializes_drive_list_when_null()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new RackPeek.Domain.Resources.Hardware.Models.Server
+        {
+            Name = "node01",
+            Drives = null
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new AddDrivesUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            serverName: "node01",
+            type: "SATA",
+            size: 500
+        );
+
+        // Assert
+        Assert.NotNull(server.Drives);
+        Assert.Single(server.Drives);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<RackPeek.Domain.Resources.Hardware.Models.Server>(s =>
+            s.Drives != null &&
+            s.Drives.Count == 1 &&
+            s.Drives[0].Type == "SATA"
+        ));
+    }
+
+    [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 AddDrivesUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            serverName: "node01",
+            type: "NVMe",
+            size: 2000
+        );
+
+        // Assert
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<RackPeek.Domain.Resources.Hardware.Models.Server>());
+    }
+}

+ 70 - 0
Tests/Hardware/AddNicUseCaseTests.cs

@@ -0,0 +1,70 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Nic;
+using Xunit;
+
+namespace Tests.Hardware;
+
+public class AddNicUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Adds_nic_when_server_exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Nics = new List<Nic>()
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new AddNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", "10GBase-T", 10000, 2);
+
+        Assert.Single(server.Nics);
+        Assert.Equal("10GBase-T", server.Nics[0].Type);
+        Assert.Equal(10000, server.Nics[0].Speed!.Value);
+        Assert.Equal(2, server.Nics[0].Ports!.Value);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Nics.Count == 1 &&
+            s.Nics[0].Type == "10GBase-T"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Initializes_list_when_null()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server { Name = "node01", Nics = null };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new AddNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", "SFP+", 10000, 1);
+
+        Assert.NotNull(server.Nics);
+        Assert.Single(server.Nics);
+
+        await repo.Received(1).UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_nothing_when_server_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+
+        repo.GetByNameAsync("node01")
+            .Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new AddNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", "SFP+", 10000, 1);
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}

+ 89 - 0
Tests/Hardware/RemoveDriveUseCaseTests.cs

@@ -0,0 +1,89 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Drive;
+using Xunit;
+
+namespace Tests.Hardware;
+
+public class RemoveDriveUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Removes_drive_when_index_is_valid()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Drives = new List<Drive>
+            {
+                new Drive { Type = "NVMe", Size = 2000 },
+                new Drive { Type = "SATA", Size = 500 }
+            }
+        };
+
+        // ❗ FIXED: return the server, not null
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new RemoveDriveUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync("node01", index: 0);
+
+        // Assert
+        Assert.Single(server.Drives);
+        Assert.Equal("SATA", server.Drives[0].Type);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Name == "node01" &&
+            s.Drives.Count == 1 &&
+            s.Drives[0].Type == "SATA"
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_index_out_of_range()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Drives = new List<Drive>
+            {
+                new Drive { Type = "NVMe", Size = 2000 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new RemoveDriveUseCase(repo);
+
+        // Act & Assert
+        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
+            await sut.ExecuteAsync("node01", index: 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 RemoveDriveUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync("node01", index: 0);
+
+        // Assert
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}

+ 71 - 0
Tests/Hardware/RemoveNicUseCaseTests.cs

@@ -0,0 +1,71 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Nic;
+using Xunit;
+
+namespace Tests.Hardware;
+
+public class RemoveNicUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Removes_nic_when_index_valid()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Nics = new List<Nic>
+            {
+                new Nic { Type = "10GBase-T", Speed = 10000, Ports = 2 },
+                new Nic { Type = "SFP+", Speed = 10000, Ports = 1 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new RemoveNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", 0);
+
+        Assert.Single(server.Nics);
+        Assert.Equal("SFP+", server.Nics[0].Type);
+
+        await repo.Received(1).UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_when_index_out_of_range()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Nics = new List<Nic> { new Nic() }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new RemoveNicUseCase(repo);
+
+        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
+            sut.ExecuteAsync("node01", 1));
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_nothing_when_server_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+
+        repo.GetByNameAsync("node01")
+            .Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new RemoveNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", 0);
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}

+ 102 - 0
Tests/Hardware/UpdateDriveUseCaseTests.cs

@@ -0,0 +1,102 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Drive;
+using Xunit;
+
+namespace Tests.Hardware;
+
+public class UpdateDriveUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Updates_drive_when_index_is_valid()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Drives = new List<Drive>
+            {
+                new Drive { Type = "NVMe", Size = 2000 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new UpdateDriveUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            serverName: "node01",
+            index: 0,
+            type: "SATA",
+            size: 500
+        );
+
+        // Assert
+        Assert.Equal("SATA", server.Drives[0].Type);
+        Assert.Equal(500, server.Drives[0].Size);
+
+        await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
+            s.Name == "node01" &&
+            s.Drives.Count == 1 &&
+            s.Drives[0].Type == "SATA" &&
+            s.Drives[0].Size == 500
+        ));
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_if_index_out_of_range()
+    {
+        // Arrange
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Drives = new List<Drive>
+            {
+                new Drive { Type = "NVMe", Size = 2000 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new UpdateDriveUseCase(repo);
+
+        // Act & Assert
+        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
+            await sut.ExecuteAsync(
+                serverName: "node01",
+                index: 1,
+                type: "SATA",
+                size: 500
+            )
+        );
+
+        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 UpdateDriveUseCase(repo);
+
+        // Act
+        await sut.ExecuteAsync(
+            serverName: "node01",
+            index: 0,
+            type: "SATA",
+            size: 500
+        );
+
+        // Assert
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}

+ 71 - 0
Tests/Hardware/UpdateNicUseCaseTests.cs

@@ -0,0 +1,71 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server.Nic;
+using Xunit;
+
+namespace Tests.Hardware;
+
+public class UpdateNicUseCaseTests
+{
+    [Fact]
+    public async Task ExecuteAsync_Updates_nic_when_index_valid()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Nics = new List<Nic>
+            {
+                new Nic { Type = "10GBase-T", Speed = 10000, Ports = 2 }
+            }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new UpdateNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", 0, "SFP+", 10000, 1);
+
+        Assert.Equal("SFP+", server.Nics[0].Type);
+        Assert.Equal(10000, server.Nics[0].Speed!.Value);
+        Assert.Equal(1, server.Nics[0].Ports!.Value);
+
+        await repo.Received(1).UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Throws_when_index_out_of_range()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var server = new Server
+        {
+            Name = "node01",
+            Nics = new List<Nic> { new Nic() }
+        };
+
+        repo.GetByNameAsync("node01").Returns(server);
+
+        var sut = new UpdateNicUseCase(repo);
+
+        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
+            sut.ExecuteAsync("node01", 1, "SFP+", 10000, 1));
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+
+    [Fact]
+    public async Task ExecuteAsync_Does_nothing_when_server_not_found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+
+        repo.GetByNameAsync("node01")
+            .Returns((RackPeek.Domain.Resources.Hardware.Models.Hardware?)null);
+
+        var sut = new UpdateNicUseCase(repo);
+
+        await sut.ExecuteAsync("node01", 0, "SFP+", 10000, 1);
+
+        await repo.DidNotReceive().UpdateAsync(Arg.Any<Server>());
+    }
+}