add preseem sync worker oban cron job

This commit is contained in:
Graham McIntire 2026-02-12 17:56:52 -06:00
parent 78040c0df7
commit b04d25e607
No known key found for this signature in database
4 changed files with 192 additions and 2 deletions

View file

@ -79,7 +79,9 @@ config :towerops, Oban,
# Delete expired IP bans every hour
{"0 * * * *", Towerops.Workers.ExpiredBanCleanupWorker},
# Delete stale violation records daily at 2 AM
{"0 2 * * *", Towerops.Workers.StaleViolationCleanupWorker}
{"0 2 * * *", Towerops.Workers.StaleViolationCleanupWorker},
# Sync Preseem data every 10 minutes
{"*/10 * * * *", Towerops.Workers.PreseemSyncWorker}
]},
# Automatically delete completed jobs after 60 seconds
{Oban.Plugins.Pruner, max_age: 60},

View file

@ -192,7 +192,9 @@ if config_env() == :prod do
# Delete expired IP bans every hour
{"0 * * * *", Towerops.Workers.ExpiredBanCleanupWorker},
# Delete stale violation records daily at 2 AM
{"0 2 * * *", Towerops.Workers.StaleViolationCleanupWorker}
{"0 2 * * *", Towerops.Workers.StaleViolationCleanupWorker},
# Sync Preseem data every 10 minutes
{"*/10 * * * *", Towerops.Workers.PreseemSyncWorker}
]},
# Automatically delete completed jobs after 60 seconds
{Oban.Plugins.Pruner, max_age: 60},

View file

@ -0,0 +1,61 @@
defmodule Towerops.Workers.PreseemSyncWorker do
@moduledoc """
Oban cron worker that syncs Preseem data for all enabled integrations.
Runs every 10 minutes. For each org with an enabled Preseem integration,
checks if enough time has elapsed since the last sync (based on the
integration's sync_interval_minutes), then calls Preseem.Sync.sync_organization/1.
"""
use Oban.Worker, queue: :maintenance
alias Towerops.Integrations
alias Towerops.Preseem.Sync
require Logger
@impl Oban.Worker
def perform(%Oban.Job{}) do
integrations = Integrations.list_enabled_integrations("preseem")
results =
Enum.map(integrations, fn integration ->
if should_sync?(integration) do
case Sync.sync_organization(integration) do
{:ok, result} ->
Logger.info("Preseem sync completed for org #{integration.organization_id}: #{inspect(result)}")
{:ok, result}
{:error, reason} ->
Logger.error("Preseem sync failed for org #{integration.organization_id}: #{inspect(reason)}")
{:error, reason}
end
else
:skipped
end
end)
synced = Enum.count(results, &match?({:ok, _}, &1))
failed = Enum.count(results, &match?({:error, _}, &1))
skipped = Enum.count(results, &(&1 == :skipped))
if synced > 0 or failed > 0 do
Logger.info("Preseem sync batch: #{synced} synced, #{failed} failed, #{skipped} skipped")
end
:ok
end
defp should_sync?(integration) do
case integration.last_synced_at do
nil ->
true
last_synced_at ->
interval_seconds = (integration.sync_interval_minutes || 10) * 60
elapsed = DateTime.diff(DateTime.utc_now(), last_synced_at, :second)
elapsed >= interval_seconds
end
end
end

View file

@ -0,0 +1,125 @@
defmodule Towerops.Workers.PreseemSyncWorkerTest do
use Towerops.DataCase, async: true
use Oban.Testing, repo: Towerops.Repo
import Towerops.AccountsFixtures
import Towerops.IntegrationsFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Preseem.Client
alias Towerops.Workers.PreseemSyncWorker
setup do
user = user_fixture()
org = organization_fixture(user.id)
%{org: org}
end
describe "perform/1" do
test "syncs enabled preseem integrations", %{org: org} do
integration = integration_fixture(org.id)
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{
"data" => [
%{
"id" => "ap-1",
"name" => "Test AP",
"mac_address" => "AA:BB:CC:DD:EE:FF",
"ip_address" => "10.0.0.1"
}
]
})
end)
assert :ok = perform_job(PreseemSyncWorker, %{})
updated = Repo.reload!(integration)
assert updated.last_synced_at
assert updated.last_sync_status in ["success", "partial"]
end
test "skips integrations synced recently", %{org: org} do
_integration =
integration_fixture(org.id, %{
last_synced_at: DateTime.truncate(DateTime.utc_now(), :second),
last_sync_status: "success"
})
# No Client stub needed - should skip without calling API
assert :ok = perform_job(PreseemSyncWorker, %{})
end
test "handles no enabled integrations" do
assert :ok = perform_job(PreseemSyncWorker, %{})
end
test "handles API errors without crashing", %{org: org} do
_integration = integration_fixture(org.id)
Req.Test.stub(Client, fn conn ->
conn
|> Plug.Conn.put_status(401)
|> Req.Test.json(%{"error" => "unauthorized"})
end)
assert :ok = perform_job(PreseemSyncWorker, %{})
end
test "respects sync_interval_minutes", %{org: org} do
# Last synced 5 minutes ago with a 10-minute interval -> should skip
five_minutes_ago =
DateTime.utc_now()
|> DateTime.add(-5, :minute)
|> DateTime.truncate(:second)
_integration =
integration_fixture(org.id, %{
sync_interval_minutes: 10,
last_synced_at: five_minutes_ago,
last_sync_status: "success"
})
# No Client stub needed - should skip without calling API
assert :ok = perform_job(PreseemSyncWorker, %{})
end
test "syncs when sync interval has elapsed", %{org: org} do
# Last synced 15 minutes ago with a 10-minute interval -> should sync
fifteen_minutes_ago =
DateTime.utc_now()
|> DateTime.add(-15, :minute)
|> DateTime.truncate(:second)
integration =
integration_fixture(org.id, %{
sync_interval_minutes: 10,
last_synced_at: fifteen_minutes_ago,
last_sync_status: "success"
})
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{"data" => []})
end)
assert :ok = perform_job(PreseemSyncWorker, %{})
updated = Repo.reload!(integration)
assert DateTime.after?(updated.last_synced_at, fifteen_minutes_ago)
end
test "syncs integrations that have never been synced", %{org: org} do
integration = integration_fixture(org.id)
assert integration.last_synced_at == nil
Req.Test.stub(Client, fn conn ->
Req.Test.json(conn, %{"data" => []})
end)
assert :ok = perform_job(PreseemSyncWorker, %{})
updated = Repo.reload!(integration)
assert updated.last_synced_at
end
end
end