Skip to content

Commit f9b54b9

Browse files
johnnytclaude
andauthored
Adds Groups.delete/2 (#20)
- Implements delete operation for Groups module - Fixes Planner tests to add ROPC user as both owner and member of groups (Planner requirement) - Increases wait time to 8 seconds for Azure group membership propagation - Handles Planner API returning 204 No Content for updates in Plans and Tasks modules - Adds Tasks.ReadWrite scope to AuthTestHelpers default scopes - Fixes auth test to check headers at correct location in Req.Request struct - Updates quality task to properly detect and report test failures These changes resolve failing integration tests and complete CRUD operations for Groups. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3f66c58 commit f9b54b9

9 files changed

Lines changed: 153 additions & 31 deletions

File tree

lib/mix/tasks/quality.ex

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,34 @@ defmodule Mix.Tasks.Quality do
270270
defp run_coverage_check do
271271
Mix.shell().info("📊 Running test coverage check...")
272272

273-
# Run coveralls with mix task to show output
274-
try do
275-
Mix.Task.run("coveralls", [])
276-
Mix.shell().info("✅ Coverage check passed")
277-
rescue
278-
e in Mix.Error ->
279-
Mix.shell().error("❌ Coverage check failed.")
280-
Mix.shell().info("💡 Run 'MIX_ENV=test mix coveralls.detail' to see uncovered lines")
281-
282-
Mix.shell().info(
283-
"💡 Add more tests to increase coverage above threshold set in coveralls.json"
284-
)
285-
286-
Mix.raise("Coverage check failed: #{Exception.message(e)}")
273+
# Run coveralls using System.cmd to capture output and exit code
274+
case System.cmd("mix", ["coveralls"], stderr_to_stdout: true, env: [{"MIX_ENV", "test"}]) do
275+
{output, 0} ->
276+
# Show the output to user
277+
Mix.shell().info(output)
278+
Mix.shell().info("✅ Coverage check passed")
279+
280+
{output, exit_code} ->
281+
# Show the output to user
282+
Mix.shell().info(output)
283+
284+
# Check if it's a test failure vs coverage failure
285+
cond do
286+
String.contains?(output, "tests, ") and
287+
(String.contains?(output, "failure") or String.contains?(output, "error")) ->
288+
Mix.shell().error("❌ Test failures detected.")
289+
Mix.raise("Tests failed - fix failing tests before proceeding")
290+
291+
true ->
292+
Mix.shell().error("❌ Coverage check failed.")
293+
Mix.shell().info("💡 Run 'MIX_ENV=test mix coveralls.detail' to see uncovered lines")
294+
295+
Mix.shell().info(
296+
"💡 Add more tests to increase coverage above threshold set in coveralls.json"
297+
)
298+
299+
Mix.raise("Coverage check failed with exit code #{exit_code}")
300+
end
287301
end
288302
end
289303

lib/msg/groups.ex

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,52 @@ defmodule Msg.Groups do
335335
end
336336
end
337337

338+
@doc """
339+
Deletes a Microsoft 365 Group.
340+
341+
**Warning:** This is a destructive operation that permanently removes the group and all
342+
associated resources including the group mailbox, SharePoint site, Planner plans,
343+
calendar, and Teams (if connected). Deleted groups may be recoverable from the
344+
recycle bin for up to 30 days.
345+
346+
## Parameters
347+
348+
- `client` - Authenticated Req.Request client
349+
- `group_id` - ID of the group to delete
350+
351+
## Returns
352+
353+
- `:ok` - Group deleted successfully (204 status)
354+
- `{:error, :not_found}` - Group doesn't exist
355+
- `{:error, :forbidden}` - Insufficient permissions
356+
- `{:error, term}` - Other errors
357+
358+
## Required Permissions
359+
360+
- `Group.ReadWrite.All` (application permission)
361+
362+
## Examples
363+
364+
:ok = Msg.Groups.delete(client, "group-id-123")
365+
366+
## See Also
367+
368+
- [Delete Group API](https://learn.microsoft.com/en-us/graph/api/group-delete)
369+
"""
370+
@spec delete(Req.Request.t(), String.t()) :: :ok | {:error, term()}
371+
def delete(client, group_id) do
372+
case Req.delete(client, url: "/groups/#{group_id}") do
373+
{:ok, %{status: 204}} ->
374+
:ok
375+
376+
{:ok, %{status: status, body: body}} ->
377+
handle_error(status, body)
378+
379+
{:error, reason} ->
380+
{:error, reason}
381+
end
382+
end
383+
338384
# Private functions
339385

340386
defp handle_error(401, _), do: {:error, :unauthorized}

lib/msg/planner/plans.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ defmodule Msg.Planner.Plans do
218218
json: updates_converted,
219219
headers: [{"If-Match", etag}]
220220
) do
221+
{:ok, %{status: 204}} ->
222+
# Planner API returns 204 No Content for updates
223+
# Fetch the updated plan to return it
224+
get(client, plan_id)
225+
221226
{:ok, %{status: status, body: body}} when status in 200..299 ->
222227
{:ok, body}
223228

