Skip to content
Permalink
Browse files

Added integration test project

  • Loading branch information
Joonas Westlin
Joonas Westlin committed Nov 24, 2019
1 parent afff5d4 commit ce951cbedf2ae341107d43518a3c2f5d1ac278ab
@@ -1,3 +1,6 @@
.vs
Joonasw.AadTestingDemo.API/bin
Joonasw.AadTestingDemo.API/obj
Joonasw.AadTestingDemo.IntegrationTests/bin
Joonasw.AadTestingDemo.IntegrationTests/obj
*.user
@@ -0,0 +1,11 @@
{
"IntegrationTest": {
"KeyVaultUrl": ""
},
"Authentication": {
"Authority": "",
"AuthorizationUrl": "",
"ClientId": "",
"ApplicationIdUri": ""
}
}
@@ -0,0 +1,24 @@
using System.Net.Http;
using Joonasw.AadTestingDemo.IntegrationTests.Utils;
using Xunit;

namespace Joonasw.AadTestingDemo.IntegrationTests
{
/// <summary>
/// Base class for all integration tests
/// </summary>
[Collection("Integration")]
public abstract class IntegrationTestBase
{
protected IntegrationTestBase(AppFixture app)
{
Client = app.Client;
Settings = app.Settings;
AccessTokenProvider = new AccessTokenProvider(app.Settings);
}

protected HttpClient Client { get; }
protected IntegrationTestSettings Settings { get; }
protected AccessTokenProvider AccessTokenProvider { get; }
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>false</IsPackable>
<UserSecretsId>c5961393-e80c-40d1-84f5-34416fbcb979</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.0.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.7.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Joonasw.AadTestingDemo.API\Joonasw.AadTestingDemo.API.csproj" />
</ItemGroup>

</Project>
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:3816/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Joonasw.AadTestingDemo.IntegrationTests": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:3817/"
}
}
}
@@ -0,0 +1,53 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Joonasw.AadTestingDemo.IntegrationTests.Utils;
using Xunit;

namespace Joonasw.AadTestingDemo.IntegrationTests
{
public class ThingsTests : IntegrationTestBase
{
public ThingsTests(AppFixture app) : base(app)
{
}

[Theory]
[InlineData("/Things")]
[InlineData("/Things/other")]
public async Task CallWithoutAuthenticationFails(string url)
{
var req = new HttpRequestMessage(HttpMethod.Get, url);

var res = await Client.SendAsync(req);

Assert.Equal(HttpStatusCode.Unauthorized, res.StatusCode);
}

[Theory]
[InlineData("/Things")]
[InlineData("/Things/other")]
public async Task CallWithUserAuthenticationSucceeds(string url)
{
var req = new HttpRequestMessage(HttpMethod.Get, url);
await AccessTokenProvider.AuthenticateRequestAsUserAsync(req);

var res = await Client.SendAsync(req);

Assert.Equal(HttpStatusCode.OK, res.StatusCode);
}

[Theory]
[InlineData("/Things")]
[InlineData("/Things/other")]
public async Task CallWithAppAuthenticationSucceeds(string url)
{
var req = new HttpRequestMessage(HttpMethod.Get, url);
await AccessTokenProvider.AuthenticateRequestAsAppAsync(req);

var res = await Client.SendAsync(req);

Assert.Equal(HttpStatusCode.OK, res.StatusCode);
}
}
}
@@ -0,0 +1,72 @@
using Microsoft.Identity.Client;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Joonasw.AadTestingDemo.IntegrationTests.Utils
{
/// <summary>
/// Authenticates requests as a user / as an app
/// for integration tests.
/// </summary>
public class AccessTokenProvider
{
private readonly IntegrationTestSettings _settings;
private readonly IConfidentialClientApplication _confidentialClientApp;
private readonly HttpClient _client;

public AccessTokenProvider(IntegrationTestSettings settings)
{
_settings = settings;
_confidentialClientApp = ConfidentialClientApplicationBuilder
.Create(settings.AppAuthentication.ClientId)
.WithClientSecret(settings.AppAuthentication.ClientSecret)
.WithAuthority(settings.Authority)
.Build();
_client = new HttpClient();
}

/// <summary>
/// Authenticates the request as a user,
/// with delegated permissions.
/// </summary>
/// <param name="req">The request to authenticate</param>
public async Task AuthenticateRequestAsUserAsync(HttpRequestMessage req)
{
var tokenReq = new HttpRequestMessage(HttpMethod.Post, _settings.UserAuthentication.TokenUrl)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "password",
["username"] = _settings.UserAuthentication.Username,
["password"] = _settings.UserAuthentication.Password,
["client_id"] = _settings.UserAuthentication.ClientId,
["client_secret"] = _settings.UserAuthentication.ClientSecret,
["scope"] = $"{_settings.ApiAppIdUri}/.default"
})
};

