diff --git a/SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs b/SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs
new file mode 100644
index 0000000..d88fb7c
--- /dev/null
+++ b/SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs
@@ -0,0 +1,178 @@
+using System.IO;
+using System.Text.Json;
+using Moq;
+using SharepointToolbox.Core.Models;
+using SharepointToolbox.Services;
+using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
+using AppMsalClientFactory = SharepointToolbox.Infrastructure.Auth.MsalClientFactory;
+
+namespace SharepointToolbox.Tests.Services;
+
+///
+/// Unit tests for AppRegistrationResult, TenantProfile.AppId, and AppRegistrationService.
+/// Graph API calls require live Entra connectivity and are marked as Integration tests.
+/// Pure logic (model behaviour, BuildRequiredResourceAccess structure) is covered here.
+///
+[Trait("Category", "Unit")]
+public class AppRegistrationServiceTests
+{
+ // ────────────────────────────────────────────────────────────────────────
+ // AppRegistrationResult — factory method tests
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Success_CarriesAppId()
+ {
+ var result = AppRegistrationResult.Success("appId123");
+
+ Assert.True(result.IsSuccess);
+ Assert.False(result.IsFallback);
+ Assert.Equal("appId123", result.AppId);
+ Assert.Null(result.ErrorMessage);
+ }
+
+ [Fact]
+ public void Failure_CarriesMessage()
+ {
+ var result = AppRegistrationResult.Failure("Something went wrong");
+
+ Assert.False(result.IsSuccess);
+ Assert.False(result.IsFallback);
+ Assert.Null(result.AppId);
+ Assert.Equal("Something went wrong", result.ErrorMessage);
+ }
+
+ [Fact]
+ public void FallbackRequired_SetsFallback()
+ {
+ var result = AppRegistrationResult.FallbackRequired();
+
+ Assert.False(result.IsSuccess);
+ Assert.True(result.IsFallback);
+ Assert.Null(result.AppId);
+ Assert.Null(result.ErrorMessage);
+ }
+
+ // ────────────────────────────────────────────────────────────────────────
+ // TenantProfile.AppId — nullable field tests
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void AppId_DefaultsToNull()
+ {
+ var profile = new TenantProfile();
+ Assert.Null(profile.AppId);
+ }
+
+ [Fact]
+ public void AppId_RoundTrips_ViaJson()
+ {
+ var profile = new TenantProfile
+ {
+ Name = "Test Tenant",
+ TenantUrl = "https://example.sharepoint.com",
+ ClientId = "client-id-abc",
+ AppId = "registered-app-id-xyz"
+ };
+
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ var json = JsonSerializer.Serialize(profile, options);
+ var loaded = JsonSerializer.Deserialize(json, options);
+
+ Assert.NotNull(loaded);
+ Assert.Equal("registered-app-id-xyz", loaded!.AppId);
+ Assert.Equal("Test Tenant", loaded.Name);
+ }
+
+ [Fact]
+ public void AppId_Null_RoundTrips_ViaJson()
+ {
+ var profile = new TenantProfile
+ {
+ Name = "Test Tenant",
+ TenantUrl = "https://example.sharepoint.com",
+ ClientId = "client-id-abc",
+ AppId = null
+ };
+
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ var json = JsonSerializer.Serialize(profile, options);
+ var loaded = JsonSerializer.Deserialize(json, options);
+
+ Assert.NotNull(loaded);
+ Assert.Null(loaded!.AppId);
+ }
+
+ // ────────────────────────────────────────────────────────────────────────
+ // AppRegistrationService — constructor / dependency wiring
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void AppRegistrationService_ImplementsInterface()
+ {
+ // Verify that the concrete class satisfies the interface contract.
+ // We instantiate with a real MsalClientFactory (no-IO path) and mocked session manager / logger.
+ var msalFactory = new AppMsalClientFactory(Path.GetTempPath());
+ var graphFactory = new AppGraphClientFactory(msalFactory);
+ var sessionManagerMock = new Mock();
+ var loggerMock = new Microsoft.Extensions.Logging.Abstractions.NullLogger();
+
+ var service = new AppRegistrationService(graphFactory, msalFactory, sessionManagerMock.Object, loggerMock);
+
+ Assert.IsAssignableFrom(service);
+ }
+
+ // ────────────────────────────────────────────────────────────────────────
+ // BuildRequiredResourceAccess — structure verification (no live calls)
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void BuildRequiredResourceAccess_ContainsTwoResources()
+ {
+ var result = AppRegistrationService.BuildRequiredResourceAccess();
+
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
+ public void BuildRequiredResourceAccess_GraphResource_HasFourScopes()
+ {
+ const string graphAppId = "00000003-0000-0000-c000-000000000000";
+ var result = AppRegistrationService.BuildRequiredResourceAccess();
+
+ var graphEntry = result.Single(r => r.ResourceAppId == graphAppId);
+ Assert.NotNull(graphEntry.ResourceAccess);
+ Assert.Equal(4, graphEntry.ResourceAccess!.Count);
+ }
+
+ [Fact]
+ public void BuildRequiredResourceAccess_SharePointResource_HasOneScope()
+ {
+ const string spoAppId = "00000003-0000-0ff1-ce00-000000000000";
+ var result = AppRegistrationService.BuildRequiredResourceAccess();
+
+ var spoEntry = result.Single(r => r.ResourceAppId == spoAppId);
+ Assert.NotNull(spoEntry.ResourceAccess);
+ Assert.Single(spoEntry.ResourceAccess!);
+ }
+
+ [Fact]
+ public void BuildRequiredResourceAccess_AllScopes_HaveScopeType()
+ {
+ var result = AppRegistrationService.BuildRequiredResourceAccess();
+
+ var allAccess = result.SelectMany(r => r.ResourceAccess!);
+ Assert.All(allAccess, ra => Assert.Equal("Scope", ra.Type));
+ }
+
+ [Fact]
+ public void BuildRequiredResourceAccess_GraphResource_ContainsUserReadScope()
+ {
+ const string graphAppId = "00000003-0000-0000-c000-000000000000";
+ var userReadGuid = Guid.Parse("e1fe6dd8-ba31-4d61-89e7-88639da4683d"); // User.Read
+ var result = AppRegistrationService.BuildRequiredResourceAccess();
+
+ var graphEntry = result.Single(r => r.ResourceAppId == graphAppId);
+ Assert.Contains(graphEntry.ResourceAccess!, ra => ra.Id == userReadGuid);
+ }
+}