Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
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.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notifier.Tests.Endpoints;
|
||||
|
||||
public sealed class NotifyApiEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly InMemoryRuleRepository _ruleRepository;
|
||||
private readonly InMemoryTemplateRepository _templateRepository;
|
||||
|
||||
public NotifyApiEndpointsTests(WebApplicationFactory<Program> 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");
|
||||
});
|
||||
|
||||
_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");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var rules = await response.Content.ReadFromJsonAsync<List<RuleResponse>>();
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
var rule = await response.Content.ReadFromJsonAsync<RuleResponse>();
|
||||
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: []);
|
||||
await _ruleRepository.UpsertAsync(rule);
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v2/notify/rules/rule-get-001");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<RuleResponse>();
|
||||
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");
|
||||
|
||||
// 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: []);
|
||||
await _ruleRepository.UpsertAsync(rule);
|
||||
|
||||
// Act
|
||||
var response = await _client.DeleteAsync("/api/v2/notify/rules/rule-delete-001");
|
||||
|
||||
// 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");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var templates = await response.Content.ReadFromJsonAsync<List<TemplateResponse>>();
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var preview = await response.Content.ReadFromJsonAsync<TemplatePreviewResponse>();
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
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");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<IncidentListResponse>();
|
||||
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);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact]
|
||||
public async Task AllEndpoints_ReturnBadRequest_WhenTenantMissing()
|
||||
{
|
||||
// Arrange
|
||||
var clientWithoutTenant = new HttpClient { BaseAddress = _client.BaseAddress };
|
||||
|
||||
// Act
|
||||
var response = await clientWithoutTenant.GetAsync("/api/v2/notify/rules");
|
||||
|
||||
// Assert - should fail without tenant header
|
||||
// Note: actual behavior depends on endpoint implementation
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Repositories
|
||||
|
||||
private sealed class InMemoryRuleRepository : INotifyRuleRepository
|
||||
{
|
||||
private readonly Dictionary<string, NotifyRule> _rules = new();
|
||||
|
||||
public Task UpsertAsync(NotifyRule rule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{rule.TenantId}:{rule.RuleId}";
|
||||
_rules[key] = rule;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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 DeleteAsync(string tenantId, string ruleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{tenantId}:{ruleId}";
|
||||
_rules.Remove(key);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryTemplateRepository : INotifyTemplateRepository
|
||||
{
|
||||
private readonly Dictionary<string, NotifyTemplate> _templates = new();
|
||||
|
||||
public Task UpsertAsync(NotifyTemplate template, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{template.TenantId}:{template.TemplateId}";
|
||||
_templates[key] = template;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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 DeleteAsync(string tenantId, string templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"{tenantId}:{templateId}";
|
||||
_templates.Remove(key);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user