8000 fix: Ash best practices audit — bug fix and hardening by mwoods79 · Pull Request #106 · huddlz-hq/huddlz · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/huddlz/communities/group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,12 @@ defmodule Huddlz.Communities.Group do

attribute :description, :ci_string do
allow_nil? true
constraints max_length: 5000
end

attribute :location, :string do
allow_nil? true
constraints max_length: 500
end

attribute :is_public, :boolean do
Expand Down
4 changes: 2 additions & 2 deletions lib/huddlz/communities/group_member/checks/group_organizer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ defmodule Huddlz.Communities.GroupMember.Checks.GroupOrganizer do
def describe(_opts), do: "actor is an organizer of the group"

@impl true
def match?(actor, %{group_id: group_id}, _opts)
when not is_nil(actor) and not is_nil(group_id) do
def match?(actor, %{subject: %{arguments: %{group_id: group_id}}}, _opts)
when is_map(actor) and is_binary(group_id) do
GroupMember
|> Ash.Query.filter(group_id: group_id, user_id: actor.id, role: :organizer)
|> Ash.exists?(authorize?: false)
Expand Down
3 changes: 3 additions & 0 deletions lib/huddlz/communities/huddl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ defmodule Huddlz.Communities.Huddl do

attribute :description, :string do
allow_nil? true
constraints max_length: 5000
end

attribute :starts_at, :utc_datetime do
Expand All @@ -352,11 +353,13 @@ defmodule Huddlz.Communities.Huddl do

attribute :physical_location, :string do
allow_nil? true
constraints max_length: 500
end

attribute :virtual_link, :string do
allow_nil? true
sensitive? true
constraints max_length: 2048
end

attribute :is_private, :boolean do
Expand Down
14 changes: 12 additions & 2 deletions lib/huddlz/communities/huddl/recurrence_helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ defmodule Huddlz.Communities.Huddl.RecurrenceHelper do

alias Huddlz.Communities.Huddl

@max_instances 104

@doc """
Recursively generates future huddlz based on the template's frequency
and repeat_until date. Each new huddl copies the source huddl's properties
and advances the start/end times by the appropriate interval.

Stops after `@max_instances` (#{@max_instances}) to prevent unbounded generation.
"""
def generate_huddlz_from_template(huddl_template, huddl) do
def generate_huddlz_from_template(huddl_template, huddl, count \\ 0)

def generate_huddlz_from_template(_huddl_template, _huddl, count)
when count >= @max_instances,
do: :ok

def generate_huddlz_from_template(huddl_template, huddl, count) do
interval_days = frequency_days(huddl_template.frequency)
starts_at = DateTime.add(huddl.starts_at, interval_days, :day)
ends_at = DateTime.add(huddl.ends_at, interval_days, :day)
Expand All @@ -33,7 +43,7 @@ defmodule Huddlz.Communities.Huddl.RecurrenceHelper do
})
|> Ash.create!(authorize?: false)

generate_huddlz_from_template(huddl_template, new_huddl)
generate_huddlz_from_template(huddl_template, new_huddl, count + 1)
end
end

Expand Down
6 changes: 6 additions & 0 deletions lib/huddlz_web/live/huddl_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule HuddlzWeb.HuddlLive do
alias Huddlz.Communities.Group
alias HuddlzWeb.Layouts
require Ash.Query
require Logger

# Authentication is optional - show cards to all but require auth for joining
on_mount {HuddlzWeb.LiveUserAuth, :live_user_optional}
Expand Down Expand Up @@ -197,6 +198,11 @@ defmodule HuddlzWeb.HuddlLive do
{loaded, dists}
end

defp load_results_with_distances({:error, reason}, _socket) do
Logger.warning("Huddl search failed: #{inspect(reason)}")
{[], []}
end

defp load_results_with_distances(_, _socket), do: {[], []}

defp compute_distances(huddls, %{assigns: %{location_active: false}}) do
Expand Down
28 changes: 28 additions & 0 deletions test/huddlz/communities/group_membership_permissions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,34 @@ defmodule Huddlz.Communities.GroupMembershipPermissionsTest do
|> Ash.create()
end

test "group organizer can add member to group", %{verified: owner, public_group: group} do
organizer = generate(user())

# Owner adds organizer
generate(
group_member(
group_id: group.id,
user_id: organizer.id,
role: :organizer,
actor: owner
)
)

# Organizer adds a new member
new_user = generate(user())

