diff --git a/backend/src/Designer/Controllers/UserController.cs b/backend/src/Designer/Controllers/UserController.cs
index 5173cdb193d..5e81574fd1f 100644
--- a/backend/src/Designer/Controllers/UserController.cs
+++ b/backend/src/Designer/Controllers/UserController.cs
@@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+using Altinn.Studio.Designer.Helpers;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authorization;
@@ -17,16 +20,18 @@ public class UserController : ControllerBase
{
private readonly IGitea _giteaApi;
private readonly IAntiforgery _antiforgery;
+ private readonly IUserService _userService;
///
/// Initializes a new instance of the class.
///
/// the gitea wrapper
/// Access to the antiforgery system in .NET Core
- public UserController(IGitea giteaWrapper, IAntiforgery antiforgery)
+ public UserController(IGitea giteaWrapper, IAntiforgery antiforgery, IUserService userService)
{
_giteaApi = giteaWrapper;
_antiforgery = antiforgery;
+ _userService = userService;
}
///
@@ -80,6 +85,16 @@ public async Task PutStarred(string org, string repository)
return success ? NoContent() : StatusCode(418);
}
+ [HttpGet]
+ [Route("org-permissions/{org}")]
+ public async Task HasAccessToCreateRepository(string org)
+ {
+ string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
+ AltinnOrgContext editingContext = AltinnOrgContext.FromOrg(org, developer);
+ UserOrgPermission userOrg = await _userService.GetUserOrgPermission(editingContext);
+ return Ok(userOrg);
+ }
+
///
/// Removes the star marking on the specified repository.
///
diff --git a/backend/src/Designer/Helpers/Guard.cs b/backend/src/Designer/Helpers/Guard.cs
index 493ed15edd8..7a9894e78b5 100644
--- a/backend/src/Designer/Helpers/Guard.cs
+++ b/backend/src/Designer/Helpers/Guard.cs
@@ -25,6 +25,15 @@ public static void AssertArgumentNotNullOrWhiteSpace(string value, string argume
}
}
+ public static void AssertValidateOrganization(string org)
+ {
+ AssertNotNullOrEmpty(org, nameof(org));
+ if (!AltinnRegexes.AltinnOrganizationNameRegex().IsMatch(org))
+ {
+ throw new ArgumentException("Provided organization name is not valid");
+ }
+ }
+
///
/// Asserts value not null.
///
diff --git a/backend/src/Designer/Infrastructure/ServiceRegistration.cs b/backend/src/Designer/Infrastructure/ServiceRegistration.cs
index 104b7d7ae9b..599a8bc1d47 100644
--- a/backend/src/Designer/Infrastructure/ServiceRegistration.cs
+++ b/backend/src/Designer/Infrastructure/ServiceRegistration.cs
@@ -54,6 +54,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/backend/src/Designer/Models/AltinnOrgContext.cs b/backend/src/Designer/Models/AltinnOrgContext.cs
new file mode 100644
index 00000000000..a27281b615f
--- /dev/null
+++ b/backend/src/Designer/Models/AltinnOrgContext.cs
@@ -0,0 +1,23 @@
+using Altinn.Studio.Designer.Helpers;
+
+namespace Altinn.Studio.Designer.Models;
+
+public class AltinnOrgContext
+{
+ public string Org { get; }
+ public string DeveloperName { get; }
+
+ private AltinnOrgContext(string org, string developerName)
+ {
+ Guard.AssertValidateOrganization(org);
+ Org = org;
+
+ Guard.AssertArgumentNotNullOrWhiteSpace(developerName, nameof(developerName));
+ DeveloperName = developerName;
+ }
+
+ public static AltinnOrgContext FromOrg(string org, string developerName)
+ {
+ return new AltinnOrgContext(org, developerName);
+ }
+}
diff --git a/backend/src/Designer/Models/AltinnRepoContext.cs b/backend/src/Designer/Models/AltinnRepoContext.cs
index c42e5a40ad7..8a3cbb3b829 100644
--- a/backend/src/Designer/Models/AltinnRepoContext.cs
+++ b/backend/src/Designer/Models/AltinnRepoContext.cs
@@ -1,5 +1,4 @@
-using System;
-using Altinn.Studio.Designer.Helpers;
+using Altinn.Studio.Designer.Helpers;
namespace Altinn.Studio.Designer.Models
{
@@ -21,21 +20,12 @@ public class AltinnRepoContext
protected AltinnRepoContext(string org, string repo)
{
- ValidateOrganization(org);
+ Guard.AssertValidateOrganization(org);
Guard.AssertValidAppRepoName(repo);
Org = org;
Repo = repo;
}
- private void ValidateOrganization(string org)
- {
- Guard.AssertNotNullOrEmpty(org, nameof(org));
- if (!AltinnRegexes.AltinnOrganizationNameRegex().IsMatch(org))
- {
- throw new ArgumentException("Provided organization name is not valid");
- }
- }
-
public static AltinnRepoContext FromOrgRepo(string org, string repo)
{
return new AltinnRepoContext(org, repo);
diff --git a/backend/src/Designer/Models/Dto/UserOrgPermission.cs b/backend/src/Designer/Models/Dto/UserOrgPermission.cs
new file mode 100644
index 00000000000..07096438d0d
--- /dev/null
+++ b/backend/src/Designer/Models/Dto/UserOrgPermission.cs
@@ -0,0 +1,6 @@
+namespace Altinn.Studio.Designer.Models.Dto;
+
+public class UserOrgPermission
+{
+ public bool CanCreateOrgRepo { get; set; }
+}
diff --git a/backend/src/Designer/RepositoryClient/Model/Team.cs b/backend/src/Designer/RepositoryClient/Model/Team.cs
index 17711303a0b..3b20f79adfe 100644
--- a/backend/src/Designer/RepositoryClient/Model/Team.cs
+++ b/backend/src/Designer/RepositoryClient/Model/Team.cs
@@ -10,6 +10,8 @@ public class Team
///
public string Name { get; set; }
+ public bool CanCreateOrgRepo { get; set; }
+
///
/// The organization that owns the team
///
diff --git a/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs b/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs
index 9f37754b8c8..6af6bcef16b 100644
--- a/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs
+++ b/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs
@@ -78,11 +78,17 @@ public async Task> GetTeams()
HttpResponseMessage response = await _httpClient.GetAsync(url);
if (response.StatusCode == HttpStatusCode.OK)
{
- teams = await response.Content.ReadAsAsync>() ?? new List();
+ string jsonString = await response.Content.ReadAsStringAsync();
+ var deserializeOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ };
+
+ teams = JsonSerializer.Deserialize>(jsonString, deserializeOptions) ?? new List();
}
else
{
- _logger.LogError("Cold not retrieve teams for user " + AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext) + " GetTeams failed with statuscode " + response.StatusCode);
+ _logger.LogError("Could not retrieve teams for user " + AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext) + " GetTeams failed with status code " + response.StatusCode);
}
return teams;
diff --git a/backend/src/Designer/Services/Implementation/UserService.cs b/backend/src/Designer/Services/Implementation/UserService.cs
new file mode 100644
index 00000000000..c2e17006611
--- /dev/null
+++ b/backend/src/Designer/Services/Implementation/UserService.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Models.Dto;
+using Altinn.Studio.Designer.RepositoryClient.Model;
+using Altinn.Studio.Designer.Services.Interfaces;
+
+namespace Altinn.Studio.Designer.Services.Implementation;
+
+public class UserService : IUserService
+{
+ private readonly IGitea _giteaApi;
+
+ public UserService(IGitea giteaApi)
+ {
+ _giteaApi = giteaApi;
+ }
+
+ public async Task GetUserOrgPermission(AltinnOrgContext altinnOrgContext)
+ {
+ bool canCreateOrgRepo = await HasPermissionToCreateOrgRepo(altinnOrgContext);
+ return new UserOrgPermission() { CanCreateOrgRepo = canCreateOrgRepo };
+ }
+
+ private bool IsUserSelfOrg(string developerName, string org)
+ {
+ return developerName == org;
+ }
+
+ private async Task HasPermissionToCreateOrgRepo(AltinnOrgContext altinnOrgContext)
+ {
+ List teams = await _giteaApi.GetTeams();
+ return IsUserSelfOrg(altinnOrgContext.DeveloperName, altinnOrgContext.Org) ||
+ teams.Any(team => CheckPermissionToCreateOrgRepo(team, altinnOrgContext.Org));
+ }
+
+ private static bool CheckPermissionToCreateOrgRepo(Team team, string org)
+ {
+ return team.CanCreateOrgRepo && team.Organization.Username == org;
+ }
+}
diff --git a/backend/src/Designer/Services/Interfaces/IUserService.cs b/backend/src/Designer/Services/Interfaces/IUserService.cs
new file mode 100644
index 00000000000..c77418b52d1
--- /dev/null
+++ b/backend/src/Designer/Services/Interfaces/IUserService.cs
@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Models.Dto;
+
+namespace Altinn.Studio.Designer.Services.Interfaces;
+
+public interface IUserService
+{
+ public Task GetUserOrgPermission(AltinnOrgContext altinnOrgContext);
+}
diff --git a/backend/src/Designer/TypedHttpClients/DelegatingHandlers/AzureDevOpsTokenDelegatingHandler.cs b/backend/src/Designer/TypedHttpClients/DelegatingHandlers/AzureDevOpsTokenDelegatingHandler.cs
new file mode 100644
index 00000000000..6832d5d11a6
--- /dev/null
+++ b/backend/src/Designer/TypedHttpClients/DelegatingHandlers/AzureDevOpsTokenDelegatingHandler.cs
@@ -0,0 +1,24 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Altinn.Studio.Designer.TypedHttpClients.DelegatingHandlers;
+
+public class AzureDevOpsTokenDelegatingHandler : DelegatingHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
+
+ if (response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError)
+ {
+ Content = new StringContent("Failed to interact with Azure DevOps. Contact system support.")
+ };
+ }
+
+ return response;
+ }
+}
diff --git a/backend/src/Designer/TypedHttpClients/TypedHttpClientRegistration.cs b/backend/src/Designer/TypedHttpClients/TypedHttpClientRegistration.cs
index 7b387507409..b20fe66cad4 100644
--- a/backend/src/Designer/TypedHttpClients/TypedHttpClientRegistration.cs
+++ b/backend/src/Designer/TypedHttpClients/TypedHttpClientRegistration.cs
@@ -38,6 +38,7 @@ public static class TypedHttpClientRegistration
public static IServiceCollection RegisterTypedHttpClients(this IServiceCollection services, IConfiguration config)
{
services.AddHttpClient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddAzureDevOpsTypedHttpClient(config);
@@ -70,7 +71,7 @@ private static IHttpClientBuilder AddAzureDevOpsTypedHttpClient(this IServiceCol
client.BaseAddress = new Uri($"{azureDevOpsSettings.BaseUri}build/builds/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);
- }).AddHttpMessageHandler();
+ }).AddHttpMessageHandler().AddHttpMessageHandler();
}
private static IHttpClientBuilder AddKubernetesWrapperTypedHttpClient(this IServiceCollection services)
diff --git a/backend/tests/Designer.Tests/GiteaIntegrationTests/UserControllerGiteaIntegrationTests.cs b/backend/tests/Designer.Tests/GiteaIntegrationTests/UserControllerGiteaIntegrationTests.cs
index e2576cb7adf..88b75a6ae6c 100644
--- a/backend/tests/Designer.Tests/GiteaIntegrationTests/UserControllerGiteaIntegrationTests.cs
+++ b/backend/tests/Designer.Tests/GiteaIntegrationTests/UserControllerGiteaIntegrationTests.cs
@@ -4,6 +4,7 @@
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.RepositoryClient.Model;
using Designer.Tests.Fixtures;
using Designer.Tests.Utils;
@@ -14,8 +15,9 @@ namespace Designer.Tests.GiteaIntegrationTests
{
public class UserControllerGiteaIntegrationTests : GiteaIntegrationTestsBase
{
-
- public UserControllerGiteaIntegrationTests(GiteaWebAppApplicationFactoryFixture factory, GiteaFixture giteaFixture, SharedDesignerHttpClientProvider sharedDesignerHttpClientProvider) : base(factory, giteaFixture, sharedDesignerHttpClientProvider)
+ public UserControllerGiteaIntegrationTests(GiteaWebAppApplicationFactoryFixture factory,
+ GiteaFixture giteaFixture, SharedDesignerHttpClientProvider sharedDesignerHttpClientProvider) : base(
+ factory, giteaFixture, sharedDesignerHttpClientProvider)
{
}
@@ -31,10 +33,8 @@ public async Task GetCurrentUser_ShouldReturnOk(string expectedUserName, string
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Headers.First(h => h.Key == "Set-Cookie").Value.Should().Contain(e => e.Contains("XSRF-TOKEN"));
string content = await response.Content.ReadAsStringAsync();
- var user = JsonSerializer.Deserialize(content, new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- });
+ var user = JsonSerializer.Deserialize(content,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
user.Login.Should().Be(expectedUserName);
user.Email.Should().Be(expectedEmail);
@@ -62,16 +62,40 @@ public async Task StarredEndpoints_ShouldBehaveAsExpected(string org)
string targetRepo = TestDataHelper.GenerateTestRepoName();
await CreateAppUsingDesigner(org, targetRepo);
- using var putStarredResponse = await HttpClient.PutAsync($"designer/api/user/starred/{org}/{targetRepo}", null);
+ using var putStarredResponse =
+ await HttpClient.PutAsync($"designer/api/user/starred/{org}/{targetRepo}", null);
putStarredResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);
await GetAndVerifyStarredRepos(targetRepo);
- using var deleteStarredResponse = await HttpClient.DeleteAsync($"designer/api/user/starred/{org}/{targetRepo}");
+ using var deleteStarredResponse =
+ await HttpClient.DeleteAsync($"designer/api/user/starred/{org}/{targetRepo}");
deleteStarredResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);
await GetAndVerifyStarredRepos();
}
+ [Theory]
+ [InlineData(GiteaConstants.TestOrgUsername, true)]
+ [InlineData("OtherOrg", false)]
+ public async Task HasAccessToCreateRepository_ShouldReturnCorrectPermissions(string org, bool expectedCanCreate)
+ {
+ string requestUrl = $"designer/api/user/org-permissions/{org}";
+
+ using var response = await HttpClient.GetAsync(requestUrl);
+
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ string content = await response.Content.ReadAsStringAsync();
+ var deserializeOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ var userOrgPermission = JsonSerializer.Deserialize(content, deserializeOptions);
+
+ userOrgPermission.Should().NotBeNull();
+ userOrgPermission.CanCreateOrgRepo.Should().Be(expectedCanCreate);
+ }
+
private async Task GetAndVerifyStarredRepos(params string[] expectedStarredRepos)
{
using var response = await HttpClient.GetAsync("designer/api/user/starred");
@@ -83,6 +107,5 @@ private async Task GetAndVerifyStarredRepos(params string[] expectedStarredRepos
content.Should().Contain(r => r.Name == expectedStarredRepo);
}
}
-
}
}
diff --git a/backend/tests/Designer.Tests/Helpers/GuardTests.cs b/backend/tests/Designer.Tests/Helpers/GuardTests.cs
index 4966c45e948..da90664720c 100644
--- a/backend/tests/Designer.Tests/Helpers/GuardTests.cs
+++ b/backend/tests/Designer.Tests/Helpers/GuardTests.cs
@@ -1,7 +1,5 @@
using System;
-
using Altinn.Studio.Designer.Helpers;
-
using Xunit;
namespace Designer.Tests.Helpers
@@ -12,7 +10,8 @@ public class GuardTests
[InlineData("filename.xsd", ".xs")]
[InlineData("path/to/filename.json", "json")]
[InlineData("path/to/filename.schema.json", "jsonschema")]
- public void AssertFileExtensionIsOfType_InCorrectType_ShouldThrowException(string file, string incorrectExtension)
+ public void AssertFileExtensionIsOfType_InCorrectType_ShouldThrowException(string file,
+ string incorrectExtension)
{
Assert.Throws(() => Guard.AssertFileExtensionIsOfType(file, incorrectExtension));
}
@@ -21,7 +20,8 @@ public void AssertFileExtensionIsOfType_InCorrectType_ShouldThrowException(strin
[InlineData("filename.xsd", ".xsd")]
[InlineData("path/to/filename.json", ".json")]
[InlineData("path/to/filename.schema.json", ".json")]
- public void AssertFileExtensionIsOfType_CorrectType_ShouldNotThrowException(string file, string correctExtension)
+ public void AssertFileExtensionIsOfType_CorrectType_ShouldNotThrowException(string file,
+ string correctExtension)
{
Guard.AssertFileExtensionIsOfType(file, correctExtension);
Assert.True(true);
@@ -43,5 +43,23 @@ public void AssertValidAppRepoName_InvalidName_ShouldThrowException(string name)
{
Assert.Throws(() => Guard.AssertValidAppRepoName(name));
}
+
+ [Theory]
+ [InlineData("ValidOrgName")]
+ public void AssertValidateOrganization_ValidOrg_ShouldNotThrowException(string org)
+ {
+ Guard.AssertValidateOrganization(org);
+ Assert.True(true);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("Invalid@OrgName")]
+ public void AssertValidateOrganization_InvalidOrg_ShouldThrowException(string org)
+ {
+ Assert.Throws(() => Guard.AssertValidateOrganization(org));
+ }
}
}
diff --git a/backend/tests/Designer.Tests/Services/UserServiceTests.cs b/backend/tests/Designer.Tests/Services/UserServiceTests.cs
new file mode 100644
index 00000000000..e55efeae404
--- /dev/null
+++ b/backend/tests/Designer.Tests/Services/UserServiceTests.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.RepositoryClient.Model;
+using Altinn.Studio.Designer.Services.Implementation;
+using Altinn.Studio.Designer.Services.Interfaces;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Xunit;
+
+namespace Designer.Tests.Services
+{
+ public class UserServiceTests
+ {
+ private readonly Mock _giteaApi;
+
+ public UserServiceTests()
+ {
+ _giteaApi = new Mock();
+ }
+
+ [Theory]
+ [InlineData("org1", false)]
+ [InlineData("org2", true)]
+ public async Task GetUserOrgPermission_ReturnsCorrectPermission(string org, bool expectedCanCreate)
+ {
+ var teams = new List
+ {
+ new()
+ {
+ Organization = new Organization { Username = org }, CanCreateOrgRepo = expectedCanCreate
+ }
+ };
+
+ _giteaApi.Setup(api => api.GetTeams()).ReturnsAsync(teams);
+
+ var userService = new UserService(_giteaApi.Object);
+
+ AltinnOrgContext altinnOrgContext = AltinnOrgContext.FromOrg(org, "developer");
+ var result = await userService.GetUserOrgPermission(altinnOrgContext);
+
+ Assert.NotNull(result);
+ Assert.Equal(expectedCanCreate, result.CanCreateOrgRepo);
+ }
+ }
+}
diff --git a/backend/tests/Designer.Tests/TypedHttpClients/DelegatingHandlers/AzureDevOpsTokenDelegatingHandlerTests.cs b/backend/tests/Designer.Tests/TypedHttpClients/DelegatingHandlers/AzureDevOpsTokenDelegatingHandlerTests.cs
new file mode 100644
index 00000000000..ee54efa5b68
--- /dev/null
+++ b/backend/tests/Designer.Tests/TypedHttpClients/DelegatingHandlers/AzureDevOpsTokenDelegatingHandlerTests.cs
@@ -0,0 +1,70 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.TypedHttpClients.DelegatingHandlers;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+public class AzureDevOpsTokenDelegatingHandlerTests
+{
+ [Fact]
+ public async Task SendAsync_WhenUnauthorized_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var mockInnerHandler = new Mock();
+
+ mockInnerHandler.Protected()
+ .Setup>("SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.Unauthorized
+ });
+
+ var handler = new AzureDevOpsTokenDelegatingHandler
+ {
+ InnerHandler = mockInnerHandler.Object
+ };
+
+ HttpClient httpClient = new HttpClient(handler);
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://dev.azure.com");
+
+ HttpResponseMessage response = await httpClient.SendAsync(request);
+ string content = await response.Content.ReadAsStringAsync();
+
+ Assert.Equal("Failed to interact with Azure DevOps. Contact system support.", content);
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task SendAsync_WhenSuccessful_ReturnsResponse()
+ {
+ var mockInnerHandler = new Mock();
+
+ mockInnerHandler.Protected()
+ .Setup>("SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent("Success")
+ });
+
+ var handler = new AzureDevOpsTokenDelegatingHandler
+ {
+ InnerHandler = mockInnerHandler.Object
+ };
+
+ HttpClient httpClient = new HttpClient(handler);
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://dev.azure.com");
+
+ HttpResponseMessage response = await httpClient.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Success", await response.Content.ReadAsStringAsync());
+ }
+}