This commit is contained in:
2026-06-10 13:06:55 -07:00
parent 4621e8380b
commit 570ab363c2
987 changed files with 22413 additions and 21031 deletions

View File

View File

View File

View File

View File

View File

View File

0
tests/api/MicCheck.Api.Tests.Integration/readme.md Normal file → Executable file
View File

View File

@@ -1,268 +1,268 @@
using System.Net;
using System.Net.Http.Json;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Admin;
[TestFixture]
public class AdminApiIntegrationTests
{
private MicCheckWebApplicationFactory _factory = null!;
private string _rawApiKey = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
_rawApiKey = SeedOrganizationWithApiKey();
}
[TearDown]
public void TearDown() => _factory.Dispose();
private string SeedOrganizationWithApiKey()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = new Organization { Name = "Test Org", CreatedAt = DateTimeOffset.UtcNow };
db.Organizations.Add(org);
db.SaveChanges();
var rawKey = ApiKeyHasher.GenerateKey();
var hashedKey = ApiKeyHasher.Hash(rawKey);
var prefix = rawKey.Length >= 8 ? rawKey[..8] : rawKey;
db.ApiKeys.Add(new ApiKey
{
Key = hashedKey,
Prefix = prefix,
Name = "Test Key",
OrganizationId = org.Id,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
return rawKey;
}
private HttpClient CreateAuthenticatedClient()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {_rawApiKey}");
return client;
}
[Test]
public async Task WhenCreatingAProject_ThenProjectIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var response = await client.PostAsJsonAsync("/api/v1/projects", new
{
name = "My Project",
organizationId = org.Id
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
}
[Test]
public async Task WhenCreatingAFeature_ThenFeatureIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
{
name = "dark_mode",
type = "Standard",
initialValue = (string?)null,
description = (string?)null
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
}
[Test]
public async Task WhenCreatingAFeature_ThenFeatureStateIsAutoCreatedForEnvironments()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
db.Environments.Add(new AppEnvironment
{
Name = "Production",
ApiKey = "env-key-prod",
ProjectId = project.Id,
CreatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
{
name = "flag_x",
type = "Standard",
initialValue = (string?)null,
description = (string?)null
});
using var verifyScope = _factory.Services.CreateScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var feature = verifyDb.Features.First(f => f.Name == "flag_x");
var states = verifyDb.FeatureStates.Where(fs => fs.FeatureId == feature.Id).ToList();
Assert.That(states, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenCreatingAnEnvironment_ThenFeatureStateIsAutoCreatedForExistingFeatures()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.Features.Add(new Feature
{
Name = "existing_flag",
ProjectId = project.Id,
CreatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
var response = await client.PostAsJsonAsync("/api/v1/environments", new
{
name = "Staging",
projectId = project.Id
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
using var verifyScope = _factory.Services.CreateScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var feature = verifyDb.Features.First(f => f.Name == "existing_flag");
var env = verifyDb.Environments.First(e => e.ProjectId == project.Id);
var states = verifyDb.FeatureStates.Where(fs => fs.EnvironmentId == env.Id && fs.FeatureId == feature.Id).ToList();
Assert.That(states, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenRequestIsMissingApiKey_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/projects?organizationId=1");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenCreatingAFeatureWithInvalidName_ThenUnprocessableEntityIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
{
name = "invalid name with spaces!",
type = "Standard"
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.UnprocessableEntity));
}
[Test]
public async Task WhenCreatingASegment_ThenSegmentIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/segments", new
{
name = "Premium Users",
rules = new[]
{
new
{
type = "All",
conditions = new[]
{
new { property = "plan", @operator = "Equal", value = "premium" }
}
}
}
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
}
}
using System.Net;
using System.Net.Http.Json;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Admin;
[TestFixture]
public class AdminApiIntegrationTests
{
private MicCheckWebApplicationFactory _factory = null!;
private string _rawApiKey = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
_rawApiKey = SeedOrganizationWithApiKey();
}
[TearDown]
public void TearDown() => _factory.Dispose();
private string SeedOrganizationWithApiKey()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = new Organization { Name = "Test Org", CreatedAt = DateTimeOffset.UtcNow };
db.Organizations.Add(org);
db.SaveChanges();
var rawKey = ApiKeyHasher.GenerateKey();
var hashedKey = ApiKeyHasher.Hash(rawKey);
var prefix = rawKey.Length >= 8 ? rawKey[..8] : rawKey;
db.ApiKeys.Add(new ApiKey
{
Key = hashedKey,
Prefix = prefix,
Name = "Test Key",
OrganizationId = org.Id,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
return rawKey;
}
private HttpClient CreateAuthenticatedClient()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {_rawApiKey}");
return client;
}
[Test]
public async Task WhenCreatingAProject_ThenProjectIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var response = await client.PostAsJsonAsync("/api/v1/projects", new
{
name = "My Project",
organizationId = org.Id
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
}
[Test]
public async Task WhenCreatingAFeature_ThenFeatureIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
{
name = "dark_mode",
type = "Standard",
initialValue = (string?)null,
description = (string?)null
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
}
[Test]
public async Task WhenCreatingAFeature_ThenFeatureStateIsAutoCreatedForEnvironments()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
db.Environments.Add(new AppEnvironment
{
Name = "Production",
ApiKey = "env-key-prod",
ProjectId = project.Id,
CreatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
{
name = "flag_x",
type = "Standard",
initialValue = (string?)null,
description = (string?)null
});
using var verifyScope = _factory.Services.CreateScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var feature = verifyDb.Features.First(f => f.Name == "flag_x");
var states = verifyDb.FeatureStates.Where(fs => fs.FeatureId == feature.Id).ToList();
Assert.That(states, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenCreatingAnEnvironment_ThenFeatureStateIsAutoCreatedForExistingFeatures()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.Features.Add(new Feature
{
Name = "existing_flag",
ProjectId = project.Id,
CreatedAt = DateTimeOffset.UtcNow
});
db.SaveChanges();
var response = await client.PostAsJsonAsync("/api/v1/environments", new
{
name = "Staging",
projectId = project.Id
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
using var verifyScope = _factory.Services.CreateScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var feature = verifyDb.Features.First(f => f.Name == "existing_flag");
var env = verifyDb.Environments.First(e => e.ProjectId == project.Id);
var states = verifyDb.FeatureStates.Where(fs => fs.EnvironmentId == env.Id && fs.FeatureId == feature.Id).ToList();
Assert.That(states, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenRequestIsMissingApiKey_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/projects?organizationId=1");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenCreatingAFeatureWithInvalidName_ThenUnprocessableEntityIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/features", new
{
name = "invalid name with spaces!",
type = "Standard"
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.UnprocessableEntity));
}
[Test]
public async Task WhenCreatingASegment_ThenSegmentIsReturned()
{
var client = CreateAuthenticatedClient();
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var org = db.Organizations.First();
var project = new Project
{
Name = "My Project",
OrganizationId = org.Id,
CreatedAt = DateTimeOffset.UtcNow
};
db.Projects.Add(project);
db.SaveChanges();
var response = await client.PostAsJsonAsync($"/api/v1/projects/{project.Id}/segments", new
{
name = "Premium Users",
rules = new[]
{
new
{
type = "All",
conditions = new[]
{
new { property = "plan", @operator = "Equal", value = "premium" }
}
}
}
});
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created));
}
}

View File

@@ -0,0 +1,72 @@
using MicCheck.Api.ApiKeys;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.ApiKeys;
[TestFixture]
public class ApiKeyHasherTests
{
[Test]
public void WhenKeyIsHashed_ThenHashIsDeterministic()
{
var key = "test-api-key";
var hash1 = ApiKeyHasher.Hash(key);
var hash2 = ApiKeyHasher.Hash(key);
Assert.That(hash1, Is.EqualTo(hash2));
}
[Test]
public void WhenKeyIsHashed_ThenHashIsLowercaseHex()
{
var hash = ApiKeyHasher.Hash("any-key");
Assert.That(hash, Does.Match("^[0-9a-f]{64}$"));
}
[Test]
public void WhenDifferentKeysAreHashed_ThenHashesDiffer()
{
var hash1 = ApiKeyHasher.Hash("key-one");
var hash2 = ApiKeyHasher.Hash("key-two");
Assert.That(hash1, Is.Not.EqualTo(hash2));
}
[Test]
public void WhenGenerateKeyIsCalled_ThenKeyIsNotEmpty()
{
var key = ApiKeyHasher.GenerateKey();
Assert.That(key, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenGenerateKeyIsCalled_ThenEachKeyIsUnique()
{
var key1 = ApiKeyHasher.GenerateKey();
var key2 = ApiKeyHasher.GenerateKey();
Assert.That(key1, Is.Not.EqualTo(key2));
}
[Test]
public void WhenGenerateKeyIsCalled_ThenKeyIsBase64UrlSafe()
{
var key = ApiKeyHasher.GenerateKey();
Assert.That(key, Does.Not.Contain("+"));
Assert.That(key, Does.Not.Contain("/"));
Assert.That(key, Does.Not.Contain("="));
}
[Test]
public void WhenGenerateKeyIsHashed_ThenHashCanBeUsedToVerify()
{
var rawKey = ApiKeyHasher.GenerateKey();
var hash = ApiKeyHasher.Hash(rawKey);
Assert.That(ApiKeyHasher.Hash(rawKey), Is.EqualTo(hash));
}
}

View File

@@ -0,0 +1,53 @@
using NUnit.Framework;
using MicCheck.Api.ApiKeys;
namespace MicCheck.Api.Tests.Unit.ApiKeys;
[TestFixture]
public class ApiKeyTests
{
[Test]
public void WhenAnApiKeyIsCreated_ThenRequiredFieldsAreSet()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "CI Pipeline Key",
OrganizationId = 1
};
Assert.That(apiKey.Key, Is.EqualTo("hashed-value"));
Assert.That(apiKey.Prefix, Is.EqualTo("hashed-value".Substring(0, 8)).Or.EqualTo("abc12345"));
Assert.That(apiKey.Name, Is.EqualTo("CI Pipeline Key"));
Assert.That(apiKey.OrganizationId, Is.EqualTo(1));
}
[Test]
public void WhenAnApiKeyIsCreated_ThenIsActiveDefaultsToFalse()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "Test Key",
OrganizationId = 1
};
Assert.That(apiKey.IsActive, Is.False);
}
[Test]
public void WhenAnApiKeyIsCreated_ThenExpiresAtDefaultsToNull()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "Test Key",
OrganizationId = 1
};
Assert.That(apiKey.ExpiresAt, Is.Null);
}
}

View File

View File

View File

@@ -0,0 +1,142 @@
using System.Net;
using System.Net.Http.Headers;
using MicCheck.Api.ApiKeys;
using MicCheck.Api.Data;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authentication;
[TestFixture]
public class ApiKeyAuthenticationHandlerTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
private async Task<string> SeedActiveApiKeyAsync(int organizationId = 1)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Test Key",
OrganizationId = organizationId,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync();
return rawKey;
}
[Test]
public async Task WhenAuthorizationHeaderIsAbsent_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenAuthorizationHeaderIsNotApiKeyScheme_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsInvalid_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsInactive_ThenUnauthorizedIsReturned()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Inactive Key",
OrganizationId = 1,
IsActive = false,
CreatedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsExpired_ThenUnauthorizedIsReturned()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Expired Key",
OrganizationId = 1,
IsActive = true,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1),
CreatedAt = DateTimeOffset.UtcNow.AddDays(-10)
});
await db.SaveChangesAsync();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsValid_ThenOkIsReturned()
{
var rawKey = await SeedActiveApiKeyAsync(organizationId: 1);
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
}

