using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Types.InboundApi;
namespace ScadaLink.InboundAPI.Tests;
///
/// ConfigurationDatabase-012: must authenticate by
/// hashing the presented candidate with the same HMAC-SHA256 pepper used at
/// creation, then comparing against the stored — never
/// against a plaintext key. The comparison stays constant-time.
///
public class ApiKeyHashValidationTests
{
private const string Pepper = "a-sufficiently-long-server-side-pepper-value";
private readonly IInboundApiRepository _repository = Substitute.For();
private static ApiKey StoredKey(ApiKeyHasher hasher, string liveKey, int id = 1, bool enabled = true)
{
var key = ApiKey.FromHash("MES-Production", hasher.Hash(liveKey));
key.Id = id;
key.IsEnabled = enabled;
return key;
}
[Fact]
public async Task ValidateAsync_WithPepperedHasher_AcceptsKeyHashedWithSamePepper()
{
var hasher = new ApiKeyHasher(Pepper);
var stored = StoredKey(hasher, "live-secret-key");
var method = new ApiMethod("ingest", "return 1;") { Id = 10 };
_repository.GetAllApiKeysAsync().Returns(new List { stored });
_repository.GetMethodByNameAsync("ingest").Returns(method);
_repository.GetApprovedKeysForMethodAsync(10).Returns(new List { stored });
var validator = new ApiKeyValidator(_repository, hasher);
var result = await validator.ValidateAsync("live-secret-key", "ingest");
Assert.True(result.IsValid);
Assert.Equal(200, result.StatusCode);
}
[Fact]
public async Task ValidateAsync_WrongKey_FailsEvenWhenItHashesToSomethingNonNull()
{
var hasher = new ApiKeyHasher(Pepper);
var stored = StoredKey(hasher, "the-real-key");
_repository.GetAllApiKeysAsync().Returns(new List { stored });
var validator = new ApiKeyValidator(_repository, hasher);
var result = await validator.ValidateAsync("a-wrong-key", "ingest");
Assert.False(result.IsValid);
Assert.Equal(401, result.StatusCode);
}
[Fact]
public async Task ValidateAsync_StoredHashIsNotThePlaintextKey()
{
// Sanity guard: the value the validator compares against must be a hash, not
// the live secret — a DB dump must not yield a usable credential.
var hasher = new ApiKeyHasher(Pepper);
var stored = StoredKey(hasher, "live-secret-key");
Assert.NotEqual("live-secret-key", stored.KeyHash);
_repository.GetAllApiKeysAsync().Returns(new List { stored });
var validator = new ApiKeyValidator(_repository, hasher);
// Presenting the stored hash itself must NOT authenticate — only the live key does.
var result = await validator.ValidateAsync(stored.KeyHash, "ingest");
Assert.False(result.IsValid);
}
[Fact]
public async Task ValidateAsync_KeyHashedUnderADifferentPepper_DoesNotAuthenticate()
{
var creationHasher = new ApiKeyHasher(Pepper);
var stored = StoredKey(creationHasher, "live-secret-key");
_repository.GetAllApiKeysAsync().Returns(new List { stored });
// A validator running with a different pepper cannot recognise the key.
var otherHasher = new ApiKeyHasher("a-totally-different-server-side-pepper-val");
var validator = new ApiKeyValidator(_repository, otherHasher);
var result = await validator.ValidateAsync("live-secret-key", "ingest");
Assert.False(result.IsValid);
}
}