tests/IntegrationTests/IngestionTests.cs
Rama main · réplica de lectura del repositorio original.
using Xunit;
using System.Net;
using AwesomeAssertions;
using Aptabase.IntegrationTests.Clients;
using Aptabase.Features.Ingestion.Buffer;
using Aptabase.Data;
using Dapper;
namespace Aptabase.IntegrationTests;
[Collection("Integration Tests")]
public class IngestionTests
{
private readonly IntegrationTestsFixture _fixture;
private readonly EventBackgroundWritter _eventWritter;
public IngestionTests(IntegrationTestsFixture fixture)
{
_fixture = fixture;
_eventWritter = _fixture.GetHostedService<EventBackgroundWritter>();
}
[Fact]
public async Task Can_Ingest_For_Known_App()
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
var code = await client.TrackEvent(DateTime.UtcNow.AddHours(-10), "Button Clicked", null);
code.Should().Be(HttpStatusCode.OK);
await _eventWritter.FlushEvents();
var count = await _fixture.UserA.CountEvents(app.Id, "24h");
count.Should().Be(1);
}
[Fact]
public async Task Can_Ingest_Future_Events()
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
var code = await client.TrackEvent(DateTime.UtcNow.AddYears(10), "Button Clicked", null);
code.Should().Be(HttpStatusCode.OK);
await _eventWritter.FlushEvents();
var count = await _fixture.UserA.CountEvents(app.Id, "24h");
count.Should().Be(1);
}
[Fact]
public async Task Can_Ingest_Multiple_Events()
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
var code = await client.TrackEvents([
(DateTime.UtcNow, "App Started", null),
(DateTime.UtcNow, "Menu Opened", null),
(DateTime.UtcNow, "Button Clicked", null),
]);
code.Should().Be(HttpStatusCode.OK);
await _eventWritter.FlushEvents();
var count = await _fixture.UserA.CountEvents(app.Id, "24h");
count.Should().Be(3);
}
[Fact]
public async Task Should_drop_events_if_invalid()
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
var code = await client.TrackEvents([
(DateTime.UtcNow, "App Started", null),
(DateTime.UtcNow, "Menu Opened", null),
(DateTime.UtcNow, "Button Clicked", "INVALID PROPS"),
]);
code.Should().Be(HttpStatusCode.OK);
await _eventWritter.FlushEvents();
var count = await _fixture.UserA.CountEvents(app.Id, "24h");
count.Should().Be(2);
}
[Fact]
public async Task Cant_Ingest_More_Than_25_Events_In_Batch()
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
var code = await client.TrackEvents(Enumerable.Range(1, 26).Select(i => (DateTime.UtcNow, "Button Clicked", (object?)null)));
code.Should().Be(HttpStatusCode.BadRequest);
await _eventWritter.FlushEvents();
var count = await _fixture.UserA.CountEvents(app.Id, "24h");
count.Should().Be(0);
}
[Fact]
public async Task Cant_Ingest_Unknown_AppKey()
{
// Dropped, but answered with success so SDKs that retry every non-2xx response don't re-send it forever
var client = new IngestionClient(_fixture.CreateClient(), "THIS-DOES-NOT-EXIST");
var code = await client.TrackEvent(DateTime.UtcNow, "Button Clicked", null);
code.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task Drops_Events_For_Locked_Account_With_Success_Status()
{
// A dedicated owner, because locking the shared fixture users would affect other tests
var owner = new AccountClient(_fixture.CreateClient());
await owner.CreateAccount("Locked Owner", $"locked.owner.{Guid.NewGuid()}@got.com");
var app = await owner.CreateApp(Guid.NewGuid().ToString());
var db = _fixture.GetService<IDbContext>();
await SetOwnerLockReason(db, app.Id, 'O');
// SDKs that retry every non-2xx response must see success, or they re-send these events forever
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
(await client.TrackEvent(DateTime.UtcNow, "Button Clicked", null)).Should().Be(HttpStatusCode.OK);
(await client.TrackEvents([
(DateTime.UtcNow, "App Started", null),
(DateTime.UtcNow, "Menu Opened", null),
])).Should().Be(HttpStatusCode.OK);
await _eventWritter.FlushEvents();
// Stats are hidden while the owner is locked, so unlock to check nothing was recorded
await SetOwnerLockReason(db, app.Id, null);
var count = await owner.CountEvents(app.Id, "24h");
count.Should().Be(0);
}
private static Task SetOwnerLockReason(IDbContext db, string appId, char? lockReason)
{
return db.Connection.ExecuteAsync(
"UPDATE users SET lock_reason = @lockReason WHERE id = (SELECT owner_id FROM apps WHERE id = @appId)",
new { appId, lockReason });
}
[Theory]
[InlineData("/api/v0/event")]
[InlineData("/api/v0/events")]
public async Task Rejects_Unknown_AppKey_Before_Parsing_Body(string path)
{
// A malformed body would be a 400 if it were parsed, so a 200 proves the app key check ran first
var client = new IngestionClient(_fixture.CreateClient(), "THIS-DOES-NOT-EXIST");
var code = await client.PostRaw(path, "{ this is not json");
code.Should().Be(HttpStatusCode.OK);
}
public static IEnumerable<object[]> ValidProps =>
[
["{}"],
["{\"name\":\"Bob\"}"],
["{\"name\":\"Bob\", \"age\":20}"],
[new { }],
[new { age = 20 }],
[new { name = "Bob", age = 20 }],
[new { name = "Bob", valid = true }],
[new { name = "Bob", surname = (object?)null }],
[new { name = "Bob", age = 20, list = new int[]{1,2,3} }],
["{\"name\":\"Bob\", \"age\":20, \"list\": [1,2,3]}"],
];
[Theory, MemberData(nameof(ValidProps))]
public async Task Cant_Ingest_With_Valid_Props(object props)
{
var appA = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), appA.AppKey);
var code = await client.TrackEvent(DateTime.UtcNow, "Button Clicked", props);
code.Should().Be(HttpStatusCode.OK);
}
public static IEnumerable<object[]> InvalidProps =>
[
[1],
["Something"],
["[1,2,3]"],
[""],
[new string[]{"A", "B", "C"}],
];
[Theory, MemberData(nameof(InvalidProps))]
public async Task Cant_Ingest_With_Invalid_Props(object props)
{
var appA = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), appA.AppKey);
var code = await client.TrackEvent(DateTime.UtcNow, "Button Clicked", props);
code.Should().Be(HttpStatusCode.BadRequest);
}
public static IEnumerable<object[]> ValidSessionIds =>
[
["8ee47a56-a457-4513-a65f-2b8c3065eb95"],
["0ef724ce-7d46-4bde-9e0f-69303ef3f2af"],
["1234-5678-9012-3456"],
[IngestionClient.NewSessionId().ToString()],
];
[Theory, MemberData(nameof(ValidSessionIds))]
public async Task Can_Ingest_Valid_SessionId(string sessionId)
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
client.SetSessionId(sessionId);
var code = await client.TrackEvent(DateTime.UtcNow, "Button Clicked", null);
code.Should().Be(HttpStatusCode.OK);
}
[Theory]
[InlineData("")]
[InlineData("1234567890123456789012345678901234567890")]
[InlineData(null)]
public async Task Cant_Ingest_Invalid_SessionId(string? sessionId)
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey);
client.SetSessionId(sessionId);
var code = await client.TrackEvent(DateTime.UtcNow, "Button Clicked", null);
code.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task Cant_Read_Stats_From_Other_Users()
{
var appA = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), appA.AppKey);
await client.TrackEvent(DateTime.UtcNow, "Button Clicked", null);
var responseA = await _fixture.UserA.GetKeyMetrics(appA.Id, "24h");
responseA.StatusCode.Should().Be(HttpStatusCode.OK);
var responseB = await _fixture.UserB.GetKeyMetrics(appA.Id, "24h");
responseB.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}
[Fact]
public async Task Check_Rate_Limiting()
{
var app = await _fixture.UserA.CreateApp(Guid.NewGuid().ToString());
var client = new IngestionClient(_fixture.CreateClient(), app.AppKey, "10.0.0.0");
for (var i = 1; i <= 22; i++)
{
var code1 = await client.TrackEvent(DateTime.UtcNow, "Button Clicked", null);
if (i <= 20)
code1.Should().Be(HttpStatusCode.OK);
else
code1.Should().Be(HttpStatusCode.TooManyRequests);
}
// A different IP should succeed because it uses a different IP
var client2 = new IngestionClient(_fixture.CreateClient(), app.AppKey, "12.0.0.0");
var code2 = await client2.TrackEvent(DateTime.UtcNow, "Button Clicked", null);
code2.Should().Be(HttpStatusCode.OK);
await _eventWritter.FlushEvents();
var count = await _fixture.UserA.CountEvents(app.Id, "24h");
count.Should().Be(21);
}
}