Files
git.stella-ops.org/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Tests/Endpoints/NotifyApiEndpointsTests.cs

350 lines
12 KiB
C#

extern alias webservice;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Notifier.WebService.Contracts;
using StellaOps.Notifier.Worker.Storage;
using StellaOps.Notify.Models;
using WebProgram = webservice::Program;
using Xunit;
namespace StellaOps.Notifier.Tests.Endpoints;
public sealed class NotifyApiEndpointsTests : IClassFixture<WebApplicationFactory<WebProgram>>
{
private readonly HttpClient _client;
private readonly InMemoryRuleRepository _ruleRepository;
private readonly InMemoryTemplateRepository _templateRepository;
private readonly WebApplicationFactory<WebProgram> _factory;
public NotifyApiEndpointsTests(WebApplicationFactory<WebProgram> factory)
{
_ruleRepository = new InMemoryRuleRepository();
_templateRepository = new InMemoryTemplateRepository();
var customFactory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton<INotifyRuleRepository>(_ruleRepository);
services.AddSingleton<INotifyTemplateRepository>(_templateRepository);
});
builder.UseSetting("Environment", "Testing");
});
_factory = customFactory;
_client = customFactory.CreateClient();
_client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant");
}
#region Rules API Tests
[Fact]
public async Task GetRules_ReturnsEmptyList_WhenNoRules()
{
// Act
var response = await _client.GetAsync("/api/v2/notify/rules", CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var rules = await response.Content.ReadFromJsonAsync<List<RuleResponse>>(cancellationToken: CancellationToken.None);
Assert.NotNull(rules);
Assert.Empty(rules);
}
[Fact]
public async Task CreateRule_ReturnsCreated_WithValidRequest()
{
// Arrange
var request = new RuleCreateRequest
{
RuleId = "rule-001",
Name = "Test Rule",
Description = "Test description",
Enabled = true,
Match = new RuleMatchRequest
{
EventKinds = ["pack.approval.granted"],
Labels = ["env=prod"]
},
Actions =
[
new RuleActionRequest
{
ActionId = "action-001",
Channel = "slack:alerts",
Template = "tmpl-slack-001"
}
]
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/notify/rules", request, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var rule = await response.Content.ReadFromJsonAsync<RuleResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(rule);
Assert.Equal("rule-001", rule.RuleId);
Assert.Equal("Test Rule", rule.Name);
}
[Fact]
public async Task GetRule_ReturnsRule_WhenExists()
{
// Arrange
var rule = NotifyRule.Create(
ruleId: "rule-get-001",
tenantId: "test-tenant",
name: "Existing Rule",
match: NotifyRuleMatch.Create(eventKinds: ["test.event"]),
actions: new[]
{
NotifyRuleAction.Create(
actionId: "action-001",
channel: "slack:alerts",
template: "tmpl-001")
});
await _ruleRepository.UpsertAsync(rule, CancellationToken.None);
// Act
var response = await _client.GetAsync("/api/v2/notify/rules/rule-get-001", CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<RuleResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(result);
Assert.Equal("rule-get-001", result.RuleId);
}
[Fact]
public async Task GetRule_ReturnsNotFound_WhenNotExists()
{
// Act
var response = await _client.GetAsync("/api/v2/notify/rules/nonexistent", CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task DeleteRule_ReturnsNoContent_WhenExists()
{
// Arrange
var rule = NotifyRule.Create(
ruleId: "rule-delete-001",
tenantId: "test-tenant",
name: "Delete Me",
match: NotifyRuleMatch.Create(),
actions: new[]
{
NotifyRuleAction.Create(
actionId: "action-001",
channel: "slack:alerts",
template: "tmpl-001")
});
await _ruleRepository.UpsertAsync(rule, CancellationToken.None);
// Act
var response = await _client.DeleteAsync("/api/v2/notify/rules/rule-delete-001", CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
#endregion
#region Templates API Tests
[Fact]
public async Task GetTemplates_ReturnsEmptyList_WhenNoTemplates()
{
// Act
var response = await _client.GetAsync("/api/v2/notify/templates", CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var templates = await response.Content.ReadFromJsonAsync<List<TemplateResponse>>(cancellationToken: CancellationToken.None);
Assert.NotNull(templates);
}
[Fact]
public async Task PreviewTemplate_ReturnsRenderedContent()
{
// Arrange
var request = new TemplatePreviewRequest
{
TemplateBody = "Hello {{name}}, you have {{count}} messages.",
SamplePayload = JsonSerializer.SerializeToNode(new { name = "World", count = 5 }) as System.Text.Json.Nodes.JsonObject
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/notify/templates/preview", request, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var preview = await response.Content.ReadFromJsonAsync<TemplatePreviewResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(preview);
Assert.Contains("Hello World", preview.RenderedBody);
Assert.Contains("5", preview.RenderedBody);
}
[Fact]
public async Task ValidateTemplate_ReturnsValid_ForCorrectTemplate()
{
// Arrange
var request = new TemplatePreviewRequest
{
TemplateBody = "Hello {{name}}!"
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/notify/templates/validate", request, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
Assert.True(result.GetProperty("isValid").GetBoolean());
}
[Fact]
public async Task ValidateTemplate_ReturnsInvalid_ForBrokenTemplate()
{
// Arrange
var request = new TemplatePreviewRequest
{
TemplateBody = "Hello {{name} - missing closing brace"
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/notify/templates/validate", request, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
Assert.False(result.GetProperty("isValid").GetBoolean());
}
#endregion
#region Incidents API Tests
[Fact]
public async Task GetIncidents_ReturnsIncidentList()
{
// Act
var response = await _client.GetAsync("/api/v2/notify/incidents", CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<IncidentListResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(result);
Assert.NotNull(result.Incidents);
}
[Fact]
public async Task AckIncident_ReturnsNoContent()
{
// Arrange
var request = new IncidentAckRequest
{
Actor = "test-user",
Comment = "Acknowledged"
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/notify/incidents/incident-001/ack", request, cancellationToken: CancellationToken.None);
// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
#endregion
#region Error Handling Tests
[Fact]
public async Task AllEndpoints_ReturnBadRequest_WhenTenantMissing()
{
// Arrange
var clientWithoutTenant = _factory.CreateClient();
// Act
var response = await clientWithoutTenant.GetAsync("/api/v2/notify/rules", CancellationToken.None);
// Assert - should fail without tenant header
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
#endregion
#region Test Repositories
private sealed class InMemoryRuleRepository : INotifyRuleRepository
{
private readonly Dictionary<string, NotifyRule> _rules = new();
public Task<NotifyRule> UpsertAsync(NotifyRule rule, CancellationToken cancellationToken = default)
{
var key = $"{rule.TenantId}:{rule.RuleId}";
_rules[key] = rule;
return Task.FromResult(rule);
}
public Task<NotifyRule?> GetAsync(string tenantId, string ruleId, CancellationToken cancellationToken = default)
{
var key = $"{tenantId}:{ruleId}";
return Task.FromResult(_rules.GetValueOrDefault(key));
}
public Task<IReadOnlyList<NotifyRule>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
{
var result = _rules.Values.Where(r => r.TenantId == tenantId).ToList();
return Task.FromResult<IReadOnlyList<NotifyRule>>(result);
}
public Task<bool> DeleteAsync(string tenantId, string ruleId, CancellationToken cancellationToken = default)
{
var key = $"{tenantId}:{ruleId}";
var removed = _rules.Remove(key);
return Task.FromResult(removed);
}
}
private sealed class InMemoryTemplateRepository : INotifyTemplateRepository
{
private readonly Dictionary<string, NotifyTemplate> _templates = new();
public Task<NotifyTemplate> UpsertAsync(NotifyTemplate template, CancellationToken cancellationToken = default)
{
var key = $"{template.TenantId}:{template.TemplateId}";
_templates[key] = template;
return Task.FromResult(template);
}
public Task<NotifyTemplate?> GetAsync(string tenantId, string templateId, CancellationToken cancellationToken = default)
{
var key = $"{tenantId}:{templateId}";
return Task.FromResult(_templates.GetValueOrDefault(key));
}
public Task<IReadOnlyList<NotifyTemplate>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
{
var result = _templates.Values.Where(t => t.TenantId == tenantId).ToList();
return Task.FromResult<IReadOnlyList<NotifyTemplate>>(result);
}
public Task<bool> DeleteAsync(string tenantId, string templateId, CancellationToken cancellationToken = default)
{
var key = $"{tenantId}:{templateId}";
var removed = _templates.Remove(key);
return Task.FromResult(removed);
}
}
#endregion
}