lib/msg/planner/tasks.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ defmodule Msg.Planner.Tasks do
237237
json: updates_converted,
238238
headers: [{"If-Match", etag}]
239239
) do
240+
{:ok, %{status: 204}} ->
241+
# Planner API returns 204 No Content for updates
242+
# Fetch the updated task to return it
243+
get(client, task_id)
244+
240245
{:ok, %{status: status, body: body}} when status in 200..299 ->
241246
{:ok, body}
242247

test/msg/integration/auth_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ defmodule Msg.Integration.AuthTest do
193193
assert %Req.Request{} = delegated_client
194194
assert delegated_client.options.base_url == "https://graph.microsoft.com/v1.0"
195195

196-
# Verify it has authorization header
197-
assert Map.has_key?(delegated_client.options, :headers)
196+
# Verify it has authorization header (in Req, headers are at top level, not in options)
197+
assert Map.has_key?(delegated_client.headers, "authorization")
198198
else
199199
# Skip if no ROPC credentials or consent not granted
200200
assert true

test/msg/integration/groups_test.exs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ defmodule Msg.Integration.GroupsTest do
109109
assert "Unified" in group["groupTypes"]
110110

111111
# Clean up - delete the created group
112-
# Note: Group deletion requires DELETE /groups/{id}
113-
# We'll add cleanup in a separate operation
114-
cleanup_group(client, group["id"])
112+
assert :ok = Groups.delete(client, group["id"])
115113
end
116114

117115
test "handles error when getting non-existent group", %{client: client} do
@@ -195,15 +193,34 @@ defmodule Msg.Integration.GroupsTest do
195193
assert :ok = Groups.add_owner(client, group["id"], other_test_user_id)
196194

197195
# Clean up
198-
cleanup_group(client, group["id"])
196+
assert :ok = Groups.delete(client, group["id"])
199197
end
200198

201-
# Helper function to clean up test groups
202-
defp cleanup_group(client, group_id) do
199+
test "deletes a group successfully", %{client: client} do
200+
timestamp = System.system_time(:second)
201+
202+
attrs = %{
203+
display_name: "Test Group to Delete #{timestamp}",
204+
mail_enabled: true,
205+
mail_nickname: "test-delete-#{timestamp}",
206+
security_enabled: false,
207+
group_types: ["Unified"],
208+
description: "Temporary test group for delete operation"
209+
}
210+
211+
{:ok, group} = Groups.create(client, attrs)
212+
group_id = group["id"]
213+
203214
# Delete the group
204-
case Req.delete(client, url: "/groups/#{group_id}") do
205-
{:ok, %{status: 204}} -> :ok
206-
_ -> :ok
207-
end
215+
assert :ok = Groups.delete(client, group_id)
216+
217+
# Verify the group is deleted
218+
assert {:error, :not_found} = Groups.get(client, group_id)
219+
end
220+
221+
test "handles error when deleting non-existent group", %{client: client} do
222+
fake_id = "00000000-0000-0000-0000-000000000000"
223+
224+
assert {:error, :not_found} = Groups.delete(client, fake_id)
208225
end
209226
end