View File

@@ -0,0 +1,185 @@
using System.Net;
using System.Net.Http.Json;
using MicCheck.Api.Authorization;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authorization;
[TestFixture]
public class AuthEndpointsTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
[Test]
public async Task WhenRegisteringWithValidDetails_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenRegisteringWithDuplicateEmail_ThenConflictIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
}
[Test]
public async Task WhenLoginWithValidCredentials_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"bob@example.com", "SecurePass1!", "Bob", "Jones", "BobCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("bob@example.com", "SecurePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenLoginWithWrongPassword_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"carol@example.com", "CorrectPass1!", "Carol", "White", "CarolCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("carol@example.com", "WrongPassword"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithUnknownEmail_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("nobody@example.com", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithEmptyEmail_ThenBadRequestIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task WhenRefreshingWithValidToken_ThenOkIsReturnedWithNewTokens()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"dave@example.com", "SecurePass1!", "Dave", "Brown", "DaveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.EqualTo(registerBody.RefreshToken));
}
[Test]
public async Task WhenRefreshingWithInvalidToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest("not-a-valid-refresh-token"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenRefreshingWithRevokedToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"eve@example.com", "SecurePass1!", "Eve", "Davis", "EveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoggingOut_ThenNoContentIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"frank@example.com", "SecurePass1!", "Frank", "Green", "FrankCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
}
[Test]
public async Task WhenLoggingOutAndRefreshing_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"grace@example.com", "SecurePass1!", "Grace", "Black", "GraceCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
}

View File

@@ -0,0 +1,180 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using MicCheck.Api.Authorization;
using MicCheck.Api.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authorization;
[TestFixture]
public class ProjectPermissionRequirementHandlerTests
{
private MicCheckDbContext _db = null!;
private Mock<IHttpContextAccessor> _httpContextAccessor = null!;
private ProjectPermissionRequirementHandler _handler = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_httpContextAccessor = new Mock<IHttpContextAccessor>();
_handler = new ProjectPermissionRequirementHandler(_db, _httpContextAccessor.Object);
}
[TearDown]
public void TearDown() => _db.Dispose();
private static AuthorizationHandlerContext CreateContext(
ClaimsPrincipal user,
ProjectPermission permission = ProjectPermission.ViewProject)
{
var requirement = new ProjectPermissionRequirement(permission);
return new AuthorizationHandlerContext([requirement], user, null);
}
private static ClaimsPrincipal CreateUserWithClaims(params Claim[] claims)
{
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer"));
}
private void SetupRouteProjectId(int projectId)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues["projectId"] = projectId.ToString();
_httpContextAccessor.Setup(a => a.HttpContext).Returns(httpContext);
}
[Test]
public async Task WhenUserIsOrganizationAdmin_ThenRequirementSucceeds()
{
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"),
new Claim("OrganizationRole", "Admin"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserHasRequiredPermission_ThenRequirementSucceeds()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 1,
ProjectId = 10,
Permissions = [ProjectPermission.ViewProject]
});
await _db.SaveChangesAsync();
SetupRouteProjectId(10);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"));
var context = CreateContext(user, ProjectPermission.ViewProject);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserIsProjectAdmin_ThenAllPermissionsAreGranted()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 2,
ProjectId = 20,
IsAdmin = true,
Permissions = []
});
await _db.SaveChangesAsync();
SetupRouteProjectId(20);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "2"));
var context = CreateContext(user, ProjectPermission.DeleteFeature);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserDoesNotHaveRequiredPermission_ThenRequirementFails()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 3,
ProjectId = 30,
Permissions = [ProjectPermission.ViewProject]
});
await _db.SaveChangesAsync();
SetupRouteProjectId(30);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "3"));
var context = CreateContext(user, ProjectPermission.DeleteFeature);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenUserHasNoProjectPermissionRecord_ThenRequirementFails()
{
SetupRouteProjectId(99);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "5"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenUserIdClaimIsMissing_ThenRequirementFails()
{
var user = CreateUserWithClaims();
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenProjectIdIsNotInRoute_ThenRequirementFails()
{
_httpContextAccessor.Setup(a => a.HttpContext).Returns(new DefaultHttpContext());
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
}

View File

@@ -0,0 +1,103 @@
using System.IdentityModel.Tokens.Jwt;
using MicCheck.Api.Authorization;
using MicCheck.Api.Organizations;
using MicCheck.Api.Users;
using Microsoft.Extensions.Configuration;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authorization;
[TestFixture]
public class TokenServiceTests
{
private IConfiguration _configuration = null!;
private TokenService _tokenService = null!;
[SetUp]
public void SetUp()
{
_configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Jwt:SecretKey"] = "test-secret-key-that-is-long-enough-32c",
["Jwt:Issuer"] = "MicCheck",
["Jwt:Audience"] = "MicCheck",
["Jwt:ExpiryMinutes"] = "60",
})
.Build();
_tokenService = new TokenService(_configuration);
}
private static User BuildUser(int id = 1, string email = "alice@example.com") =>
new()
{
Email = email,
FirstName = "Alice",
LastName = "Smith",
PasswordHash = "hashed",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
};
[Test]
public void WhenAUserIsProvided_ThenATokenIsReturned()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.Token, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenATokenIsGenerated_ThenItContainsTheUserEmailClaim()
{
var user = BuildUser(email: "bob@example.com");
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "bob@example.com"),
Is.True);
}
[Test]
public void WhenATokenIsGenerated_ThenItHasAFutureExpiry()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.ExpiresAt, Is.GreaterThan(DateTime.UtcNow));
}
[Test]
public void WhenATokenIsGenerated_ThenItHasTheConfiguredIssuer()
{
var result = _tokenService.GenerateToken(BuildUser());
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Issuer, Is.EqualTo("MicCheck"));
}
[Test]
public void WhenUserBelongsToOrganizations_ThenTokenContainsOrganizationClaims()
{
var user = BuildUser();
user.Organizations.Add(new OrganizationUser
{
OrganizationId = 42,
UserId = user.Id,
Role = OrganizationRole.Admin
});
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationId" && c.Value == "42"), Is.True);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationRole" && c.Value == "Admin"), Is.True);
}
}

View File

