350 lines
12 KiB
C#
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
|
|
}
|