From e302b8e9c4e9f92173a82d15daaa5288e353aae0 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 12 Feb 2026 16:55:57 -0600 Subject: [PATCH] add integrations context with CRUD operations --- lib/towerops/integrations.ex | 62 +++++++++++++ test/towerops/integrations_test.exs | 137 ++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 lib/towerops/integrations.ex create mode 100644 test/towerops/integrations_test.exs diff --git a/lib/towerops/integrations.ex b/lib/towerops/integrations.ex new file mode 100644 index 00000000..a025c2ec --- /dev/null +++ b/lib/towerops/integrations.ex @@ -0,0 +1,62 @@ +defmodule Towerops.Integrations do + @moduledoc """ + Context for managing third-party integrations per organization. + """ + import Ecto.Query + + alias Towerops.Integrations.Integration + alias Towerops.Repo + + def list_integrations(organization_id) do + Integration + |> where(organization_id: ^organization_id) + |> order_by(:provider) + |> Repo.all() + end + + def get_integration(organization_id, provider) do + case Repo.get_by(Integration, organization_id: organization_id, provider: provider) do + nil -> {:error, :not_found} + integration -> {:ok, integration} + end + end + + def get_integration!(organization_id, provider) do + Repo.get_by!(Integration, organization_id: organization_id, provider: provider) + end + + def create_integration(organization_id, attrs) do + %Integration{} + |> Integration.changeset(Map.put(attrs, :organization_id, organization_id)) + |> Repo.insert() + end + + def update_integration(%Integration{} = integration, attrs) do + integration + |> Integration.changeset(attrs) + |> Repo.update() + end + + def update_sync_status(%Integration{} = integration, status) do + integration + |> Integration.changeset(%{ + last_sync_status: status, + last_synced_at: DateTime.truncate(DateTime.utc_now(), :second) + }) + |> Repo.update() + end + + def delete_integration(%Integration{} = integration) do + Repo.delete(integration) + end + + def change_integration(%Integration{} = integration, attrs \\ %{}) do + Integration.changeset(integration, attrs) + end + + def list_enabled_integrations(provider) do + Integration + |> where(provider: ^provider, enabled: true) + |> Repo.all() + end +end diff --git a/test/towerops/integrations_test.exs b/test/towerops/integrations_test.exs new file mode 100644 index 00000000..c3869a11 --- /dev/null +++ b/test/towerops/integrations_test.exs @@ -0,0 +1,137 @@ +defmodule Towerops.IntegrationsTest do + use Towerops.DataCase, async: true + + import Towerops.AccountsFixtures + import Towerops.OrganizationsFixtures + + alias Towerops.Integrations + alias Towerops.Integrations.Integration + + setup do + user = user_fixture() + organization = organization_fixture(user.id) + %{organization: organization} + end + + describe "list_integrations/1" do + test "returns all integrations for an organization", %{organization: org} do + {:ok, _} = Integrations.create_integration(org.id, %{provider: "preseem"}) + integrations = Integrations.list_integrations(org.id) + assert length(integrations) == 1 + end + + test "does not return integrations from other orgs", %{organization: org} do + user2 = user_fixture() + org2 = organization_fixture(user2.id) + {:ok, _} = Integrations.create_integration(org2.id, %{provider: "preseem"}) + assert Integrations.list_integrations(org.id) == [] + end + end + + describe "get_integration/2" do + test "returns integration by org and provider", %{organization: org} do + {:ok, created} = Integrations.create_integration(org.id, %{provider: "preseem"}) + assert {:ok, fetched} = Integrations.get_integration(org.id, "preseem") + assert fetched.id == created.id + end + + test "returns error when not found", %{organization: org} do + assert {:error, :not_found} = Integrations.get_integration(org.id, "preseem") + end + end + + describe "get_integration!/2" do + test "returns integration by org and provider", %{organization: org} do + {:ok, created} = Integrations.create_integration(org.id, %{provider: "preseem"}) + fetched = Integrations.get_integration!(org.id, "preseem") + assert fetched.id == created.id + end + + test "raises when not found", %{organization: org} do + assert_raise Ecto.NoResultsError, fn -> + Integrations.get_integration!(org.id, "preseem") + end + end + end + + describe "create_integration/2" do + test "creates with valid attrs", %{organization: org} do + attrs = %{provider: "preseem", credentials: %{"api_key" => "test-123"}, enabled: true} + assert {:ok, %Integration{} = integration} = Integrations.create_integration(org.id, attrs) + assert integration.provider == "preseem" + assert integration.credentials == %{"api_key" => "test-123"} + assert integration.enabled == true + assert integration.organization_id == org.id + end + + test "rejects duplicate provider per org", %{organization: org} do + {:ok, _} = Integrations.create_integration(org.id, %{provider: "preseem"}) + assert {:error, changeset} = Integrations.create_integration(org.id, %{provider: "preseem"}) + assert "has already been taken" in errors_on(changeset).provider + end + end + + describe "update_integration/2" do + test "updates credentials", %{organization: org} do + {:ok, integration} = + Integrations.create_integration(org.id, %{ + provider: "preseem", + credentials: %{"api_key" => "old-key"} + }) + + assert {:ok, updated} = + Integrations.update_integration(integration, %{credentials: %{"api_key" => "new-key"}}) + + assert updated.credentials == %{"api_key" => "new-key"} + end + + test "updates enabled flag", %{organization: org} do + {:ok, integration} = + Integrations.create_integration(org.id, %{provider: "preseem", enabled: false}) + + assert {:ok, updated} = Integrations.update_integration(integration, %{enabled: true}) + assert updated.enabled == true + end + end + + describe "update_sync_status/2" do + test "updates last_synced_at and last_sync_status", %{organization: org} do + {:ok, integration} = Integrations.create_integration(org.id, %{provider: "preseem"}) + assert integration.last_sync_status == "never" + assert integration.last_synced_at == nil + + assert {:ok, updated} = Integrations.update_sync_status(integration, "success") + assert updated.last_sync_status == "success" + assert updated.last_synced_at + end + end + + describe "delete_integration/1" do + test "deletes the integration", %{organization: org} do + {:ok, integration} = Integrations.create_integration(org.id, %{provider: "preseem"}) + assert {:ok, _} = Integrations.delete_integration(integration) + assert {:error, :not_found} = Integrations.get_integration(org.id, "preseem") + end + end + + describe "change_integration/2" do + test "returns a changeset", %{organization: org} do + {:ok, integration} = Integrations.create_integration(org.id, %{provider: "preseem"}) + assert %Ecto.Changeset{} = Integrations.change_integration(integration) + end + end + + describe "list_enabled_integrations/1" do + test "returns only enabled integrations for a provider", %{organization: org} do + {:ok, _} = Integrations.create_integration(org.id, %{provider: "preseem", enabled: true}) + + user2 = user_fixture() + org2 = organization_fixture(user2.id) + {:ok, _} = Integrations.create_integration(org2.id, %{provider: "preseem", enabled: false}) + + enabled = Integrations.list_enabled_integrations("preseem") + assert length(enabled) == 1 + assert hd(enabled).organization_id == org.id + end + end +end