api-surface #1
12 changed files with 621 additions and 11 deletions
feat(api): commands, emojis/stickers, moderation, users, misc rollout — full surface
commit
6aec66f5ad
|
|
@ -408,6 +408,8 @@ defmodule Dexcord.Api do
|
|||
Dexcord.Api.Users,
|
||||
Dexcord.Api.Interactions,
|
||||
Dexcord.Api.Commands,
|
||||
Dexcord.Api.EmojisStickers,
|
||||
Dexcord.Api.Moderation,
|
||||
Dexcord.Api.Misc
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -19,4 +19,57 @@ defmodule Dexcord.Api.Commands do
|
|||
"/applications/:application_id/guilds/:guild_id/commands",
|
||||
body: true,
|
||||
returns: [Dexcord.ApplicationCommand]
|
||||
|
||||
endpoint :get_global_commands, :get, "/applications/:application_id/commands",
|
||||
query: [:with_localizations],
|
||||
returns: [Dexcord.ApplicationCommand]
|
||||
|
||||
endpoint :create_global_command, :post, "/applications/:application_id/commands",
|
||||
body: true,
|
||||
returns: Dexcord.ApplicationCommand
|
||||
|
||||
endpoint :get_global_command, :get, "/applications/:application_id/commands/:command_id",
|
||||
returns: Dexcord.ApplicationCommand
|
||||
|
||||
endpoint :edit_global_command, :patch, "/applications/:application_id/commands/:command_id",
|
||||
body: true,
|
||||
returns: Dexcord.ApplicationCommand
|
||||
|
||||
endpoint :delete_global_command, :delete, "/applications/:application_id/commands/:command_id",
|
||||
returns: nil
|
||||
|
||||
endpoint :get_guild_commands, :get, "/applications/:application_id/guilds/:guild_id/commands",
|
||||
query: [:with_localizations],
|
||||
returns: [Dexcord.ApplicationCommand]
|
||||
|
||||
endpoint :create_guild_command,
|
||||
:post,
|
||||
"/applications/:application_id/guilds/:guild_id/commands",
|
||||
body: true,
|
||||
returns: Dexcord.ApplicationCommand
|
||||
|
||||
endpoint :get_guild_command,
|
||||
:get,
|
||||
"/applications/:application_id/guilds/:guild_id/commands/:command_id",
|
||||
returns: Dexcord.ApplicationCommand
|
||||
|
||||
endpoint :edit_guild_command,
|
||||
:patch,
|
||||
"/applications/:application_id/guilds/:guild_id/commands/:command_id",
|
||||
body: true,
|
||||
returns: Dexcord.ApplicationCommand
|
||||
|
||||
endpoint :delete_guild_command,
|
||||
:delete,
|
||||
"/applications/:application_id/guilds/:guild_id/commands/:command_id", returns: nil
|
||||
|
||||
endpoint :get_guild_command_permissions,
|
||||
:get,
|
||||
"/applications/:application_id/guilds/:guild_id/commands/permissions",
|
||||
returns: [Dexcord.GuildApplicationCommandPermissions]
|
||||
|
||||
endpoint :get_command_permissions,
|
||||
:get,
|
||||
"/applications/:application_id/guilds/:guild_id/commands/:command_id/permissions",
|
||||
returns: Dexcord.GuildApplicationCommandPermissions
|
||||
end
|
||||
|
|
|
|||
69
lib/dexcord/api/emojis_stickers.ex
Normal file
69
lib/dexcord/api/emojis_stickers.ex
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
defmodule Dexcord.Api.EmojisStickers do
|
||||
@moduledoc """
|
||||
Emoji and sticker endpoints, declared with the `Dexcord.Api.Endpoint` DSL —
|
||||
mirrors the Discord "Emoji" and "Sticker" resource pages.
|
||||
|
||||
Emoji create/edit bodies carry base64 `image` data in plain JSON (no
|
||||
multipart). `create_guild_sticker` is the exception: it uses plain form-field
|
||||
multipart (`files: :form`) with `name`/`description`/`tags` parts plus a single
|
||||
`file` part. `list_application_emojis` and the sticker-pack listings return
|
||||
`{"items": [...]}` / pack envelopes with no model (`:raw`).
|
||||
"""
|
||||
use Dexcord.Api.Endpoint
|
||||
|
||||
endpoint :list_guild_emojis, :get, "/guilds/:guild_id/emojis", returns: [Dexcord.Emoji]
|
||||
endpoint :get_guild_emoji, :get, "/guilds/:guild_id/emojis/:emoji_id", returns: Dexcord.Emoji
|
||||
|
||||
endpoint :create_guild_emoji, :post, "/guilds/:guild_id/emojis",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.Emoji
|
||||
|
||||
endpoint :edit_guild_emoji, :patch, "/guilds/:guild_id/emojis/:emoji_id",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.Emoji
|
||||
|
||||
endpoint :delete_guild_emoji, :delete, "/guilds/:guild_id/emojis/:emoji_id",
|
||||
reason: true,
|
||||
returns: nil
|
||||
|
||||
endpoint :list_application_emojis, :get, "/applications/:application_id/emojis", returns: :raw
|
||||
|
||||
endpoint :get_application_emoji, :get, "/applications/:application_id/emojis/:emoji_id",
|
||||
returns: Dexcord.Emoji
|
||||
|
||||
endpoint :create_application_emoji, :post, "/applications/:application_id/emojis",
|
||||
body: true,
|
||||
returns: Dexcord.Emoji
|
||||
|
||||
endpoint :edit_application_emoji, :patch, "/applications/:application_id/emojis/:emoji_id",
|
||||
body: true,
|
||||
returns: Dexcord.Emoji
|
||||
|
||||
endpoint :delete_application_emoji, :delete, "/applications/:application_id/emojis/:emoji_id",
|
||||
returns: nil
|
||||
|
||||
endpoint :get_sticker, :get, "/stickers/:sticker_id", returns: Dexcord.Sticker
|
||||
endpoint :list_sticker_packs, :get, "/sticker-packs", returns: :raw
|
||||
endpoint :get_sticker_pack, :get, "/sticker-packs/:pack_id", returns: :raw
|
||||
endpoint :list_guild_stickers, :get, "/guilds/:guild_id/stickers", returns: [Dexcord.Sticker]
|
||||
|
||||
endpoint :get_guild_sticker, :get, "/guilds/:guild_id/stickers/:sticker_id",
|
||||
returns: Dexcord.Sticker
|
||||
|
||||
endpoint :create_guild_sticker, :post, "/guilds/:guild_id/stickers",
|
||||
body: true,
|
||||
files: :form,
|
||||
reason: true,
|
||||
returns: Dexcord.Sticker
|
||||
|
||||
endpoint :edit_guild_sticker, :patch, "/guilds/:guild_id/stickers/:sticker_id",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.Sticker
|
||||
|
||||
endpoint :delete_guild_sticker, :delete, "/guilds/:guild_id/stickers/:sticker_id",
|
||||
reason: true,
|
||||
returns: nil
|
||||
end
|
||||
|
|
@ -3,17 +3,29 @@ defmodule Dexcord.Api.Misc do
|
|||
Gateway / application-info endpoints, declared with the
|
||||
`Dexcord.Api.Endpoint` DSL.
|
||||
|
||||
`get_gateway_bot/0` stays `:raw` — its `url` / `shards` / `session_start_limit`
|
||||
shape has no model and the gateway reads it as a raw map.
|
||||
`get_current_application/0` returns a `Dexcord.ApplicationInfo`.
|
||||
`get_gateway/0` and `get_gateway_bot/0` stay `:raw` — their
|
||||
`url` / `shards` / `session_start_limit` shapes have no model and the gateway
|
||||
reads them as raw maps.
|
||||
|
||||
`get_current_application/0` hits the docs-canonical `GET /applications/@me`;
|
||||
the older `GET /oauth2/applications/@me` route is preserved as
|
||||
`get_current_bot_application/0`. Both return a `Dexcord.ApplicationInfo`.
|
||||
|
||||
`get_guild` moved to `Dexcord.Api.Guilds` — the `Dexcord.Api` facade delegate
|
||||
follows the group automatically.
|
||||
"""
|
||||
use Dexcord.Api.Endpoint
|
||||
|
||||
endpoint :get_gateway, :get, "/gateway", returns: :raw
|
||||
endpoint :get_gateway_bot, :get, "/gateway/bot", returns: :raw
|
||||
endpoint :get_current_application, :get, "/applications/@me", returns: Dexcord.ApplicationInfo
|
||||
|
||||
endpoint :get_current_application, :get, "/oauth2/applications/@me",
|
||||
endpoint :edit_current_application, :patch, "/applications/@me",
|
||||
body: true,
|
||||
returns: Dexcord.ApplicationInfo
|
||||
|
||||
endpoint :get_current_bot_application, :get, "/oauth2/applications/@me",
|
||||
returns: Dexcord.ApplicationInfo
|
||||
|
||||
endpoint :list_voice_regions, :get, "/voice/regions", returns: [Dexcord.VoiceRegion]
|
||||
end
|
||||
|
|
|
|||
69
lib/dexcord/api/moderation.ex
Normal file
69
lib/dexcord/api/moderation.ex
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
defmodule Dexcord.Api.Moderation do
|
||||
@moduledoc """
|
||||
Auto-moderation and guild-scheduled-event endpoints, declared with the
|
||||
`Dexcord.Api.Endpoint` DSL — mirrors the Discord "Auto Moderation" and
|
||||
"Guild Scheduled Event" resource pages.
|
||||
|
||||
`delete_guild_scheduled_event` deliberately has NO `reason:` support (the docs
|
||||
are asymmetric — create/edit take a reason, delete does not).
|
||||
"""
|
||||
use Dexcord.Api.Endpoint
|
||||
|
||||
endpoint :list_auto_moderation_rules, :get, "/guilds/:guild_id/auto-moderation/rules",
|
||||
returns: [Dexcord.AutoModerationRule]
|
||||
|
||||
endpoint :get_auto_moderation_rule,
|
||||
:get,
|
||||
"/guilds/:guild_id/auto-moderation/rules/:auto_moderation_rule_id",
|
||||
returns: Dexcord.AutoModerationRule
|
||||
|
||||
endpoint :create_auto_moderation_rule, :post, "/guilds/:guild_id/auto-moderation/rules",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.AutoModerationRule
|
||||
|
||||
endpoint :edit_auto_moderation_rule,
|
||||
:patch,
|
||||
"/guilds/:guild_id/auto-moderation/rules/:auto_moderation_rule_id",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.AutoModerationRule
|
||||
|
||||
endpoint :delete_auto_moderation_rule,
|
||||
:delete,
|
||||
"/guilds/:guild_id/auto-moderation/rules/:auto_moderation_rule_id",
|
||||
reason: true,
|
||||
returns: nil
|
||||
|
||||
endpoint :list_guild_scheduled_events, :get, "/guilds/:guild_id/scheduled-events",
|
||||
query: [:with_user_count],
|
||||
returns: [Dexcord.GuildScheduledEvent]
|
||||
|
||||
endpoint :create_guild_scheduled_event, :post, "/guilds/:guild_id/scheduled-events",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.GuildScheduledEvent
|
||||
|
||||
endpoint :get_guild_scheduled_event,
|
||||
:get,
|
||||
"/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id",
|
||||
query: [:with_user_count],
|
||||
returns: Dexcord.GuildScheduledEvent
|
||||
|
||||
endpoint :edit_guild_scheduled_event,
|
||||
:patch,
|
||||
"/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id",
|
||||
body: true,
|
||||
reason: true,
|
||||
returns: Dexcord.GuildScheduledEvent
|
||||
|
||||
endpoint :delete_guild_scheduled_event,
|
||||
:delete,
|
||||
"/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id", returns: nil
|
||||
|
||||
endpoint :get_guild_scheduled_event_users,
|
||||
:get,
|
||||
"/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id/users",
|
||||
query: [:limit, :with_member, :before, :after],
|
||||
returns: [Dexcord.GuildScheduledEventUser]
|
||||
end
|
||||
|
|
@ -13,6 +13,20 @@ defmodule Dexcord.Api.Users do
|
|||
endpoint :get_current_user, :get, "/users/@me", returns: Dexcord.User
|
||||
endpoint :get_user, :get, "/users/:user_id", returns: Dexcord.User
|
||||
|
||||
endpoint :edit_current_user, :patch, "/users/@me", body: true, returns: Dexcord.User
|
||||
|
||||
endpoint :get_current_user_guilds, :get, "/users/@me/guilds",
|
||||
query: [:before, :after, :limit, :with_counts],
|
||||
returns: [Dexcord.PartialGuild]
|
||||
|
||||
endpoint :get_current_user_guild_member, :get, "/users/@me/guilds/:guild_id/member",
|
||||
returns: Dexcord.Member
|
||||
|
||||
endpoint :leave_guild, :delete, "/users/@me/guilds/:guild_id", returns: nil
|
||||
|
||||
endpoint :get_current_user_connections, :get, "/users/@me/connections",
|
||||
returns: [Dexcord.Connection]
|
||||
|
||||
@create_dm_spec %{
|
||||
name: :create_dm,
|
||||
method: :post,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Dexcord.Slash.Registrar do
|
|||
|
||||
On start it:
|
||||
|
||||
1. calls `GET /oauth2/applications/@me` to learn the application id and caches
|
||||
1. calls `GET /applications/@me` to learn the application id and caches
|
||||
it via `Dexcord.Config.put_application_id/1` (a dedicated `:persistent_term`
|
||||
key, so any process can read it back with `Dexcord.Config.application_id/0`);
|
||||
2. bulk-overwrites the module's `commands/0`:
|
||||
|
|
@ -114,7 +114,7 @@ defmodule Dexcord.Slash.Registrar do
|
|||
|
||||
other ->
|
||||
{:error,
|
||||
"could not resolve the application id (GET /oauth2/applications/@me): " <>
|
||||
"could not resolve the application id (GET /applications/@me): " <>
|
||||
inspect(other)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
140
test/dexcord/api_commands_test.exs
Normal file
140
test/dexcord/api_commands_test.exs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
defmodule Dexcord.ApiCommandsTest do
|
||||
@moduledoc """
|
||||
Representative wire tests for the extended `Dexcord.Api.Commands` group
|
||||
(Phase 5 Task 5), including a struct body encoded via `to_map/1` with a
|
||||
recursive (sub-command-group) options tree.
|
||||
"""
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Dexcord.Api
|
||||
alias Dexcord.Api.Ratelimit
|
||||
alias Dexcord.FakeRest
|
||||
|
||||
@token "test.token.value"
|
||||
|
||||
setup do
|
||||
Dexcord.EnvSandbox.sandbox_env()
|
||||
Dexcord.Config.put(%{token: @token, handler: nil, intents: 0})
|
||||
|
||||
start_supervised!({Finch, name: Dexcord.Finch})
|
||||
start_supervised!(Ratelimit)
|
||||
start_supervised!(FakeRest)
|
||||
|
||||
Application.put_env(:dexcord, :api_base_url, FakeRest.base_url())
|
||||
FakeRest.subscribe(self())
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "get_global_commands forwards with_localizations and decodes a list" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/applications/1/commands",
|
||||
FakeRest.resp(200, body: ~s([{"id":"2","name":"ping"}]))
|
||||
)
|
||||
|
||||
assert {:ok, [%Dexcord.ApplicationCommand{id: 2, name: "ping"}]} =
|
||||
Api.get_global_commands(1, with_localizations: true)
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
assert info.query_string == "with_localizations=true"
|
||||
end
|
||||
|
||||
test "create_global_command encodes a recursive ApplicationCommand struct body" do
|
||||
FakeRest.stub(
|
||||
:post,
|
||||
"/applications/1/commands",
|
||||
FakeRest.resp(201, body: ~s({"id":"7","name":"config"}))
|
||||
)
|
||||
|
||||
command = %Dexcord.ApplicationCommand{
|
||||
name: "config",
|
||||
description: "configure the bot",
|
||||
options: [
|
||||
%Dexcord.ApplicationCommand.Option{
|
||||
type: :sub_command_group,
|
||||
name: "group",
|
||||
description: "a group",
|
||||
options: [
|
||||
%Dexcord.ApplicationCommand.Option{
|
||||
type: :sub_command,
|
||||
name: "sub",
|
||||
description: "a sub-command"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert {:ok, %Dexcord.ApplicationCommand{id: 7, name: "config"}} =
|
||||
Api.create_global_command(1, command)
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
body = JSON.decode!(info.body)
|
||||
assert body["name"] == "config"
|
||||
assert [group] = body["options"]
|
||||
# sub_command_group encodes to 2, the nested sub_command to 1.
|
||||
assert group["type"] == 2
|
||||
assert group["name"] == "group"
|
||||
assert [sub] = group["options"]
|
||||
assert sub["type"] == 1
|
||||
assert sub["name"] == "sub"
|
||||
end
|
||||
|
||||
test "edit_global_command PATCHes and decodes an ApplicationCommand" do
|
||||
FakeRest.stub(
|
||||
:patch,
|
||||
"/applications/1/commands/2",
|
||||
FakeRest.resp(200, body: ~s({"id":"2","name":"renamed"}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.ApplicationCommand{id: 2, name: "renamed"}} =
|
||||
Api.edit_global_command(1, 2, %{"name" => "renamed"})
|
||||
end
|
||||
|
||||
test "delete_global_command returns nil" do
|
||||
FakeRest.stub(:delete, "/applications/1/commands/2", FakeRest.resp(204))
|
||||
|
||||
assert {:ok, nil} = Api.delete_global_command(1, 2)
|
||||
end
|
||||
|
||||
test "create_guild_command decodes an ApplicationCommand" do
|
||||
FakeRest.stub(
|
||||
:post,
|
||||
"/applications/1/guilds/9/commands",
|
||||
FakeRest.resp(201, body: ~s({"id":"3","name":"g"}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.ApplicationCommand{id: 3, name: "g"}} =
|
||||
Api.create_guild_command(1, 9, %{"name" => "g", "description" => "d"})
|
||||
end
|
||||
|
||||
test "get_guild_command_permissions decodes a list of permission structs" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/applications/1/guilds/9/commands/permissions",
|
||||
FakeRest.resp(200,
|
||||
body: ~s([{"id":"2","permissions":[{"id":"5","type":1,"permission":true}]}])
|
||||
)
|
||||
)
|
||||
|
||||
assert {:ok,
|
||||
[
|
||||
%Dexcord.GuildApplicationCommandPermissions{
|
||||
id: 2,
|
||||
permissions: [%Dexcord.ApplicationCommandPermission{id: 5, permission: true}]
|
||||
}
|
||||
]} = Api.get_guild_command_permissions(1, 9)
|
||||
end
|
||||
|
||||
test "get_command_permissions decodes a single permissions struct" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/applications/1/guilds/9/commands/2/permissions",
|
||||
FakeRest.resp(200, body: ~s({"id":"2","permissions":[]}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.GuildApplicationCommandPermissions{id: 2, permissions: []}} =
|
||||
Api.get_command_permissions(1, 9, 2)
|
||||
end
|
||||
end
|
||||
|
|
@ -75,6 +75,8 @@ defmodule Dexcord.ApiFacadeTest do
|
|||
Dexcord.Api.Users,
|
||||
Dexcord.Api.Interactions,
|
||||
Dexcord.Api.Commands,
|
||||
Dexcord.Api.EmojisStickers,
|
||||
Dexcord.Api.Moderation,
|
||||
Dexcord.Api.Misc
|
||||
]
|
||||
end
|
||||
|
|
|
|||
248
test/dexcord/api_moderation_test.exs
Normal file
248
test/dexcord/api_moderation_test.exs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
defmodule Dexcord.ApiModerationTest do
|
||||
@moduledoc """
|
||||
Representative wire tests for the Phase 5 Task 5 groups: `Dexcord.Api.Moderation`
|
||||
(auto-moderation + scheduled events), `Dexcord.Api.EmojisStickers` (base64 JSON
|
||||
emoji bodies vs. plain-form sticker multipart), the `Dexcord.Api.Users`
|
||||
additions, and the `Dexcord.Api.Misc` `get_current_application` re-point.
|
||||
"""
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Dexcord.Api
|
||||
alias Dexcord.Api.Ratelimit
|
||||
alias Dexcord.FakeRest
|
||||
|
||||
@token "test.token.value"
|
||||
|
||||
setup do
|
||||
Dexcord.EnvSandbox.sandbox_env()
|
||||
Dexcord.Config.put(%{token: @token, handler: nil, intents: 0})
|
||||
|
||||
start_supervised!({Finch, name: Dexcord.Finch})
|
||||
start_supervised!(Ratelimit)
|
||||
start_supervised!(FakeRest)
|
||||
|
||||
Application.put_env(:dexcord, :api_base_url, FakeRest.base_url())
|
||||
FakeRest.subscribe(self())
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
# --- Moderation: auto-moderation ---------------------------------------
|
||||
|
||||
test "create_auto_moderation_rule posts a body and decodes the rule with a reason" do
|
||||
FakeRest.stub(
|
||||
:post,
|
||||
"/guilds/1/auto-moderation/rules",
|
||||
FakeRest.resp(200, body: ~s({"id":"5","name":"no-spam","trigger_type":1}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.AutoModerationRule{id: 5, name: "no-spam"}} =
|
||||
Api.create_auto_moderation_rule(1, %{"name" => "no-spam"}, reason: "policy")
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
assert Map.new(info.headers)["x-audit-log-reason"] == "policy"
|
||||
end
|
||||
|
||||
test "list_auto_moderation_rules decodes a list of rules" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/guilds/1/auto-moderation/rules",
|
||||
FakeRest.resp(200, body: ~s([{"id":"5","name":"a"},{"id":"6","name":"b"}]))
|
||||
)
|
||||
|
||||
assert {:ok, [%Dexcord.AutoModerationRule{id: 5}, %Dexcord.AutoModerationRule{id: 6}]} =
|
||||
Api.list_auto_moderation_rules(1)
|
||||
end
|
||||
|
||||
# --- Moderation: scheduled events --------------------------------------
|
||||
|
||||
test "get_guild_scheduled_event forwards with_user_count and decodes the event" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/guilds/1/scheduled-events/2",
|
||||
FakeRest.resp(200, body: ~s({"id":"2","name":"party","user_count":10}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.GuildScheduledEvent{id: 2, name: "party", user_count: 10}} =
|
||||
Api.get_guild_scheduled_event(1, 2, with_user_count: true)
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
assert info.query_string == "with_user_count=true"
|
||||
end
|
||||
|
||||
test "delete_guild_scheduled_event returns nil (no reason support)" do
|
||||
FakeRest.stub(:delete, "/guilds/1/scheduled-events/2", FakeRest.resp(204))
|
||||
|
||||
assert {:ok, nil} = Api.delete_guild_scheduled_event(1, 2)
|
||||
end
|
||||
|
||||
# --- Emojis / Stickers -------------------------------------------------
|
||||
|
||||
test "create_guild_emoji sends a base64 image in a plain JSON body (NOT multipart)" do
|
||||
FakeRest.stub(
|
||||
:post,
|
||||
"/guilds/1/emojis",
|
||||
FakeRest.resp(200, body: ~s({"id":"9","name":"blob"}))
|
||||
)
|
||||
|
||||
image = "data:image/png;base64,iVBORw0KGgo="
|
||||
|
||||
assert {:ok, %Dexcord.Emoji{id: 9, name: "blob"}} =
|
||||
Api.create_guild_emoji(1, %{"name" => "blob", "image" => image}, reason: "add")
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
headers = Map.new(info.headers)
|
||||
assert headers["content-type"] == "application/json"
|
||||
assert JSON.decode!(info.body) == %{"name" => "blob", "image" => image}
|
||||
end
|
||||
|
||||
test "create_guild_sticker sends plain-form multipart (name/description/tags + file)" do
|
||||
FakeRest.stub(
|
||||
:post,
|
||||
"/guilds/1/stickers",
|
||||
FakeRest.resp(201, body: ~s({"id":"9","name":"wave"}))
|
||||
)
|
||||
|
||||
file = %{filename: "wave.png", data: <<137, 80, 78, 71>>, content_type: "image/png"}
|
||||
|
||||
assert {:ok, %Dexcord.Sticker{id: 9, name: "wave"}} =
|
||||
Api.create_guild_sticker(
|
||||
1,
|
||||
%{"name" => "wave", "description" => "a wave", "tags" => "wave"},
|
||||
files: [file],
|
||||
reason: "add sticker"
|
||||
)
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
headers = Map.new(info.headers)
|
||||
assert "multipart/form-data; boundary=" <> boundary = headers["content-type"]
|
||||
assert headers["x-audit-log-reason"] == "add%20sticker"
|
||||
|
||||
named = info.body |> parse_multipart(boundary) |> by_name()
|
||||
|
||||
assert named["name"].data == "wave"
|
||||
assert named["name"].filename == nil
|
||||
assert named["description"].data == "a wave"
|
||||
assert named["tags"].data == "wave"
|
||||
|
||||
assert %{"file" => file_part} = named
|
||||
assert file_part.filename == "wave.png"
|
||||
assert file_part.content_type == "image/png"
|
||||
assert file_part.data == <<137, 80, 78, 71>>
|
||||
end
|
||||
|
||||
test "list_application_emojis returns the raw {items: [...]} envelope" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/applications/1/emojis",
|
||||
FakeRest.resp(200, body: ~s({"items":[]}))
|
||||
)
|
||||
|
||||
assert {:ok, %{"items" => []}} = Api.list_application_emojis(1)
|
||||
end
|
||||
|
||||
# --- Users -------------------------------------------------------------
|
||||
|
||||
test "edit_current_user PATCHes and decodes a User" do
|
||||
FakeRest.stub(
|
||||
:patch,
|
||||
"/users/@me",
|
||||
FakeRest.resp(200, body: ~s({"id":"42","username":"bot"}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.User{id: 42, username: "bot"}} =
|
||||
Api.edit_current_user(%{"username" => "bot"})
|
||||
end
|
||||
|
||||
test "get_current_user_guilds forwards the query and decodes PartialGuilds" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/users/@me/guilds",
|
||||
FakeRest.resp(200, body: ~s([{"id":"1","name":"g1"},{"id":"2","name":"g2"}]))
|
||||
)
|
||||
|
||||
assert {:ok, [%Dexcord.PartialGuild{id: 1, name: "g1"}, %Dexcord.PartialGuild{id: 2}]} =
|
||||
Api.get_current_user_guilds(limit: 100, with_counts: true)
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
assert URI.decode_query(info.query_string) == %{"limit" => "100", "with_counts" => "true"}
|
||||
end
|
||||
|
||||
test "leave_guild returns nil" do
|
||||
FakeRest.stub(:delete, "/users/@me/guilds/1", FakeRest.resp(204))
|
||||
|
||||
assert {:ok, nil} = Api.leave_guild(1)
|
||||
end
|
||||
|
||||
# --- Misc: application-info re-point ------------------------------------
|
||||
|
||||
test "get_current_application now hits the canonical /applications/@me route" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/applications/@me",
|
||||
FakeRest.resp(200, body: ~s({"id":"99","name":"App"}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.ApplicationInfo{id: 99}} = Api.get_current_application()
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
assert info.path == "/applications/@me"
|
||||
end
|
||||
|
||||
test "get_current_bot_application keeps the oauth2 route" do
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/oauth2/applications/@me",
|
||||
FakeRest.resp(200, body: ~s({"id":"99","name":"App"}))
|
||||
)
|
||||
|
||||
assert {:ok, %Dexcord.ApplicationInfo{id: 99}} = Api.get_current_bot_application()
|
||||
|
||||
assert_receive {:rest_hit, info}
|
||||
assert info.path == "/oauth2/applications/@me"
|
||||
end
|
||||
|
||||
# --- multipart parsing helpers (copied from api_integration_test.exs) ---
|
||||
|
||||
defp parse_multipart(body, boundary) do
|
||||
body
|
||||
|> String.split("--" <> boundary)
|
||||
|> Enum.drop(1)
|
||||
|> Enum.reject(fn seg -> seg == "--\r\n" or seg == "--" end)
|
||||
|> Enum.map(fn seg ->
|
||||
seg = String.replace_prefix(seg, "\r\n", "")
|
||||
[raw_headers, rest] = String.split(seg, "\r\n\r\n", parts: 2)
|
||||
data = String.replace_suffix(rest, "\r\n", "")
|
||||
disposition = header_value(raw_headers, "content-disposition")
|
||||
|
||||
%{
|
||||
name: extract(disposition, ~r/name="([^"]*)"/),
|
||||
filename: extract(disposition, ~r/filename="([^"]*)"/),
|
||||
content_type: header_value(raw_headers, "content-type"),
|
||||
data: data
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp by_name(parts), do: Map.new(parts, fn part -> {part.name, part} end)
|
||||
|
||||
defp header_value(raw_headers, name) do
|
||||
raw_headers
|
||||
|> String.split("\r\n")
|
||||
|> Enum.find_value(fn line ->
|
||||
case String.split(line, ": ", parts: 2) do
|
||||
[key, value] -> if String.downcase(key) == name, do: value
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp extract(nil, _re), do: nil
|
||||
|
||||
defp extract(string, re) do
|
||||
case Regex.run(re, string) do
|
||||
[_, captured] -> captured
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -40,10 +40,11 @@ defmodule Dexcord.RegistrarIntegrationTest do
|
|||
# The application/guild ids are numeric snowflakes: `get_current_application`
|
||||
# now decodes to `%Dexcord.ApplicationInfo{}` (id cast to an integer) and the
|
||||
# `bulk_overwrite_*` endpoints cast their `*_id` path params via
|
||||
# `Dexcord.Snowflake.cast/1`.
|
||||
# `Dexcord.Snowflake.cast/1`. As of Phase 5 Task 5, `get_current_application`
|
||||
# is re-pointed to the docs-canonical `GET /applications/@me`.
|
||||
FakeRest.stub(
|
||||
:get,
|
||||
"/oauth2/applications/@me",
|
||||
"/applications/@me",
|
||||
FakeRest.resp(200, body: ~s({"id":"99"}))
|
||||
)
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ defmodule Dexcord.RegistrarIntegrationTest do
|
|||
|
||||
assert Dexcord.Config.application_id() == 99
|
||||
|
||||
assert_receive {:rest_hit, %{method: "GET", path: "/oauth2/applications/@me"}}
|
||||
assert_receive {:rest_hit, %{method: "GET", path: "/applications/@me"}}
|
||||
|
||||
assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands", body: body}}
|
||||
|
||||
|
|
@ -87,7 +88,7 @@ defmodule Dexcord.RegistrarIntegrationTest do
|
|||
assert Registrar.run(config) == :ok
|
||||
end)
|
||||
|
||||
assert_receive {:rest_hit, %{method: "GET", path: "/oauth2/applications/@me"}}
|
||||
assert_receive {:rest_hit, %{method: "GET", path: "/applications/@me"}}
|
||||
# The global-commands overwrite must NOT be attempted for an empty list.
|
||||
refute_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands"}}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ defmodule Dexcord.RegistrarTreeTest do
|
|||
Application.put_env(:dexcord, :api_base_url, FakeRest.base_url())
|
||||
FakeRest.subscribe(self())
|
||||
|
||||
FakeRest.stub(:get, "/oauth2/applications/@me", FakeRest.resp(200, body: ~s({"id":"1"})))
|
||||
FakeRest.stub(:get, "/applications/@me", FakeRest.resp(200, body: ~s({"id":"1"})))
|
||||
# Registration fails forever with a 4xx.
|
||||
FakeRest.stub(:put, "/applications/1/commands", FakeRest.resp(403, body: "forbidden"))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue