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).
397 lines
12 KiB
Elixir
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
|