towerops/test/towerops/activity_feed_test.exs
Graham McIntire 2a5254343e
fix: ensure sync logs are in different minutes for activity feed test
The ActivityFeed groups sync logs by minute, so when creating test data with
random timestamps, multiple logs could fall within the same minute and get
grouped together, causing the test to fail. Now ensures each sync log is
exactly 60 seconds apart (different minutes).
2026-03-10 10:15:29 -05:00

397 lines
12 KiB
Elixir

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
# Create sync logs in different minutes (sync groups by minute)
["success", "failed", "partial"]
|> Enum.with_index()
|> Enum.each(fn {status, idx} ->
{:ok, _} =
%SyncLog{}
|> SyncLog.changeset(%{
organization_id: organization.id,
integration_id: integration.id,
status: status,
records_synced: 10,
# Ensure each is in a different minute (60 seconds apart)
inserted_at: DateTime.add(~U[2026-03-06 10:00:00Z], idx * 60, :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