Ctrl K
ComunidadGuías…

tests/IntegrationTests/IngestionTests.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
    }
}