test(activity_feed): add comprehensive test coverage
- Test empty activity list - Test preseem sync logs with correct DateTime format - Test duration formatting with integer precision - Test sync logs without duration - Test grouping by minute - Test sorting by timestamp descending - Test limit parameter - Test date range filtering - Test organization isolation - Test different sync statuses (success, failed, partial) - Test count_by_type functionality
This commit is contained in:
parent
70718a53dc
commit
3a5da6f68c
1 changed files with 393 additions and 0 deletions
393
test/towerops/activity_feed_test.exs
Normal file
393
test/towerops/activity_feed_test.exs
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
defmodule Towerops.ActivityFeedTest do
|
||||
use Towerops.DataCase
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.ActivityFeed
|
||||
alias Towerops.Integrations
|
||||
alias Towerops.Preseem.SyncLog
|
||||
alias Towerops.Repo
|
||||
|
||||
describe "list_org_activity/2" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
# Create integration for sync logs
|
||||
{:ok, integration} =
|
||||
Integrations.create_integration(organization.id, %{
|
||||
provider: "preseem",
|
||||
enabled: true,
|
||||
credentials: %{"api_key" => "test_key"}
|
||||
})
|
||||
|
||||
%{user: user, organization: organization, integration: integration}
|
||||
end
|
||||
|
||||
test "returns empty list when no activity exists", %{organization: organization} do
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
assert activities == []
|
||||
end
|
||||
|
||||
test "includes preseem sync logs with correct timestamp format", %{
|
||||
organization: organization,
|
||||
integration: integration
|
||||
} do
|
||||
# Create sync logs
|
||||
{:ok, _sync_log1} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 150,
|
||||
duration_ms: 223,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, _sync_log2} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "failed",
|
||||
records_synced: 0,
|
||||
duration_ms: nil,
|
||||
inserted_at: ~U[2026-03-06 10:01:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
|
||||
assert length(activities) == 2
|
||||
|
||||
# Check first activity (most recent)
|
||||
[first | _] = activities
|
||||
assert first.type == :sync
|
||||
assert first.summary =~ "Preseem sync"
|
||||
assert first.detail =~ "records synced"
|
||||
|
||||
# Verify timestamp is DateTime (not NaiveDateTime)
|
||||
assert %DateTime{} = first.timestamp
|
||||
assert first.timestamp.time_zone == "Etc/UTC"
|
||||
end
|
||||
|
||||
test "formats duration with reasonable precision", %{
|
||||
organization: organization,
|
||||
integration: integration
|
||||
} do
|
||||
# Create sync log with duration that will be averaged
|
||||
{:ok, _sync_log} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 100,
|
||||
duration_ms: 223,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
|
||||
assert length(activities) == 1
|
||||
[activity] = activities
|
||||
|
||||
# Should show duration (will be 223 from avg, rounded to integer)
|
||||
assert activity.detail == "100 records synced in 223ms"
|
||||
end
|
||||
|
||||
test "handles sync logs without duration", %{organization: organization, integration: integration} do
|
||||
{:ok, _sync_log} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 50,
|
||||
duration_ms: nil,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
|
||||
assert length(activities) == 1
|
||||
[activity] = activities
|
||||
|
||||
# Should not include duration
|
||||
assert activity.detail == "50 records synced"
|
||||
end
|
||||
|
||||
test "groups sync logs by minute", %{organization: organization, integration: integration} do
|
||||
# Create multiple sync logs within the same minute
|
||||
base_time = ~U[2026-03-06 10:00:00Z]
|
||||
|
||||
for i <- 0..4 do
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 10 * i,
|
||||
duration_ms: 100 + i,
|
||||
inserted_at: DateTime.add(base_time, i * 10, :second)
|
||||
})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
|
||||
# Should be grouped into 1 activity (same minute)
|
||||
assert length(activities) == 1
|
||||
[activity] = activities
|
||||
|
||||
# Should sum records_synced (0 + 10 + 20 + 30 + 40 = 100)
|
||||
assert activity.detail =~ "100 records synced"
|
||||
|
||||
# Should average duration_ms ((100 + 101 + 102 + 103 + 104) / 5 = 102)
|
||||
assert activity.detail =~ "102ms"
|
||||
end
|
||||
|
||||
test "sorts activities by timestamp descending", %{
|
||||
organization: organization,
|
||||
integration: integration
|
||||
} do
|
||||
# Create sync logs at different times
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 1,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 2,
|
||||
inserted_at: ~U[2026-03-06 11:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 3,
|
||||
inserted_at: ~U[2026-03-06 09:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
|
||||
assert length(activities) == 3
|
||||
|
||||
# Should be sorted newest first
|
||||
[first, second, third] = activities
|
||||
assert first.detail =~ "2 records"
|
||||
assert second.detail =~ "1 records"
|
||||
assert third.detail =~ "3 records"
|
||||
end
|
||||
|
||||
test "respects limit parameter", %{organization: organization, integration: integration} do
|
||||
# Create many sync logs
|
||||
for i <- 1..10 do
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: i,
|
||||
inserted_at: DateTime.add(~U[2026-03-06 10:00:00Z], i * 60, :second)
|
||||
})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id, limit: 5)
|
||||
|
||||
assert length(activities) == 5
|
||||
end
|
||||
|
||||
test "filters by date range", %{organization: organization, integration: integration} do
|
||||
# Create sync logs over several days
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 1,
|
||||
inserted_at: ~U[2026-03-05 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 2,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 3,
|
||||
inserted_at: ~U[2026-03-07 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
activities =
|
||||
ActivityFeed.list_org_activity(organization.id,
|
||||
after: ~U[2026-03-06 00:00:00Z],
|
||||
before: ~U[2026-03-07 00:00:00Z]
|
||||
)
|
||||
|
||||
assert length(activities) == 1
|
||||
[activity] = activities
|
||||
assert activity.detail =~ "2 records"
|
||||
end
|
||||
|
||||
test "only returns activities for the specified organization", %{user: user} do
|
||||
org1 = organization_fixture(user.id)
|
||||
org2 = organization_fixture(user.id, %{name: "Other Org"})
|
||||
|
||||
# Create integrations for both orgs
|
||||
{:ok, int1} =
|
||||
Integrations.create_integration(org1.id, %{
|
||||
provider: "preseem",
|
||||
enabled: true,
|
||||
credentials: %{"api_key" => "test_key1"}
|
||||
})
|
||||
|
||||
{:ok, int2} =
|
||||
Integrations.create_integration(org2.id, %{
|
||||
provider: "preseem",
|
||||
enabled: true,
|
||||
credentials: %{"api_key" => "test_key2"}
|
||||
})
|
||||
|
||||
# Create sync log for org1
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: org1.id,
|
||||
integration_id: int1.id,
|
||||
status: "success",
|
||||
records_synced: 100,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
# Create sync log for org2
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: org2.id,
|
||||
integration_id: int2.id,
|
||||
status: "success",
|
||||
records_synced: 200,
|
||||
inserted_at: ~U[2026-03-06 10:00:00Z]
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
# Should only see org1's activities
|
||||
activities = ActivityFeed.list_org_activity(org1.id)
|
||||
assert length(activities) == 1
|
||||
[activity] = activities
|
||||
assert activity.detail =~ "100 records"
|
||||
|
||||
# Should only see org2's activities
|
||||
activities = ActivityFeed.list_org_activity(org2.id)
|
||||
assert length(activities) == 1
|
||||
[activity] = activities
|
||||
assert activity.detail =~ "200 records"
|
||||
end
|
||||
|
||||
test "handles different sync statuses", %{organization: organization, integration: integration} do
|
||||
for status <- ["success", "failed", "partial"] do
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: status,
|
||||
records_synced: 10,
|
||||
inserted_at: DateTime.add(~U[2026-03-06 10:00:00Z], :rand.uniform(3600), :second)
|
||||
})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
activities = ActivityFeed.list_org_activity(organization.id)
|
||||
|
||||
assert length(activities) == 3
|
||||
|
||||
# Check that statuses are reflected in summaries
|
||||
summaries = Enum.map(activities, & &1.summary)
|
||||
assert Enum.any?(summaries, &(&1 =~ "completed"))
|
||||
assert Enum.any?(summaries, &(&1 =~ "failed"))
|
||||
assert Enum.any?(summaries, &(&1 =~ "partial"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_by_type/1" do
|
||||
test "returns zero counts when no activity exists" do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
counts = ActivityFeed.count_by_type(organization.id)
|
||||
|
||||
assert is_map(counts)
|
||||
assert counts |> Map.values() |> Enum.all?(&(&1 == 0))
|
||||
end
|
||||
|
||||
test "counts recent sync activities" do
|
||||
user = user_fixture()
|
||||
organization = organization_fixture(user.id)
|
||||
|
||||
# Create integration
|
||||
{:ok, integration} =
|
||||
Integrations.create_integration(organization.id, %{
|
||||
provider: "preseem",
|
||||
enabled: true,
|
||||
credentials: %{"api_key" => "test_key"}
|
||||
})
|
||||
|
||||
# Create recent sync log
|
||||
{:ok, _} =
|
||||
%SyncLog{}
|
||||
|> SyncLog.changeset(%{
|
||||
organization_id: organization.id,
|
||||
integration_id: integration.id,
|
||||
status: "success",
|
||||
records_synced: 100,
|
||||
inserted_at: DateTime.utc_now()
|
||||
})
|
||||
|> Repo.insert()
|
||||
|
||||
counts = ActivityFeed.count_by_type(organization.id)
|
||||
|
||||
assert counts[:sync] > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue