| defmodule Plausible.AuditTest do |
| use Plausible.DataCase |
|
|
| on_ee do |
| alias Plausible.Audit |
| alias Plausible.Audit.Encoder |
| alias Plausible.Audit.Entry |
| alias Plausible.Audit.TestSchema |
|
|
| describe "Audit.Encoder" do |
| test "encodes integer, float, and bitstring as themselves" do |
| assert Encoder.encode(42) == 42 |
| assert Encoder.encode(3.14) == 3.14 |
| assert Encoder.encode("hello") == "hello" |
| end |
|
|
| test "encodes atoms" do |
| assert Encoder.encode(:foo) == "foo" |
| assert Encoder.encode(nil) == nil |
| assert Encoder.encode(true) == true |
| assert Encoder.encode(false) == false |
| end |
|
|
| test "encodes Date, DateTime, NaiveDateTime, Time as strings" do |
| dt = ~U[2024-06-01 12:00:00Z] |
| date = ~D[2024-06-01] |
| ndt = ~N[2024-06-01 12:00:00] |
| time = ~T[12:00:00] |
| assert Encoder.encode(dt) == to_string(dt) |
| assert Encoder.encode(date) == to_string(date) |
| assert Encoder.encode(ndt) == to_string(ndt) |
| assert Encoder.encode(time) == to_string(time) |
| end |
|
|
| test "encodes lists recursively" do |
| assert Encoder.encode([:foo, 1, "bar"]) == ["foo", 1, "bar"] |
| end |
|
|
| test "encodes map recursively" do |
| map = %{foo: :bar, num: 1, nested: %{baz: :qux}} |
| assert Encoder.encode(map) == %{foo: "bar", num: 1, nested: %{baz: "qux"}} |
| end |
|
|
| test "raises if association not loaded and not allowed" do |
| map = %{ |
| assoc: %Ecto.Association.NotLoaded{ |
| __field__: :assoc, |
| __owner__: DummyStruct, |
| __cardinality__: :one |
| }, |
| __allow_not_loaded__: [] |
| } |
|
|
| assert_raise Audit.EncoderError, ~r/assoc association not loaded/, fn -> |
| Encoder.encode(map) |
| end |
| end |
|
|
| test "skips not loaded association if allowed" do |
| map = %{ |
| assoc: %Ecto.Association.NotLoaded{ |
| __field__: :assoc, |
| __owner__: DummyStruct, |
| __cardinality__: :one |
| }, |
| __allow_not_loaded__: [:assoc] |
| } |
|
|
| assert Encoder.encode(map) == %{} |
| end |
|
|
| test "enforcing schema association present" do |
| assert_raise Audit.EncoderError, fn -> |
| Audit.encode(%TestSchema.VariantWithAssociation{id: 1}) |
| end |
|
|
| assert %{id: 1, team: "some"} = |
| Audit.encode(%TestSchema.VariantWithAssociation{id: 1, team: :some}) |
|
|
| assert %{id: 1} = |
| Audit.encode(%TestSchema.VariantWithAssociationAllowNotLoaded{id: 1}) |
|
|
| assert %{id: 1, team: "some"} = |
| Audit.encode(%TestSchema.VariantWithAssociationAllowNotLoaded{ |
| id: 1, |
| team: :some |
| }) |
| end |
|
|
| test "returns data if only data is present" do |
| data = %{foo: :bar} |
| changeset = %Ecto.Changeset{data: data, changes: %{}} |
| assert Encoder.encode(changeset) == %{foo: "bar"} |
| end |
|
|
| test "returns changes if only changes are present" do |
| changes = %{foo: :baz} |
| changeset = %Ecto.Changeset{data: %{}, changes: changes} |
| assert Encoder.encode(changeset) == %{foo: "baz"} |
| end |
|
|
| test "returns empty map if both data and changes are empty" do |
| changeset = %Ecto.Changeset{data: %{}, changes: %{}} |
| assert Encoder.encode(changeset) == %{} |
| end |
|
|
| test "returns before/after map if both data and changes are present" do |
| data = %{foo: :bar} |
| changes = %{foo: :baz} |
| changeset = %Ecto.Changeset{data: data, changes: changes} |
| assert Encoder.encode(changeset) == %{before: %{foo: "bar"}, after: %{foo: "baz"}} |
| end |
| end |
|
|
| describe "Audit.Entry" do |
| test "changeset/2 with valid params and context" do |
| Entry.set_context(%{current_user: %{id: 42}, current_team: %{id: 7}}) |
| params = %{entity: "User", entity_id: "42", meta: %{name: "bar"}} |
| cs = Entry.changeset("login", params) |
| assert cs.valid? |
| assert cs.data.name == "login" |
| assert cs.changes.entity == "User" |
| assert cs.changes.entity_id == "42" |
| assert cs.changes.meta == %{name: "bar"} |
| assert cs.changes.user_id == 42 |
| assert cs.changes.team_id == 7 |
| assert cs.changes.actor_type == :user |
| assert is_struct(cs.changes.datetime, NaiveDateTime) |
| end |
|
|
| test "changeset/2 missing required fields" do |
| cs = Entry.changeset("test", %{}) |
| refute cs.valid? |
|
|
| assert cs.errors == [ |
| entity: {"can't be blank", [validation: :required]}, |
| entity_id: {"can't be blank", [validation: :required]} |
| ] |
| end |
|
|
| test "changeset/2 with missing context" do |
| cs = Entry.changeset("test", %{entity: "E", entity_id: "1"}) |
| assert is_nil(cs.changes.user_id) |
| assert is_nil(cs.changes.team_id) |
| assert cs.data.user_id == 0 |
| assert cs.data.team_id == 0 |
| assert cs.data.actor_type == :system |
| end |
|
|
| test "new/3 with struct and params" do |
| Entry.set_context(%{current_user: %{id: 1}, current_team: %{id: 2}}) |
| struct = %TestSchema{id: 123} |
| cs = Entry.new("test", struct, %{meta: %{x: 1}}) |
| assert cs.changes.entity == "Plausible.Audit.TestSchema" |
| assert cs.changes.entity_id == "123" |
| assert cs.changes.meta == %{x: 1} |
| end |
|
|
| test "include_change/2 encodes changeset" do |
| struct = %TestSchema{id: 1, name: "bar"} |
| changeset = Ecto.Changeset.change(struct, name: "baz") |
|
|
| entry = Entry.new("update", struct) |
| entry = Entry.include_change(entry, changeset) |
| assert entry.changes.change == %{after: %{name: "baz"}, before: %{name: "bar", id: 1}} |
| end |
|
|
| test "include_change/2 accepts raw map" do |
| struct = %TestSchema{id: 1, name: "bar"} |
|
|
| entry = Entry.new("update", struct) |
| entry = Entry.include_change(entry, %{foo: :bar}) |
| assert entry.changes.change == %{foo: :bar} |
| end |
| end |
|
|
| describe "Repo integration" do |
| test "update_with_audit/2" do |
| user = new_user() |> Repo.preload([:sso_integration, :sso_domain]) |
|
|
| cs = Plausible.Auth.User.name_changeset(user, %{name: "John Doe"}) |
|
|
| assert {:ok, %Plausible.Auth.User{name: "John Doe"}} = |
| Repo.update_with_audit(cs, "user_update") |
|
|
| assert [ |
| %Plausible.Audit.Entry{ |
| name: "user_update", |
| change: %{ |
| "after" => %{"name" => "John Doe"}, |
| "before" => %{ |
| "name" => "Jane Smith" |
| } |
| } |
| } |
| ] = Audit.list_entries(entity: "Plausible.Auth.User", entity_id: "#{user.id}") |
| end |
| end |
|
|
| test "update_with_audit!/2" do |
| user = new_user() |> Repo.preload([:sso_integration, :sso_domain]) |
| cs = Plausible.Auth.User.name_changeset(user, %{name: "John Doe"}) |
|
|
| assert %Plausible.Auth.User{name: "John Doe"} = |
| Repo.update_with_audit!(cs, "user_update") |
|
|
| assert [ |
| %Plausible.Audit.Entry{ |
| name: "user_update", |
| change: %{ |
| "after" => %{"name" => "John Doe"}, |
| "before" => %{ |
| "name" => "Jane Smith" |
| } |
| } |
| } |
| ] = Audit.list_entries(entity: "Plausible.Auth.User", entity_id: "#{user.id}") |
| end |
|
|
| test "insert_with_audit/2" do |
| changeset = |
| Plausible.Auth.User.new(%{ |
| name: "Jane Doe", |
| email: "jane@example.com", |
| password: "very-secret-and-very-long-123", |
| password_confirmation: "very-secret-and-very-long-123" |
| }) |
|
|
| {:ok, %Plausible.Auth.User{id: user_id, name: "Jane Doe"} = user} = |
| Repo.insert_with_audit(changeset, "user_insert") |
|
|
| entity_id = to_string(user_id) |
|
|
| assert [ |
| %Plausible.Audit.Entry{ |
| name: "user_insert", |
| entity_id: ^entity_id, |
| change: %{ |
| "email" => "jane@example.com", |
| "email_verified" => false, |
| "id" => ^user_id, |
| "last_team_identifier" => nil, |
| "name" => "Jane Doe", |
| "previous_email" => nil, |
| "totp_enabled" => false |
| } |
| } |
| ] = Audit.list_entries(entity: "Plausible.Auth.User", entity_id: "#{user.id}") |
| end |
|
|
| test "insert_with_audit!/2" do |
| changeset = |
| Plausible.Auth.User.new(%{ |
| name: "Jane Doe", |
| email: "jane@example.com", |
| password: "very-secret-and-very-long-123", |
| password_confirmation: "very-secret-and-very-long-123" |
| }) |
|
|
| assert %Plausible.Auth.User{name: "Jane Doe", id: user_id} = |
| Repo.insert_with_audit!(changeset, "user_insert") |
|
|
| assert [ |
| %Plausible.Audit.Entry{ |
| name: "user_insert", |
| change: %{} |
| } |
| ] = Audit.list_entries(entity: "Plausible.Auth.User", entity_id: "#{user_id}") |
| end |
|
|
| test "delete_with_audit!/2" do |
| user = new_user() |
| user_email = user.email |
| user_id = user.id |
| assert %Plausible.Auth.User{} = Repo.delete_with_audit!(user, "user_delete") |
|
|
| assert [ |
| %Plausible.Audit.Entry{ |
| name: "user_delete", |
| change: %{ |
| "email" => ^user_email, |
| "email_verified" => true, |
| "id" => ^user_id, |
| "last_team_identifier" => nil, |
| "name" => "Jane Smith", |
| "previous_email" => nil, |
| "totp_enabled" => false |
| } |
| } |
| ] = Audit.list_entries(entity: "Plausible.Auth.User", entity_id: "#{user.id}") |
| end |
| end |
| end |
|
|