@@ -1,72 +1,72 @@
using MicCheck.Api.Common.Security.ApiKeys;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.ApiKeys;
[TestFixture]
public class ApiKeyHasherTests
{
[Test]
public void WhenKeyIsHashed_ThenHashIsDeterministic()
{
var key = "test-api-key";
var hash1 = ApiKeyHasher.Hash(key);
var hash2 = ApiKeyHasher.Hash(key);
Assert.That(hash1, Is.EqualTo(hash2));
}
[Test]
public void WhenKeyIsHashed_ThenHashIsLowercaseHex()
{
var hash = ApiKeyHasher.Hash("any-key");
Assert.That(hash, Does.Match("^[0-9a-f]{64}$"));
}
[Test]
public void WhenDifferentKeysAreHashed_ThenHashesDiffer()
{
var hash1 = ApiKeyHasher.Hash("key-one");
var hash2 = ApiKeyHasher.Hash("key-two");
Assert.That(hash1, Is.Not.EqualTo(hash2));
}
[Test]
public void WhenGenerateKeyIsCalled_ThenKeyIsNotEmpty()
{
var key = ApiKeyHasher.GenerateKey();
Assert.That(key, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenGenerateKeyIsCalled_ThenEachKeyIsUnique()
{
var key1 = ApiKeyHasher.GenerateKey();
var key2 = ApiKeyHasher.GenerateKey();
Assert.That(key1, Is.Not.EqualTo(key2));
}
[Test]
public void WhenGenerateKeyIsCalled_ThenKeyIsBase64UrlSafe()
{
var key = ApiKeyHasher.GenerateKey();
Assert.That(key, Does.Not.Contain("+"));
Assert.That(key, Does.Not.Contain("/"));
Assert.That(key, Does.Not.Contain("="));
}
[Test]
public void WhenGenerateKeyIsHashed_ThenHashCanBeUsedToVerify()
{
var rawKey = ApiKeyHasher.GenerateKey();
var hash = ApiKeyHasher.Hash(rawKey);
Assert.That(ApiKeyHasher.Hash(rawKey), Is.EqualTo(hash));
}
}
using MicCheck.Api.Common.Security.ApiKeys;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.ApiKeys;
[TestFixture]
public class ApiKeyHasherTests
{
[Test]
public void WhenKeyIsHashed_ThenHashIsDeterministic()
{
var key = "test-api-key";
var hash1 = ApiKeyHasher.Hash(key);
var hash2 = ApiKeyHasher.Hash(key);
Assert.That(hash1, Is.EqualTo(hash2));
}
[Test]
public void WhenKeyIsHashed_ThenHashIsLowercaseHex()
{
var hash = ApiKeyHasher.Hash("any-key");
Assert.That(hash, Does.Match("^[0-9a-f]{64}$"));
}
[Test]
public void WhenDifferentKeysAreHashed_ThenHashesDiffer()
{
var hash1 = ApiKeyHasher.Hash("key-one");
var hash2 = ApiKeyHasher.Hash("key-two");
Assert.That(hash1, Is.Not.EqualTo(hash2));
}
[Test]
public void WhenGenerateKeyIsCalled_ThenKeyIsNotEmpty()
{
var key = ApiKeyHasher.GenerateKey();
Assert.That(key, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenGenerateKeyIsCalled_ThenEachKeyIsUnique()
{
var key1 = ApiKeyHasher.GenerateKey();
var key2 = ApiKeyHasher.GenerateKey();
Assert.That(key1, Is.Not.EqualTo(key2));
}
[Test]
public void WhenGenerateKeyIsCalled_ThenKeyIsBase64UrlSafe()
{
var key = ApiKeyHasher.GenerateKey();
Assert.That(key, Does.Not.Contain("+"));
Assert.That(key, Does.Not.Contain("/"));
Assert.That(key, Does.Not.Contain("="));
}
[Test]
public void WhenGenerateKeyIsHashed_ThenHashCanBeUsedToVerify()
{
var rawKey = ApiKeyHasher.GenerateKey();
var hash = ApiKeyHasher.Hash(rawKey);
Assert.That(ApiKeyHasher.Hash(rawKey), Is.EqualTo(hash));
}
}

View File

@@ -1,53 +1,53 @@
using NUnit.Framework;
using MicCheck.Api.Common.Security.ApiKeys;
namespace MicCheck.Api.Tests.Unit.Common.Security.ApiKeys;
[TestFixture]
public class ApiKeyTests
{
[Test]
public void WhenAnApiKeyIsCreated_ThenRequiredFieldsAreSet()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "CI Pipeline Key",
OrganizationId = 1
};
Assert.That(apiKey.Key, Is.EqualTo("hashed-value"));
Assert.That(apiKey.Prefix, Is.EqualTo("hashed-value".Substring(0, 8)).Or.EqualTo("abc12345"));
Assert.That(apiKey.Name, Is.EqualTo("CI Pipeline Key"));
Assert.That(apiKey.OrganizationId, Is.EqualTo(1));
}
[Test]
public void WhenAnApiKeyIsCreated_ThenIsActiveDefaultsToFalse()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "Test Key",
OrganizationId = 1
};
Assert.That(apiKey.IsActive, Is.False);
}
[Test]
public void WhenAnApiKeyIsCreated_ThenExpiresAtDefaultsToNull()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "Test Key",
OrganizationId = 1
};
Assert.That(apiKey.ExpiresAt, Is.Null);
}
}
using NUnit.Framework;
using MicCheck.Api.Common.Security.ApiKeys;
namespace MicCheck.Api.Tests.Unit.Common.Security.ApiKeys;
[TestFixture]
public class ApiKeyTests
{
[Test]
public void WhenAnApiKeyIsCreated_ThenRequiredFieldsAreSet()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "CI Pipeline Key",
OrganizationId = 1
};
Assert.That(apiKey.Key, Is.EqualTo("hashed-value"));
Assert.That(apiKey.Prefix, Is.EqualTo("hashed-value".Substring(0, 8)).Or.EqualTo("abc12345"));
Assert.That(apiKey.Name, Is.EqualTo("CI Pipeline Key"));
Assert.That(apiKey.OrganizationId, Is.EqualTo(1));
}
[Test]
public void WhenAnApiKeyIsCreated_ThenIsActiveDefaultsToFalse()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "Test Key",
OrganizationId = 1
};
Assert.That(apiKey.IsActive, Is.False);
}
[Test]
public void WhenAnApiKeyIsCreated_ThenExpiresAtDefaultsToNull()
{
var apiKey = new ApiKey
{
Key = "hashed-value",
Prefix = "abc12345",
Name = "Test Key",
OrganizationId = 1
};
Assert.That(apiKey.ExpiresAt, Is.Null);
}
}

View File

@@ -1,142 +1,142 @@
using System.Net;
using System.Net.Http.Headers;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Data;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authentication;
[TestFixture]
public class ApiKeyAuthenticationHandlerTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
private async Task<string> SeedActiveApiKeyAsync(int organizationId = 1)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Test Key",
OrganizationId = organizationId,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync();
return rawKey;
}
[Test]
public async Task WhenAuthorizationHeaderIsAbsent_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenAuthorizationHeaderIsNotApiKeyScheme_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsInvalid_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsInactive_ThenUnauthorizedIsReturned()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Inactive Key",
OrganizationId = 1,
IsActive = false,
CreatedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsExpired_ThenUnauthorizedIsReturned()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Expired Key",
OrganizationId = 1,
IsActive = true,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1),
CreatedAt = DateTimeOffset.UtcNow.AddDays(-10)
});
await db.SaveChangesAsync();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsValid_ThenOkIsReturned()
{
var rawKey = await SeedActiveApiKeyAsync(organizationId: 1);
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
}
using System.Net;
using System.Net.Http.Headers;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Data;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authentication;
[TestFixture]
public class ApiKeyAuthenticationHandlerTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
private async Task<string> SeedActiveApiKeyAsync(int organizationId = 1)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Test Key",
OrganizationId = organizationId,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync();
return rawKey;
}
[Test]
public async Task WhenAuthorizationHeaderIsAbsent_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenAuthorizationHeaderIsNotApiKeyScheme_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "not-a-jwt");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsInvalid_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", "Api-Key not-a-real-key");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsInactive_ThenUnauthorizedIsReturned()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Inactive Key",
OrganizationId = 1,
IsActive = false,
CreatedAt = DateTimeOffset.UtcNow
});
await db.SaveChangesAsync();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsExpired_ThenUnauthorizedIsReturned()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MicCheckDbContext>();
var rawKey = ApiKeyHasher.GenerateKey();
db.ApiKeys.Add(new ApiKey
{
Key = ApiKeyHasher.Hash(rawKey),
Prefix = rawKey[..8],
Name = "Expired Key",
OrganizationId = 1,
IsActive = true,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1),
CreatedAt = DateTimeOffset.UtcNow.AddDays(-10)
});
await db.SaveChangesAsync();
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenApiKeyIsValid_ThenOkIsReturned()
{
var rawKey = await SeedActiveApiKeyAsync(organizationId: 1);
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", $"Api-Key {rawKey}");
var response = await client.GetAsync("/api/v1/organisations/1/api-keys/");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
}

View File

