store: delete a group
Assisted-by: GLM-5.3 via pi
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])