test/msg/integration/planner/plans_test.exs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,16 @@ defmodule Msg.Integration.Planner.PlansTest do
4343
{:ok, created_group} = Groups.create(app_client, group)
4444
group_id = created_group["id"]
4545

46-
# Give Azure some time to provision the group
47-
Process.sleep(2000)
46+
# Add the ROPC user as both owner and member (Planner requirement)
47+
system_user_id = System.get_env("MICROSOFT_TEST_USER_ID")
48+
49+
if system_user_id do
50+
:ok = Groups.add_owner(app_client, group_id, system_user_id)
51+
:ok = Groups.add_member(app_client, group_id, system_user_id)
52+
end
53+
54+
# Give Azure time to propagate group membership (needs 8+ seconds)
55+
Process.sleep(8000)
4856

4957
# Create plan (requires delegated permissions)
5058
{:ok, created_plan} =
@@ -113,7 +121,15 @@ defmodule Msg.Integration.Planner.PlansTest do
113121
{:ok, created_group} = Groups.create(app_client, group)
114122
group_id = created_group["id"]
115123

116-
Process.sleep(2000)
124+
# Add the ROPC user as both owner and member (Planner requirement)
125+
system_user_id = System.get_env("MICROSOFT_TEST_USER_ID")
126+
127+
if system_user_id do
128+
:ok = Groups.add_owner(app_client, group_id, system_user_id)
129+
:ok = Groups.add_member(app_client, group_id, system_user_id)
130+
end
131+
132+
Process.sleep(8000)
117133

118134
# Create a plan
119135
{:ok, plan} =

test/msg/integration/planner/tasks_test.exs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,16 @@ defmodule Msg.Integration.Planner.TasksTest do
3636
{:ok, created_group} = Groups.create(app_client, group)
3737
group_id = created_group["id"]
3838

39-
Process.sleep(2000)
39+
# Add the ROPC user as both owner and member (Planner requirement)
40+
system_user_id = System.get_env("MICROSOFT_TEST_USER_ID")
41+
42+
if system_user_id do
43+
:ok = Groups.add_owner(app_client, group_id, system_user_id)
44+
:ok = Groups.add_member(app_client, group_id, system_user_id)
45+
end
46+
47+
# Give Azure time to propagate group membership (needs 8+ seconds)
48+
Process.sleep(8000)
4049

4150
{:ok, plan} =
4251
Plans.create(delegated_client, %{
@@ -219,7 +228,16 @@ defmodule Msg.Integration.Planner.TasksTest do
219228
{:ok, created_group} = Groups.create(app_client, group)
220229
group_id = created_group["id"]
221230

222-
Process.sleep(2000)
231+
# Add the ROPC user as both owner and member (Planner requirement)
232+
system_user_id = System.get_env("MICROSOFT_TEST_USER_ID")
233+
234+
if system_user_id do
235+
:ok = Groups.add_owner(app_client, group_id, system_user_id)
236+
:ok = Groups.add_member(app_client, group_id, system_user_id)
237+
end
238+
239+
# Give Azure time to propagate group membership (needs 8+ seconds)
240+
Process.sleep(8000)
223241

224242
{:ok, plan} =
225243
Plans.create(delegated_client, %{

test/support/auth_test_helpers.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ defmodule Msg.AuthTestHelpers do
8181
Keyword.get(opts, :scopes, [
8282
"Calendars.ReadWrite.Shared",
8383
"Group.ReadWrite.All",
84+
"Tasks.ReadWrite",
8485
"offline_access"
8586
])
8687

0 commit comments

Comments
 (0)