@@ -1,185 +1,185 @@
using System.Net;
using System.Net.Http.Json;
using MicCheck.Api.Common.Security.Authorization;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
[TestFixture]
public class AuthEndpointsTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
[Test]
public async Task WhenRegisteringWithValidDetails_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenRegisteringWithDuplicateEmail_ThenConflictIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
}
[Test]
public async Task WhenLoginWithValidCredentials_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"bob@example.com", "SecurePass1!", "Bob", "Jones", "BobCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("bob@example.com", "SecurePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenLoginWithWrongPassword_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"carol@example.com", "CorrectPass1!", "Carol", "White", "CarolCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("carol@example.com", "WrongPassword"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithUnknownEmail_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("nobody@example.com", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithEmptyEmail_ThenBadRequestIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task WhenRefreshingWithValidToken_ThenOkIsReturnedWithNewTokens()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"dave@example.com", "SecurePass1!", "Dave", "Brown", "DaveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.EqualTo(registerBody.RefreshToken));
}
[Test]
public async Task WhenRefreshingWithInvalidToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest("not-a-valid-refresh-token"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenRefreshingWithRevokedToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"eve@example.com", "SecurePass1!", "Eve", "Davis", "EveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoggingOut_ThenNoContentIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"frank@example.com", "SecurePass1!", "Frank", "Green", "FrankCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
}
[Test]
public async Task WhenLoggingOutAndRefreshing_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"grace@example.com", "SecurePass1!", "Grace", "Black", "GraceCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
}
using System.Net;
using System.Net.Http.Json;
using MicCheck.Api.Common.Security.Authorization;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
[TestFixture]
public class AuthEndpointsTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
[Test]
public async Task WhenRegisteringWithValidDetails_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenRegisteringWithDuplicateEmail_ThenConflictIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
}
[Test]
public async Task WhenLoginWithValidCredentials_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"bob@example.com", "SecurePass1!", "Bob", "Jones", "BobCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("bob@example.com", "SecurePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenLoginWithWrongPassword_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"carol@example.com", "CorrectPass1!", "Carol", "White", "CarolCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("carol@example.com", "WrongPassword"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithUnknownEmail_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("nobody@example.com", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithEmptyEmail_ThenBadRequestIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task WhenRefreshingWithValidToken_ThenOkIsReturnedWithNewTokens()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"dave@example.com", "SecurePass1!", "Dave", "Brown", "DaveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.EqualTo(registerBody.RefreshToken));
}
[Test]
public async Task WhenRefreshingWithInvalidToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest("not-a-valid-refresh-token"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenRefreshingWithRevokedToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"eve@example.com", "SecurePass1!", "Eve", "Davis", "EveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoggingOut_ThenNoContentIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"frank@example.com", "SecurePass1!", "Frank", "Green", "FrankCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
}
[Test]
public async Task WhenLoggingOutAndRefreshing_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"grace@example.com", "SecurePass1!", "Grace", "Black", "GraceCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
}

View File

@@ -1,180 +1,180 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
[TestFixture]
public class ProjectPermissionRequirementHandlerTests
{
private MicCheckDbContext _db = null!;
private Mock<IHttpContextAccessor> _httpContextAccessor = null!;
private ProjectPermissionRequirementHandler _handler = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_httpContextAccessor = new Mock<IHttpContextAccessor>();
_handler = new ProjectPermissionRequirementHandler(_db, _httpContextAccessor.Object);
}
[TearDown]
public void TearDown() => _db.Dispose();
private static AuthorizationHandlerContext CreateContext(
ClaimsPrincipal user,
ProjectPermission permission = ProjectPermission.ViewProject)
{
var requirement = new ProjectPermissionRequirement(permission);
return new AuthorizationHandlerContext([requirement], user, null);
}
private static ClaimsPrincipal CreateUserWithClaims(params Claim[] claims)
{
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer"));
}
private void SetupRouteProjectId(int projectId)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues["projectId"] = projectId.ToString();
_httpContextAccessor.Setup(a => a.HttpContext).Returns(httpContext);
}
[Test]
public async Task WhenUserIsOrganizationAdmin_ThenRequirementSucceeds()
{
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"),
new Claim("OrganizationRole", "Admin"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserHasRequiredPermission_ThenRequirementSucceeds()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 1,
ProjectId = 10,
Permissions = [ProjectPermission.ViewProject]
});
await _db.SaveChangesAsync();
SetupRouteProjectId(10);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"));
var context = CreateContext(user, ProjectPermission.ViewProject);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserIsProjectAdmin_ThenAllPermissionsAreGranted()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 2,
ProjectId = 20,
IsAdmin = true,
Permissions = []
});
await _db.SaveChangesAsync();
SetupRouteProjectId(20);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "2"));
var context = CreateContext(user, ProjectPermission.DeleteFeature);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserDoesNotHaveRequiredPermission_ThenRequirementFails()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 3,
ProjectId = 30,
Permissions = [ProjectPermission.ViewProject]
});
await _db.SaveChangesAsync();
SetupRouteProjectId(30);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "3"));
var context = CreateContext(user, ProjectPermission.DeleteFeature);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenUserHasNoProjectPermissionRecord_ThenRequirementFails()
{
SetupRouteProjectId(99);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "5"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenUserIdClaimIsMissing_ThenRequirementFails()
{
var user = CreateUserWithClaims();
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenProjectIdIsNotInRoute_ThenRequirementFails()
{
_httpContextAccessor.Setup(a => a.HttpContext).Returns(new DefaultHttpContext());
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
}
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
[TestFixture]
public class ProjectPermissionRequirementHandlerTests
{
private MicCheckDbContext _db = null!;
private Mock<IHttpContextAccessor> _httpContextAccessor = null!;
private ProjectPermissionRequirementHandler _handler = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
_httpContextAccessor = new Mock<IHttpContextAccessor>();
_handler = new ProjectPermissionRequirementHandler(_db, _httpContextAccessor.Object);
}
[TearDown]
public void TearDown() => _db.Dispose();
private static AuthorizationHandlerContext CreateContext(
ClaimsPrincipal user,
ProjectPermission permission = ProjectPermission.ViewProject)
{
var requirement = new ProjectPermissionRequirement(permission);
return new AuthorizationHandlerContext([requirement], user, null);
}
private static ClaimsPrincipal CreateUserWithClaims(params Claim[] claims)
{
return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer"));
}
private void SetupRouteProjectId(int projectId)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.RouteValues["projectId"] = projectId.ToString();
_httpContextAccessor.Setup(a => a.HttpContext).Returns(httpContext);
}
[Test]
public async Task WhenUserIsOrganizationAdmin_ThenRequirementSucceeds()
{
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"),
new Claim("OrganizationRole", "Admin"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserHasRequiredPermission_ThenRequirementSucceeds()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 1,
ProjectId = 10,
Permissions = [ProjectPermission.ViewProject]
});
await _db.SaveChangesAsync();
SetupRouteProjectId(10);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"));
var context = CreateContext(user, ProjectPermission.ViewProject);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserIsProjectAdmin_ThenAllPermissionsAreGranted()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 2,
ProjectId = 20,
IsAdmin = true,
Permissions = []
});
await _db.SaveChangesAsync();
SetupRouteProjectId(20);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "2"));
var context = CreateContext(user, ProjectPermission.DeleteFeature);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.True);
}
[Test]
public async Task WhenUserDoesNotHaveRequiredPermission_ThenRequirementFails()
{
_db.UserProjectPermissions.Add(new UserProjectPermission
{
UserId = 3,
ProjectId = 30,
Permissions = [ProjectPermission.ViewProject]
});
await _db.SaveChangesAsync();
SetupRouteProjectId(30);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "3"));
var context = CreateContext(user, ProjectPermission.DeleteFeature);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenUserHasNoProjectPermissionRecord_ThenRequirementFails()
{
SetupRouteProjectId(99);
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "5"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenUserIdClaimIsMissing_ThenRequirementFails()
{
var user = CreateUserWithClaims();
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
[Test]
public async Task WhenProjectIdIsNotInRoute_ThenRequirementFails()
{
_httpContextAccessor.Setup(a => a.HttpContext).Returns(new DefaultHttpContext());
var user = CreateUserWithClaims(
new Claim(JwtRegisteredClaimNames.Sub, "1"));
var context = CreateContext(user);
await _handler.HandleAsync(context);
Assert.That(context.HasSucceeded, Is.False);
}
}

View File

@@ -1,103 +1,103 @@
using System.IdentityModel.Tokens.Jwt;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Organizations;
using MicCheck.Api.Users;
using Microsoft.Extensions.Configuration;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
[TestFixture]
public class TokenServiceTests
{
private IConfiguration _configuration = null!;
private TokenService _tokenService = null!;
[SetUp]
public void SetUp()
{
_configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Jwt:SecretKey"] = "test-secret-key-that-is-long-enough-32c",
["Jwt:Issuer"] = "MicCheck",
["Jwt:Audience"] = "MicCheck",
["Jwt:ExpiryMinutes"] = "60",
})
.Build();
_tokenService = new TokenService(_configuration);
}
private static User BuildUser(int id = 1, string email = "alice@example.com") =>
new()
{
Email = email,
FirstName = "Alice",
LastName = "Smith",
PasswordHash = "hashed",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
};
[Test]
public void WhenAUserIsProvided_ThenATokenIsReturned()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.Token, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenATokenIsGenerated_ThenItContainsTheUserEmailClaim()
{
var user = BuildUser(email: "bob@example.com");
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "bob@example.com"),
Is.True);
}
[Test]
public void WhenATokenIsGenerated_ThenItHasAFutureExpiry()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.ExpiresAt, Is.GreaterThan(DateTime.UtcNow));
}
[Test]
public void WhenATokenIsGenerated_ThenItHasTheConfiguredIssuer()
{
var result = _tokenService.GenerateToken(BuildUser());
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Issuer, Is.EqualTo("MicCheck"));
}
[Test]
public void WhenUserBelongsToOrganizations_ThenTokenContainsOrganizationClaims()
{
var user = BuildUser();
user.Organizations.Add(new OrganizationUser
{
OrganizationId = 42,
UserId = user.Id,
Role = OrganizationRole.Admin
});
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationId" && c.Value == "42"), Is.True);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationRole" && c.Value == "Admin"), Is.True);
}
}
using System.IdentityModel.Tokens.Jwt;
using MicCheck.Api.Common.Security.Authorization;
using MicCheck.Api.Organizations;
using MicCheck.Api.Users;
using Microsoft.Extensions.Configuration;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization;
[TestFixture]
public class TokenServiceTests
{
private IConfiguration _configuration = null!;
private TokenService _tokenService = null!;
[SetUp]
public void SetUp()
{
_configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Jwt:SecretKey"] = "test-secret-key-that-is-long-enough-32c",
["Jwt:Issuer"] = "MicCheck",
["Jwt:Audience"] = "MicCheck",
["Jwt:ExpiryMinutes"] = "60",
})
.Build();
_tokenService = new TokenService(_configuration);
}
private static User BuildUser(int id = 1, string email = "alice@example.com") =>
new()
{
Email = email,
FirstName = "Alice",
LastName = "Smith",
PasswordHash = "hashed",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
};
[Test]
public void WhenAUserIsProvided_ThenATokenIsReturned()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.Token, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenATokenIsGenerated_ThenItContainsTheUserEmailClaim()
{
var user = BuildUser(email: "bob@example.com");
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "bob@example.com"),
Is.True);
}
[Test]
public void WhenATokenIsGenerated_ThenItHasAFutureExpiry()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.ExpiresAt, Is.GreaterThan(DateTime.UtcNow));
}
[Test]
public void WhenATokenIsGenerated_ThenItHasTheConfiguredIssuer()
{
var result = _tokenService.GenerateToken(BuildUser());
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Issuer, Is.EqualTo("MicCheck"));
}
[Test]
public void WhenUserBelongsToOrganizations_ThenTokenContainsOrganizationClaims()
{
var user = BuildUser();
user.Organizations.Add(new OrganizationUser
{
OrganizationId = 42,
UserId = user.Id,
Role = OrganizationRole.Admin
});
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationId" && c.Value == "42"), Is.True);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationRole" && c.Value == "Admin"), Is.True);
}
}

