store: delete a group
Assisted-by: GLM-5.3 via pi
change lqwyypvmzkzktspsmzsvqlwuwnvpxrso
commit aca9c4f21ac03171a61950aac8ab11d8b47280c9
author Alpha Chen <alpha@kejadlen.dev>
date
parent xwnmlkms
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 7408741..751e59c 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -694,6 +694,33 @@ module ProTacts
       end
     end
 
+    # A group goes, and its members stop serving what it lent them: the
+    # row's delete cascades the memberships and the lent lines away
+    # with it (db/migrations/004_groups.rb). Members are logged as
+    # leavers are (#remove_member's reason) — a `sync:` group's
+    # whatever their bytes did, a book's deletion taking its cards out
+    # of it. Everyone's book is refused, #name_book's reason: every
+    # card a client created joined it, so deleting it would tell every
+    # client to drop every card it ever synced. A group nobody has is
+    # the ordinary miss, #delete's shape.
+    #: (String id) -> bool
+    def delete_group(id)
+      name = groups.where(id:).sole.fetch(:name).to_s
+      # Refused before the transaction, #name_book's reason for the
+      # same refusal: Sequel wraps what a rollback raises.
+      raise EveryonesBookName, "#{EVERYONE} is everyone's book, not a group to delete" if name == EVERYONE
+
+      @database.transaction do
+        members = member_ids([id])
+        fan_out(members, moved: sync_name?(name) ? members : []) {
+          groups.where(id:).delete
+        }
+        true
+      end
+    rescue Sequel::NoMatchingRow
+      false
+    end
+
     # A card's side of #edit_group: one transaction, leavers first and
     # joiners last for that method's reason. A name creates a group to
     # join inside the same transaction, so a taken name (#create_group)
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 4727cfc..23e1013 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -1315,6 +1315,60 @@ class StoreTest < Minitest::Test
     end
   end
 
+  def test_a_deleted_group_takes_its_membership_and_lent_lines_away
+    with_store({"aiden" => AIDEN, "znorth" => ZED}) do |store|
+      id = FixtureData.seed_group(store, members: %w[aiden znorth], lines: [HOUSEHOLD_ADDRESS])
+
+      assert store.delete_group(id)
+
+      assert_nil store.group(id)
+      %w[aiden znorth].each do |member|
+        assert_equal [], store.groups_of(member).map(&:id)
+        change = store.changes_of(member).first
+        assert_equal "group", change.action
+        assert_equal store.contact(member).etag, change.etag
+        assert_equal [HOUSEHOLD_ADDRESS], change.diff.removed
+      end
+    end
+  end
+
+  def test_deleting_a_sync_group_logs_its_members_whatever_their_bytes
+    with_store({"aiden" => AIDEN}) do |store|
+      id = FixtureData.seed_group(store, name: "sync:Alpha Chen", members: ["aiden"])
+
+      store.delete_group(id)
+
+      assert_equal %w[group put], store.changes_of("aiden").map(&:action)
+    end
+  end
+
+  def test_deleting_a_group_that_lends_nothing_logs_no_member
+    with_store({"aiden" => AIDEN}) do |store|
+      id = FixtureData.seed_group(store, members: ["aiden"])
+
+      store.delete_group(id)
+
+      assert_equal %w[put], store.changes_of("aiden").map(&:action)
+    end
+  end
+
+  def test_everyones_book_is_not_a_group_to_delete
+    with_store({"aiden" => AIDEN}) do |store|
+      id = FixtureData.seed_group(store, name: ProTacts::Store::EVERYONE, members: ["aiden"])
+
+      assert_raises(ProTacts::Store::EveryonesBookName) { store.delete_group(id) }
+
+      assert_equal [id], store.groups_of("aiden").map(&:id)
+      assert_equal %w[put], store.changes_of("aiden").map(&:action)
+    end
+  end
+
+  def test_deleting_a_group_nobody_has_is_no_delete
+    with_store({"aiden" => AIDEN}) do |store|
+      refute store.delete_group("nobody")
+    end
+  end
+
   def test_a_regroup_leaves_and_joins
     with_store({"aiden" => AIDEN}) do |store|
       left = FixtureData.seed_group(store, members: ["aiden"], lines: [HOUSEHOLD_ADDRESS])