diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..5f27bc7
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+# erdserteer ertt
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ff3f4ef..39cb0c8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,5 @@
{
"appService.preDeployTask": "publish-release",
- "appService.deploySubpath": "bin/Release/net9.0/publish"
+ "appService.deploySubpath": "bin/Release/net9.0/publish",
+ "codeQL.createQuery.qlPackLocation": "c:\\Users\\jomyburg\\OneDrive - Microsoft\\Documents\\Content\\repos\\AzureIntegration"
}
\ No newline at end of file
diff --git a/AzureIntegration.csproj b/AzureIntegration.csproj
index 1a81f33..0376369 100644
--- a/AzureIntegration.csproj
+++ b/AzureIntegration.csproj
@@ -1,16 +1,18 @@
- net9.0
+ net8.0
enable
enable
-
-
-
+
+
+
+
+
diff --git a/AzureResourceGroups.md b/AzureResourceGroups.md
new file mode 100644
index 0000000..93f49f6
--- /dev/null
+++ b/AzureResourceGroups.md
@@ -0,0 +1,24 @@
+# Azure Subscriptions and Resource Groups
+
+## PROD Subscription
+
+**ID:** fe040da2-5247-41cd-a8d7-3755d235fda7
+
+| Resource Group | Location |
+| ------------------------- | ------------- |
+| AI | swedencentral |
+| DefaultResourceGroup-SUK | uksouth |
+| Default-ActivityLogAlerts | eastus |
+| alwayson | eastus |
+
+---
+
+## DEV Subscription
+
+**ID:** cd2a8d75-0982-4292-be56-6142df44c75f
+
+| Resource Group | Location |
+| ------------------------- | -------- |
+| AlwaysOn | uksouth |
+| IoT | uksouth |
+| Default-ActivityLogAlerts | eastus |
diff --git a/Controllers/ResourceGroupsControllerTest.cs b/Controllers/ResourceGroupsControllerTest.cs
new file mode 100644
index 0000000..6423e8b
--- /dev/null
+++ b/Controllers/ResourceGroupsControllerTest.cs
@@ -0,0 +1,206 @@
+using AzureIntegration.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using Moq.Protected;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace AzureIntegration.Tests.Controllers
+{
+ public class ResourceGroupsControllerTest
+ {
+ private readonly Mock _mockHttpClientFactory;
+ private readonly Mock _mockConfiguration;
+
+ public ResourceGroupsControllerTest()
+ {
+ _mockHttpClientFactory = new Mock();
+ _mockConfiguration = new Mock();
+ }
+
+ [Fact]
+ public async Task GetAzureManagementGroups_ReturnsManagementGroups_WhenApiCallSucceeds()
+ {
+ // Arrange
+ var mockHttpMessageHandler = new Mock();
+ var response = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(@"
+ {
+ ""value"": [
+ {
+ ""id"": ""/providers/Microsoft.Management/managementGroups/mg1"",
+ ""type"": ""Microsoft.Management/managementGroups"",
+ ""properties"": {
+ ""displayName"": ""Management Group 1""
+ }
+ },
+ {
+ ""id"": ""/providers/Microsoft.Management/managementGroups/mg2"",
+ ""type"": ""Microsoft.Management/managementGroups"",
+ ""properties"": {
+ ""displayName"": ""Management Group 2""
+ }
+ }
+ ]
+ }")
+ };
+
+ mockHttpMessageHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(response);
+
+ var httpClient = new HttpClient(mockHttpMessageHandler.Object);
+ _mockHttpClientFactory
+ .Setup(factory => factory.CreateClient("AzureServices"))
+ .Returns(httpClient);
+
+ var controller = new ResourceGroupsController(_mockHttpClientFactory.Object, _mockConfiguration.Object);
+
+ // Act
+ var result = await controller.ManagementGroups();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ var model = Assert.IsAssignableFrom>(viewResult.Model);
+ Assert.Equal(2, model.Count);
+ Assert.Equal("Management Group 1", model[0].Name);
+ Assert.Equal("/providers/Microsoft.Management/managementGroups/mg1", model[0].Id);
+ Assert.Equal("Microsoft.Management/managementGroups", model[0].Type);
+ Assert.Equal("Management Group 2", model[1].Name);
+ }
+
+ [Fact]
+ public async Task GetAzureManagementGroups_ReturnsEmptyList_WhenNoValuePropertyInResponse()
+ {
+ // Arrange
+ var mockHttpMessageHandler = new Mock();
+ var response = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(@"{ ""someOtherProperty"": [] }")
+ };
+
+ mockHttpMessageHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(response);
+
+ var httpClient = new HttpClient(mockHttpMessageHandler.Object);
+ _mockHttpClientFactory
+ .Setup(factory => factory.CreateClient("AzureServices"))
+ .Returns(httpClient);
+
+ var controller = new ResourceGroupsController(_mockHttpClientFactory.Object, _mockConfiguration.Object);
+
+ // Act
+ var result = await controller.ManagementGroups();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ var model = Assert.IsAssignableFrom>(viewResult.Model);
+ Assert.Empty(model);
+ }
+
+ [Fact]
+ public async Task GetAzureManagementGroups_HandlesNullValues_InResponseProperties()
+ {
+ // Arrange
+ var mockHttpMessageHandler = new Mock();
+ var response = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(@"
+ {
+ ""value"": [
+ {
+ ""id"": null,
+ ""type"": null,
+ ""properties"": {
+ ""displayName"": null
+ }
+ }
+ ]
+ }")
+ };
+
+ mockHttpMessageHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(response);
+
+ var httpClient = new HttpClient(mockHttpMessageHandler.Object);
+ _mockHttpClientFactory
+ .Setup(factory => factory.CreateClient("AzureServices"))
+ .Returns(httpClient);
+
+ var controller = new ResourceGroupsController(_mockHttpClientFactory.Object, _mockConfiguration.Object);
+
+ // Act
+ var result = await controller.ManagementGroups();
+
+ // Assert
+ var viewResult = Assert.IsType(result);
+ var model = Assert.IsAssignableFrom>(viewResult.Model);
+ Assert.Single(model);
+ Assert.Equal(string.Empty, model[0].Name);
+ Assert.Equal(string.Empty, model[0].Id);
+ Assert.Equal(string.Empty, model[0].Type);
+ }
+
+ [Fact]
+ public async Task GetAzureManagementGroups_CallsCorrectEndpoint()
+ {
+ // Arrange
+ var mockHttpMessageHandler = new Mock();
+ HttpRequestMessage capturedRequest = null;
+
+ mockHttpMessageHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .Callback((request, _) => capturedRequest = request)
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(@"{ ""value"": [] }")
+ });
+
+ var httpClient = new HttpClient(mockHttpMessageHandler.Object);
+ _mockHttpClientFactory
+ .Setup(factory => factory.CreateClient("AzureServices"))
+ .Returns(httpClient);
+
+ var controller = new ResourceGroupsController(_mockHttpClientFactory.Object, _mockConfiguration.Object);
+
+ // Act
+ await controller.ManagementGroups();
+
+ // Assert
+ Assert.NotNull(capturedRequest);
+ Assert.Equal(HttpMethod.Get, capturedRequest.Method);
+ Assert.Equal("providers/Microsoft.Management/managementGroups?api-version=2020-05-01",
+ capturedRequest.RequestUri.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index d0a0eba..8a0645a 100644
--- a/Program.cs
+++ b/Program.cs
@@ -40,7 +40,7 @@
// Register the HTTP client
builder.Services.AddHttpClient();
-builder.Services.AddSingleton(new Subscription(subscriptionId));
+builder.Services.AddSingleton(new Subscription(subscriptionId ?? throw new InvalidOperationException("SubscriptionId is required")));
// Register the configuration
builder.Services.AddSingleton(builder.Configuration);
builder.Services.AddDistributedMemoryCache();
diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml
index 2c1de97..9011c24 100644
--- a/Views/Shared/_Layout.cshtml
+++ b/Views/Shared/_Layout.cshtml
@@ -62,10 +62,37 @@
© 2025 - AzureIntegration - Privacy
+
+
+
+
@await RenderSectionAsync("Scripts", required: false)
+