View File

View File

@@ -1,224 +1,224 @@
using NUnit.Framework;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;
using MicCheck.Api.Identities;
using MicCheck.Api.Audit;
using MicCheck.Api.Webhooks;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Users;
using Microsoft.EntityFrameworkCore;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Data;
[TestFixture]
public class MicCheckDbContextTests
{
private MicCheckDbContext _db = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
}
[TearDown]
public void TearDown() => _db.Dispose();
[Test]
public async Task WhenAnOrganizationIsSaved_ThenItCanBeRetrievedById()
{
var organization = new Organization { Name = "Acme", CreatedAt = DateTimeOffset.UtcNow };
_db.Organizations.Add(organization);
await _db.SaveChangesAsync();
var retrieved = await _db.Organizations.FindAsync(organization.Id);
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved!.Name, Is.EqualTo("Acme"));
}
[Test]
public async Task WhenAProjectIsSaved_ThenItCanBeRetrievedByOrganization()
{
var organization = new Organization { Name = "Acme", CreatedAt = DateTimeOffset.UtcNow };
_db.Organizations.Add(organization);
await _db.SaveChangesAsync();
var project = new Project { Name = "My Project", OrganizationId = organization.Id, CreatedAt = DateTimeOffset.UtcNow };
_db.Projects.Add(project);
await _db.SaveChangesAsync();
var projects = await _db.Projects.Where(p => p.OrganizationId == organization.Id).ToListAsync();
Assert.That(projects, Has.Count.EqualTo(1));
Assert.That(projects[0].Name, Is.EqualTo("My Project"));
}
[Test]
public async Task WhenAnEnvironmentIsSaved_ThenItCanBeRetrievedByApiKey()
{
var project = new Project { Name = "Test", OrganizationId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Projects.Add(project);
await _db.SaveChangesAsync();
var env = new AppEnvironment { Name = "Production", ApiKey = "env-key-abc", ProjectId = project.Id, CreatedAt = DateTimeOffset.UtcNow };
_db.Environments.Add(env);
await _db.SaveChangesAsync();
var retrieved = await _db.Environments.FirstOrDefaultAsync(e => e.ApiKey == "env-key-abc");
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved!.Name, Is.EqualTo("Production"));
}
[Test]
public async Task WhenAFeatureIsSaved_ThenItCanBeRetrievedByProject()
{
var feature = new Feature { Name = "dark_mode", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Features.Add(feature);
await _db.SaveChangesAsync();
var features = await _db.Features.Where(f => f.ProjectId == 1).ToListAsync();
Assert.That(features, Has.Count.EqualTo(1));
Assert.That(features[0].Name, Is.EqualTo("dark_mode"));
}
[Test]
public async Task WhenAFeatureStateIsSaved_ThenItCanBeQueriedByEnvironment()
{
var state = new FeatureState
{
FeatureId = 1,
EnvironmentId = 1,
Enabled = true,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
};
_db.FeatureStates.Add(state);
await _db.SaveChangesAsync();
var states = await _db.FeatureStates.Where(fs => fs.EnvironmentId == 1).ToListAsync();
Assert.That(states, Has.Count.EqualTo(1));
Assert.That(states[0].Enabled, Is.True);
}
[Test]
public async Task WhenASegmentIsSaved_ThenItsRulesCanBeLoaded()
{
var segment = new Segment { Name = "Power Users", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Segments.Add(segment);
await _db.SaveChangesAsync();
var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All };
_db.SegmentRules.Add(rule);
await _db.SaveChangesAsync();
var loaded = await _db.Segments
.Include(s => s.Rules)
.FirstAsync(s => s.Id == segment.Id);
Assert.That(loaded.Rules, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenAnIdentityIsSaved_ThenItsTraitsCanBeLoaded()
{
var identity = new Identity { Identifier = "user-123", EnvironmentId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Identities.Add(identity);
await _db.SaveChangesAsync();
_db.IdentityTraits.Add(new IdentityTrait { IdentityId = identity.Id, Key = "plan", Value = "premium" });
await _db.SaveChangesAsync();
var loaded = await _db.Identities
.Include(i => i.Traits)
.FirstAsync(i => i.Id == identity.Id);
Assert.That(loaded.Traits, Has.Count.EqualTo(1));
Assert.That(loaded.Traits.First().Key, Is.EqualTo("plan"));
}
[Test]
public async Task WhenAnAuditLogIsSaved_ThenItCanBeFilteredByOrganization()
{
_db.AuditLogs.Add(new AuditLog
{
ResourceType = "Feature",
ResourceId = "1",
Action = "Created",
OrganizationId = 42,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var logs = await _db.AuditLogs.Where(a => a.OrganizationId == 42).ToListAsync();
Assert.That(logs, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenAWebhookIsSaved_ThenItCanBeFilteredByEnvironment()
{
_db.Webhooks.Add(new Webhook
{
Url = "https://example.com/hook",
Scope = WebhookScope.Environment,
EnvironmentId = 5,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var webhooks = await _db.Webhooks.Where(w => w.EnvironmentId == 5).ToListAsync();
Assert.That(webhooks, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenAnApiKeyIsSaved_ThenItCanBeRetrievedByHashedKey()
{
_db.ApiKeys.Add(new ApiKey
{
Key = "hashed-value-xyz",
Prefix = "hashed-va",
Name = "CI Key",
OrganizationId = 1,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var key = await _db.ApiKeys.FirstOrDefaultAsync(k => k.Key == "hashed-value-xyz");
Assert.That(key, Is.Not.Null);
Assert.That(key!.IsActive, Is.True);
}
[Test]
public async Task WhenAUserIsSaved_ThenItCanBeRetrievedByEmail()
{
_db.Users.Add(new User
{
Email = "alice@example.com",
PasswordHash = "hashed",
FirstName = "Alice",
LastName = "Smith",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == "alice@example.com");
Assert.That(user, Is.Not.Null);
Assert.That(user!.FirstName, Is.EqualTo("Alice"));
}
}
using NUnit.Framework;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
using MicCheck.Api.Segments;
using MicCheck.Api.Identities;
using MicCheck.Api.Audit;
using MicCheck.Api.Webhooks;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Users;
using Microsoft.EntityFrameworkCore;
using AppEnvironment = MicCheck.Api.Environments.Environment;
namespace MicCheck.Api.Tests.Unit.Data;
[TestFixture]
public class MicCheckDbContextTests
{
private MicCheckDbContext _db = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new MicCheckDbContext(options);
}
[TearDown]
public void TearDown() => _db.Dispose();
[Test]
public async Task WhenAnOrganizationIsSaved_ThenItCanBeRetrievedById()
{
var organization = new Organization { Name = "Acme", CreatedAt = DateTimeOffset.UtcNow };
_db.Organizations.Add(organization);
await _db.SaveChangesAsync();
var retrieved = await _db.Organizations.FindAsync(organization.Id);
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved!.Name, Is.EqualTo("Acme"));
}
[Test]
public async Task WhenAProjectIsSaved_ThenItCanBeRetrievedByOrganization()
{
var organization = new Organization { Name = "Acme", CreatedAt = DateTimeOffset.UtcNow };
_db.Organizations.Add(organization);
await _db.SaveChangesAsync();
var project = new Project { Name = "My Project", OrganizationId = organization.Id, CreatedAt = DateTimeOffset.UtcNow };
_db.Projects.Add(project);
await _db.SaveChangesAsync();
var projects = await _db.Projects.Where(p => p.OrganizationId == organization.Id).ToListAsync();
Assert.That(projects, Has.Count.EqualTo(1));
Assert.That(projects[0].Name, Is.EqualTo("My Project"));
}
[Test]
public async Task WhenAnEnvironmentIsSaved_ThenItCanBeRetrievedByApiKey()
{
var project = new Project { Name = "Test", OrganizationId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Projects.Add(project);
await _db.SaveChangesAsync();
var env = new AppEnvironment { Name = "Production", ApiKey = "env-key-abc", ProjectId = project.Id, CreatedAt = DateTimeOffset.UtcNow };
_db.Environments.Add(env);
await _db.SaveChangesAsync();
var retrieved = await _db.Environments.FirstOrDefaultAsync(e => e.ApiKey == "env-key-abc");
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved!.Name, Is.EqualTo("Production"));
}
[Test]
public async Task WhenAFeatureIsSaved_ThenItCanBeRetrievedByProject()
{
var feature = new Feature { Name = "dark_mode", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Features.Add(feature);
await _db.SaveChangesAsync();
var features = await _db.Features.Where(f => f.ProjectId == 1).ToListAsync();
Assert.That(features, Has.Count.EqualTo(1));
Assert.That(features[0].Name, Is.EqualTo("dark_mode"));
}
[Test]
public async Task WhenAFeatureStateIsSaved_ThenItCanBeQueriedByEnvironment()
{
var state = new FeatureState
{
FeatureId = 1,
EnvironmentId = 1,
Enabled = true,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
};
_db.FeatureStates.Add(state);
await _db.SaveChangesAsync();
var states = await _db.FeatureStates.Where(fs => fs.EnvironmentId == 1).ToListAsync();
Assert.That(states, Has.Count.EqualTo(1));
Assert.That(states[0].Enabled, Is.True);
}
[Test]
public async Task WhenASegmentIsSaved_ThenItsRulesCanBeLoaded()
{
var segment = new Segment { Name = "Power Users", ProjectId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Segments.Add(segment);
await _db.SaveChangesAsync();
var rule = new SegmentRule { SegmentId = segment.Id, Type = SegmentRuleType.All };
_db.SegmentRules.Add(rule);
await _db.SaveChangesAsync();
var loaded = await _db.Segments
.Include(s => s.Rules)
.FirstAsync(s => s.Id == segment.Id);
Assert.That(loaded.Rules, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenAnIdentityIsSaved_ThenItsTraitsCanBeLoaded()
{
var identity = new Identity { Identifier = "user-123", EnvironmentId = 1, CreatedAt = DateTimeOffset.UtcNow };
_db.Identities.Add(identity);
await _db.SaveChangesAsync();
_db.IdentityTraits.Add(new IdentityTrait { IdentityId = identity.Id, Key = "plan", Value = "premium" });
await _db.SaveChangesAsync();
var loaded = await _db.Identities
.Include(i => i.Traits)
.FirstAsync(i => i.Id == identity.Id);
Assert.That(loaded.Traits, Has.Count.EqualTo(1));
Assert.That(loaded.Traits.First().Key, Is.EqualTo("plan"));
}
[Test]
public async Task WhenAnAuditLogIsSaved_ThenItCanBeFilteredByOrganization()
{
_db.AuditLogs.Add(new AuditLog
{
ResourceType = "Feature",
ResourceId = "1",
Action = "Created",
OrganizationId = 42,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var logs = await _db.AuditLogs.Where(a => a.OrganizationId == 42).ToListAsync();
Assert.That(logs, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenAWebhookIsSaved_ThenItCanBeFilteredByEnvironment()
{
_db.Webhooks.Add(new Webhook
{
Url = "https://example.com/hook",
Scope = WebhookScope.Environment,
EnvironmentId = 5,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var webhooks = await _db.Webhooks.Where(w => w.EnvironmentId == 5).ToListAsync();
Assert.That(webhooks, Has.Count.EqualTo(1));
}
[Test]
public async Task WhenAnApiKeyIsSaved_ThenItCanBeRetrievedByHashedKey()
{
_db.ApiKeys.Add(new ApiKey
{
Key = "hashed-value-xyz",
Prefix = "hashed-va",
Name = "CI Key",
OrganizationId = 1,
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var key = await _db.ApiKeys.FirstOrDefaultAsync(k => k.Key == "hashed-value-xyz");
Assert.That(key, Is.Not.Null);
Assert.That(key!.IsActive, Is.True);
}
[Test]
public async Task WhenAUserIsSaved_ThenItCanBeRetrievedByEmail()
{
_db.Users.Add(new User
{
Email = "alice@example.com",
PasswordHash = "hashed",
FirstName = "Alice",
LastName = "Smith",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
});
await _db.SaveChangesAsync();
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == "alice@example.com");
Assert.That(user, Is.Not.Null);
Assert.That(user!.FirstName, Is.EqualTo("Alice"));
}
}

View File

View File

View File

View File

View File

View File

View File

View File

@@ -1,70 +1,70 @@
using NUnit.Framework;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
namespace MicCheck.Api.Tests.Unit.Organizations;
[TestFixture]
public class OrganizationTests
{
[Test]
public void WhenAnOrganizationIsCreated_ThenCollectionsAreInitializedEmpty()
{
var organization = new Organization { Name = "Acme" };
Assert.That(organization.Projects, Is.Empty);
Assert.That(organization.Members, Is.Empty);
Assert.That(organization.ApiKeys, Is.Empty);
}
[Test]
public void WhenAnOrganizationIsCreated_ThenNameIsSet()
{
var organization = new Organization { Name = "Acme" };
Assert.That(organization.Name, Is.EqualTo("Acme"));
}
[Test]
public void WhenAProjectIsAddedToAnOrganization_ThenItAppearsInTheProjectsCollection()
{
var organization = new Organization { Name = "Acme" };
var project = new Project { Name = "My Project", OrganizationId = organization.Id };
organization.Projects.Add(project);
Assert.That(organization.Projects, Has.Count.EqualTo(1));
}
[Test]
public void WhenAnApiKeyIsAddedToAnOrganization_ThenItAppearsInTheApiKeysCollection()
{
var organization = new Organization { Name = "Acme" };
var apiKey = new ApiKey { Key = "hashed-key", Prefix = "abc12345", Name = "CI Key", OrganizationId = organization.Id };
organization.ApiKeys.Add(apiKey);
Assert.That(organization.ApiKeys, Has.Count.EqualTo(1));
}
}
[TestFixture]
public class OrganizationUserTests
{
[Test]
public void WhenAnOrganizationUserIsCreated_ThenRoleDefaultsToUser()
{
var member = new OrganizationUser { OrganizationId = 1, UserId = 1 };
Assert.That(member.Role, Is.EqualTo(OrganizationRole.User));
}
[Test]
public void WhenAnOrganizationUserRoleIsSetToAdmin_ThenRoleIsAdmin()
{
var member = new OrganizationUser { OrganizationId = 1, UserId = 1, Role = OrganizationRole.Admin };
Assert.That(member.Role, Is.EqualTo(OrganizationRole.Admin));
}
}
using NUnit.Framework;
using MicCheck.Api.Common.Security.ApiKeys;
using MicCheck.Api.Organizations;
using MicCheck.Api.Projects;
namespace MicCheck.Api.Tests.Unit.Organizations;
[TestFixture]
public class OrganizationTests
{
[Test]
public void WhenAnOrganizationIsCreated_ThenCollectionsAreInitializedEmpty()
{
var organization = new Organization { Name = "Acme" };
Assert.That(organization.Projects, Is.Empty);
Assert.That(organization.Members, Is.Empty);
Assert.That(organization.ApiKeys, Is.Empty);
}
[Test]
public void WhenAnOrganizationIsCreated_ThenNameIsSet()
{
var organization = new Organization { Name = "Acme" };
Assert.That(organization.Name, Is.EqualTo("Acme"));
}
[Test]
public void WhenAProjectIsAddedToAnOrganization_ThenItAppearsInTheProjectsCollection()
{
var organization = new Organization { Name = "Acme" };
var project = new Project { Name = "My Project", OrganizationId = organization.Id };
organization.Projects.Add(project);
Assert.That(organization.Projects, Has.Count.EqualTo(1));
}
[Test]
public void WhenAnApiKeyIsAddedToAnOrganization_ThenItAppearsInTheApiKeysCollection()
{
var organization = new Organization { Name = "Acme" };
var apiKey = new ApiKey { Key = "hashed-key", Prefix = "abc12345", Name = "CI Key", OrganizationId = organization.Id };
organization.ApiKeys.Add(apiKey);
Assert.That(organization.ApiKeys, Has.Count.EqualTo(1));
}
}
[TestFixture]
public class OrganizationUserTests
{
[Test]
public void WhenAnOrganizationUserIsCreated_ThenRoleDefaultsToUser()
{
var member = new OrganizationUser { OrganizationId = 1, UserId = 1 };
Assert.That(member.Role, Is.EqualTo(OrganizationRole.User));
}
[Test]
public void WhenAnOrganizationUserRoleIsSetToAdmin_ThenRoleIsAdmin()
{
var member = new OrganizationUser { OrganizationId = 1, UserId = 1, Role = OrganizationRole.Admin };
Assert.That(member.Role, Is.EqualTo(OrganizationRole.Admin));
}
}

View File

View File

View File

View File

0
tests/api/MicCheck.Api.Tests.Unit/Users/UserTests.cs Normal file → Executable file
View File

View File

View File

View File