IdDatabaseAuditTest.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.idstore.tests.database;

import com.io7m.ervilla.api.EContainerSupervisorType;
import com.io7m.ervilla.test_extension.ErvillaCloseAfterAll;
import com.io7m.ervilla.test_extension.ErvillaConfiguration;
import com.io7m.ervilla.test_extension.ErvillaExtension;
import com.io7m.idstore.database.api.IdDatabaseAuditQueriesType;
import com.io7m.idstore.database.api.IdDatabaseConnectionType;
import com.io7m.idstore.database.api.IdDatabaseTransactionType;
import com.io7m.idstore.database.api.IdDatabaseType;
import com.io7m.idstore.model.IdAuditEvent;
import com.io7m.idstore.model.IdAuditSearchParameters;
import com.io7m.idstore.model.IdTimeRange;
import com.io7m.idstore.tests.extensions.IdTestDatabases;
import com.io7m.zelador.test_extension.CloseableResourcesType;
import com.io7m.zelador.test_extension.ZeladorExtension;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;

import static com.io7m.idstore.database.api.IdDatabaseRole.IDSTORE;
import static java.time.OffsetDateTime.now;
import static java.util.Optional.empty;
import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith({ErvillaExtension.class, ZeladorExtension.class})
@ErvillaConfiguration(disabledIfUnsupported = true)
public final class IdDatabaseAuditTest
{
  private static IdTestDatabases.IdDatabaseFixture DATABASE_FIXTURE;
  private IdDatabaseConnectionType connection;
  private IdDatabaseTransactionType transaction;
  private IdDatabaseType database;

  @BeforeAll
  public static void setupOnce(
    final @ErvillaCloseAfterAll EContainerSupervisorType containers)
    throws Exception
  {
    DATABASE_FIXTURE =
      IdTestDatabases.create(containers, 15432);
  }

  @BeforeEach
  public void setup(
    final CloseableResourcesType closeables)
    throws Exception
  {
    DATABASE_FIXTURE.reset();

    this.database =
      closeables.addPerTestResource(DATABASE_FIXTURE.createDatabase());
    this.connection =
      closeables.addPerTestResource(this.database.openConnection(IDSTORE));
    this.transaction =
      closeables.addPerTestResource(this.connection.openTransaction());
  }