assert {:ok, membership} =
GroupMember
|> Ash.Changeset.for_create(
:add_member,
%{group_id: group.id, user_id: new_user.id, role: :member},
actor: organizer
)
|> Ash.create()

assert membership.role == :member
end

test "group owner can add organizer to group", %{verified: owner, public_group: group} do
new_verified = generate(user())

Expand Down
32 changes: 32 additions & 0 deletions test/huddlz/communities/group_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,38 @@ defmodule Huddlz.Communities.GroupTest do
end
end

describe "attribute constraints" do
test "rejects description over 5000 characters" do
owner = generate(user())
long_desc = String.duplicate("a", 5001)

assert {:error, _} =
Group
|> Ash.Changeset.for_create(:create_group, %{
name: "Test Group",
description: long_desc,
is_public: true,
owner_id: owner.id
})
|> Ash.create(actor: owner)
end

test "rejects location over 500 characters" do
owner = generate(user())
long_loc = String.duplicate("a", 501)

assert {:error, _} =
Group
|> Ash.Changeset.for_create(:create_group, %{
name: "Test Group",
location: long_loc,
is_public: true,
owner_id: owner.id
})
|> Ash.create(actor: owner)
end
end

describe "group management" do
setup do
owner = generate(user(role: :user))
Expand Down
21 changes: 21 additions & 0 deletions test/huddlz/communities/huddl/recurrence_helper_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ defmodule Huddlz.Communities.Huddl.RecurrenceHelperTest do
end
end

describe "generate_huddlz_from_template/3 max instances" do
test "caps generation at max_instances even when repeat_until is far in the future", ctx do
# 5 years of weekly = ~260 instances, but should cap at 104
repeat_until = Date.add(Date.utc_today(), 365 * 5)

template =
HuddlTemplate
|> Ash.Changeset.for_create(:create, %{frequency: :weekly, repeat_until: repeat_until})
|> Ash.create!(authorize?: false)

RecurrenceHelper.generate_huddlz_from_template(template, ctx.huddl)

generated =
Huddl
|> Ash.Query.filter(huddl_template_id == ^template.id)
|> Ash.read!(authorize?: false)

assert length(generated) == 104
end
end

describe "generate_huddlz_from_template/2 copies huddl properties" do
test "new huddlz inherit title, description, and other fields from source", ctx do
repeat_until = Date.add(Date.utc_today(), 10)
Expand Down
51 changes: 51 additions & 0 deletions test/huddlz/communities/huddl_access_control_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,57 @@ defmodule Huddlz.Communities.HuddlAccessControlTest do
assert {:error, %Ash.Error.Forbidden{}} = Ash.destroy(huddl, actor: member)
end

test "rejects description over 5000 characters", %{owner: owner, group: group} do
long_desc = String.duplicate("a", 5001)

assert {:error, _} =
Huddl
|> Ash.Changeset.for_create(:create, %{
title: "Test Huddl",
description: long_desc,
event_type: :virtual,
starts_at: DateTime.add(DateTime.utc_now(), 7, :day),
ends_at: DateTime.add(DateTime.utc_now(), 7 * 24 * 3600 + 3600, :second),
creator_id: owner.id,
group_id: group.id
})
|> Ash.create(actor: owner)
end

test "rejects physical_location over 500 characters", %{owner: owner, group: group} do
long_loc = String.duplicate("a", 501)

assert {:error, _} =
94B2 Huddl
|> Ash.Changeset.for_create(:create, %{
title: "Test Huddl",
physical_location: long_loc,
event_type: :in_person,
starts_at: DateTime.add(DateTime.utc_now(), 7, :day),
ends_at: DateTime.add(DateTime.utc_now(), 7 * 24 * 3600 + 3600, :second),
creator_id: owner.id,
group_id: group.id
})
|> Ash.create(actor: owner)
end

test "rejects virtual_link over 2048 characters", %{owner: owner, group: group} do
long_link = "https://example.com/" <> String.duplicate("a", 2029)

assert {:error, _} =
Huddl
|> Ash.Changeset.for_create(:create, %{
title: "Test Huddl",
virtual_link: long_link,
event_type: :virtual,
starts_at: DateTime.add(DateTime.utc_now(), 7, :day),
ends_at: DateTime.add(DateTime.utc_now(), 7 * 24 * 3600 + 3600, :second),
creator_id: owner.id,
group_id: group.id
})
|> Ash.create(actor: owner)
end

test "admin can update and destroy huddl", %{admin: admin, huddl: huddl} do
assert {:ok, updated} =
huddl
Expand Down
Loading
0