up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,349 +1,349 @@
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");
// 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: new[]
{
NotifyRuleAction.Create(
actionId: "action-001",
channel: "slack:alerts",
template: "tmpl-001")
});
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: new[]
{
NotifyRuleAction.Create(
actionId: "action-001",
channel: "slack:alerts",
template: "tmpl-001")
});
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 = _factory.CreateClient();
// Act
var response = await clientWithoutTenant.GetAsync("/api/v2/notify/rules");
// 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
}
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");
// 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: new[]
{
NotifyRuleAction.Create(
actionId: "action-001",
channel: "slack:alerts",
template: "tmpl-001")
});
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: new[]
{
NotifyRuleAction.Create(
actionId: "action-001",
channel: "slack:alerts",
template: "tmpl-001")
});
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 = _factory.CreateClient();
// Act
var response = await clientWithoutTenant.GetAsync("/api/v2/notify/rules");
// 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
}