var res = await _client.SendAsync(tokenReq);

string json = await res.Content.ReadAsStringAsync();
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(json);

req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
}


/// <summary>
/// Authenticates the request as an app,
/// with application permissions.
/// </summary>
/// <param name="req">The request to authenticate</param>
public async Task AuthenticateRequestAsAppAsync(HttpRequestMessage req)
{
var scopes = new[] { $"{_settings.ApiAppIdUri}/.default" };
var result = await _confidentialClientApp.AcquireTokenForClient(scopes).ExecuteAsync();
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
}
}
}
@@ -0,0 +1,19 @@
namespace Joonasw.AadTestingDemo.IntegrationTests.Utils
{
/// <summary>
/// Settings for authenticating a test
/// request as an app
/// </summary>
public class AppAuthenticationSettings
{
/// <summary>
/// Client id / application id for the
/// registered test app
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// Secret for the registered test app
/// </summary>
public string ClientSecret { get; set; }
}
}
@@ -0,0 +1,71 @@
using Joonasw.AadTestingDemo.API;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using System;
using System.Net.Http;

namespace Joonasw.AadTestingDemo.IntegrationTests.Utils
{
public class AppFixture : IDisposable
{
private readonly WebApplicationFactory<Startup> _webAppFactory;

public HttpClient Client { get; }
public IntegrationTestSettings Settings { get; private set; }

public AppFixture()
{
_webAppFactory = new WebApplicationFactory<Startup>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("IntegrationTesting");
builder.ConfigureAppConfiguration(configBuilder =>
{
// Adds user secrets for the integration test project
// Contains the Key Vault URL for me
configBuilder.AddUserSecrets<AppFixture>();

// Build temporary config, get Key Vault URL, add Key Vault as config source
var config = configBuilder.Build();
string keyVaultUrl = config["IntegrationTest:KeyVaultUrl"];
if (!string.IsNullOrEmpty(keyVaultUrl))
{
// This will use Managed Identity / local user authentication
// For this to work in a CI pipeline,
// you will need to somehow pass in a client id + client secret
// and use a different overload that takes those.
// Locally doing this is better though.
configBuilder.AddAzureKeyVault(keyVaultUrl);
config = configBuilder.Build();
}

Settings = config.GetSection("IntegrationTest").Get<IntegrationTestSettings>();
});
});
Client = _webAppFactory.CreateDefaultClient();
}

private bool _disposedValue = false; // To detect redundant calls

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_webAppFactory.Dispose();
}

_disposedValue = true;
}
}

// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
}
}
}
@@ -0,0 +1,13 @@
using Xunit;

namespace Joonasw.AadTestingDemo.IntegrationTests.Utils
{
/// <summary>
/// Sets up each test class within the Integration
/// collection to get AppFixture in their constructors
/// </summary>
[CollectionDefinition("Integration")]
public class AppFixtureCollectionConnector : ICollectionFixture<AppFixture>
{
}
}
@@ -0,0 +1,23 @@
namespace Joonasw.AadTestingDemo.IntegrationTests.Utils
{
public class IntegrationTestSettings
{
/// <summary>
/// Base URL of the Azure Key Vault
/// containing settings for tests,
/// including authentication credentials.
/// </summary>
public string KeyVaultUrl { get; set; }
/// <summary>
/// The Azure AD authority, e.g. https://login.microsoftonline.com/your-aad-tenant-id/v2.0
/// </summary>
public string Authority { get; set; }
/// <summary>
/// The App ID URI for the API app registration,
/// e.g. api://some-guid-generated-by-aad
/// </summary>
public string ApiAppIdUri { get; set; }
public UserAuthenticationSettings UserAuthentication { get; set; }
public AppAuthenticationSettings AppAuthentication { get; set; }
}
}
@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Joonasw.AadTestingDemo.IntegrationTests.Utils
{
class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
}
}

0 comments on commit ce951cb

Please sign in to comment.
You can’t perform that action at this time.