  @Test
  public void testAuditQuery0()
    throws Exception
  {
    final var adminId =
      IdTestDatabases.createAdminInitial(
        this.transaction, "admin", "12345678");

    final var audit =
      this.transaction.queries(IdDatabaseAuditQueriesType.class);

    final var then = now();
    audit.auditPut(adminId, then.plusSeconds(1), "ET_0", "E0");
    audit.auditPut(adminId, then.plusSeconds(2), "ET_0", "E1");
    audit.auditPut(adminId, then.plusSeconds(3), "ET_0", "E2");
    audit.auditPut(adminId, then.plusSeconds(4), "ET_1", "F3");
    audit.auditPut(adminId, then.plusSeconds(5), "ET_1", "F4");
    audit.auditPut(adminId, then.plusSeconds(6), "ET_1", "F5");
    audit.auditPut(adminId, then.plusSeconds(7), "ET_2", "G6");
    audit.auditPut(adminId, then.plusSeconds(8), "ET_2", "G7");
    audit.auditPut(adminId, then.plusSeconds(9), "ET_2", "G8");

    this.transaction.commit();

    final var parameters =
      new IdAuditSearchParameters(
        new IdTimeRange(then, then.plusDays(1L)),
        empty(),
        empty(),
        empty(),
        4
      );

    final var events = audit.auditEventsSearch(parameters);

    {
      final var page = events.pageCurrent(audit);
      final var items = page.items();
      assertEquals(4, items.size());
      assertEquals(1, page.pageIndex());
      assertEquals(3, page.pageCount());
      assertEquals("E0", items.get(0).message());
      assertEquals("E1", items.get(1).message());
      assertEquals("E2", items.get(2).message());
      assertEquals("F3", items.get(3).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(4, items.size());
      assertEquals(2, page.pageIndex());
      assertEquals(3, page.pageCount());
      assertEquals("F4", items.get(0).message());
      assertEquals("F5", items.get(1).message());
      assertEquals("G6", items.get(2).message());
      assertEquals("G7", items.get(3).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(3, page.pageIndex());
      assertEquals(3, page.pageCount());
      assertEquals("G8", items.get(0).message());
    }
  }

  @Test
  public void testAuditQuery1()
    throws Exception
  {
    final var adminId =
      IdTestDatabases.createAdminInitial(
        this.transaction, "admin", "12345678");

    final var audit =
      this.transaction.queries(IdDatabaseAuditQueriesType.class);

    final var then = now();
    audit.auditPut(adminId, then.plusSeconds(1), "ET_0", "E0");
    audit.auditPut(adminId, then.plusSeconds(2), "ET_0", "E1");
    audit.auditPut(adminId, then.plusSeconds(3), "ET_0", "E2");
    audit.auditPut(adminId, then.plusSeconds(4), "ET_1", "F3");
    audit.auditPut(adminId, then.plusSeconds(5), "ET_1", "F4");
    audit.auditPut(adminId, then.plusSeconds(6), "ET_1", "F5");
    audit.auditPut(adminId, then.plusSeconds(7), "ET_2", "G6");
    audit.auditPut(adminId, then.plusSeconds(8), "ET_2", "G7");
    audit.auditPut(adminId, then.plusSeconds(9), "ET_2", "G8");

    this.transaction.commit();

    final var parameters =
      new IdAuditSearchParameters(
        new IdTimeRange(then, then.plusDays(1L)),
        empty(),
        Optional.of("ET_0"),
        empty(),
        1
      );

    final var events =
      audit.auditEventsSearch(parameters);

    {
      final var page = events.pageCurrent(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(1, page.pageIndex());
      assertEquals(4, page.pageCount());
      assertEquals("E0", items.get(0).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(2, page.pageIndex());
      assertEquals(4, page.pageCount());
      assertEquals("E1", items.get(0).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(3, page.pageIndex());
      assertEquals(4, page.pageCount());
      assertEquals("E2", items.get(0).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(0, items.size());
      assertEquals(4, page.pageIndex());
      assertEquals(4, page.pageCount());
    }
  }

  @Test
  public void testAuditQuery2()
    throws Exception
  {
    final var adminId =
      IdTestDatabases.createAdminInitial(
        this.transaction, "admin", "12345678");

    final var audit =
      this.transaction.queries(IdDatabaseAuditQueriesType.class);

    final var then = now();
    audit.auditPut(adminId, then.plusSeconds(1), "ET_0", "E0");
    audit.auditPut(adminId, then.plusSeconds(2), "ET_0", "E1");
    audit.auditPut(adminId, then.plusSeconds(3), "ET_0", "E2");
    audit.auditPut(adminId, then.plusSeconds(4), "ET_1", "F3");
    audit.auditPut(adminId, then.plusSeconds(5), "ET_1", "F4");
    audit.auditPut(adminId, then.plusSeconds(6), "ET_1", "F5");
    audit.auditPut(adminId, then.plusSeconds(7), "ET_2", "G6");
    audit.auditPut(adminId, then.plusSeconds(8), "ET_2", "G7");
    audit.auditPut(adminId, then.plusSeconds(9), "ET_2", "G8");

    this.transaction.commit();

    final var parameters =
      new IdAuditSearchParameters(
        new IdTimeRange(then, then.plusDays(1L)),
        empty(),
        empty(),
        Optional.of("F"),
        1
      );

    final var events =
      audit.auditEventsSearch(parameters);

    {
      final var page = events.pageCurrent(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(1, page.pageIndex());
      assertEquals(4, page.pageCount());
      assertEquals("F3", items.get(0).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(2, page.pageIndex());
      assertEquals(4, page.pageCount());
      assertEquals("F4", items.get(0).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(1, items.size());
      assertEquals(3, page.pageIndex());
      assertEquals(4, page.pageCount());
      assertEquals("F5", items.get(0).message());
    }

    {
      final var page = events.pageNext(audit);
      final var items = page.items();
      assertEquals(0, items.size());
      assertEquals(4, page.pageIndex());
      assertEquals(4, page.pageCount());
    }
  }

  @Test
  public void testAuditSearchPaging()
    throws Exception
  {
    final var adminId =
      IdTestDatabases.createAdminInitial(
        this.transaction, "admin", "12345678");

    final var audit =
      this.transaction.queries(IdDatabaseAuditQueriesType.class);

    final var then = now();
    for (int index = 0; index < 533; ++index) {
      audit.auditPut(
        adminId,
        then.plusSeconds(index),
        String.format("ET_%04d", Integer.valueOf(index)),
        String.format("E_%04d", Integer.valueOf(index))
      );
    }

    this.transaction.commit();

    final var parameters =
      new IdAuditSearchParameters(
        new IdTimeRange(then, then.plusDays(1L)),
        empty(),
        empty(),
        empty(),
        100
      );

    final var paging =
      audit.auditEventsSearch(parameters);

    {
      final var page = paging.pageCurrent(audit);
      assertEquals(1, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 0, 100, page.items());
    }

    {
      final var page = paging.pageNext(audit);
      assertEquals(2, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 100, 200, page.items());
    }

    {
      final var page = paging.pageNext(audit);
      assertEquals(3, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 200, 300, page.items());
    }

    {
      final var page = paging.pageNext(audit);
      assertEquals(4, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 300, 400, page.items());
    }

    {
      final var page = paging.pageNext(audit);
      assertEquals(5, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 400, 500, page.items());
    }

    {
      final var page = paging.pageNext(audit);
      assertEquals(6, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(33, page.items().size());
      checkPage(then, 500, 533, page.items());
    }

    {
      final var page = paging.pagePrevious(audit);
      assertEquals(5, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 400, 500, page.items());
    }

    {
      final var page = paging.pagePrevious(audit);
      assertEquals(4, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 300, 400, page.items());
    }

    {
      final var page = paging.pagePrevious(audit);
      assertEquals(3, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 200, 300, page.items());
    }

    {
      final var page = paging.pagePrevious(audit);
      assertEquals(2, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 100, 200, page.items());
    }

    {
      final var page = paging.pagePrevious(audit);
      assertEquals(1, page.pageIndex());
      assertEquals(6, page.pageCount());
      assertEquals(100, page.items().size());
      checkPage(then, 0, 100, page.items());
    }
  }

  private static void checkPage(
    final OffsetDateTime time,
    final int indexLow,
    final int indexHigh,
    final List<IdAuditEvent> items)
  {
    final var timeCoarse = time.withNano(0);

    var index = indexLow;
    final var iter = items.iterator();
    while (index < indexHigh) {
      final var item =
        iter.next();
      final var itemTimeCoarse =
        item.time().withNano(0);

      assertEquals(timeCoarse.plusSeconds(index), itemTimeCoarse);
      assertEquals(
        String.format("ET_%04d", Integer.valueOf(index)),
        item.type());
      assertEquals(
        String.format("E_%04d", Integer.valueOf(index)),
        item.message());
      ++index;
    }
  }
}