Skip to content

Commit fff4579

Browse files
authored
feat: add unit tests for discord invite resolver (#711)
1 parent 0406c4f commit fff4579

7 files changed

+376
-8
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Minor: Server perks are now sorted alphabetically. (#711)
6+
- Dev: Added unit tests for the Discord invite resolver. (#711)
7+
58
## 2.0.5
69

710
- Fix: Release script not working. (#704)
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package discord
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
7+
"github.com/go-chi/chi/v5"
8+
)
9+
10+
var (
11+
data_raw = map[string][]byte{}
12+
)
13+
14+
func init() {
15+
data_raw["bad"] = []byte(`xD`)
16+
data_raw["forsen"] = []byte(`{"type":0,"code":"forsen","expires_at":null,"flags":2,"guild":{"id":"97034666673975296","name":"Forsen","splash":"05b8f7eb7f06f11da324945b0bac65ee","banner":"a_b10dd2b4e2c25b002ad9c303432a373c","description":null,"icon":"a_ea433153b6ce120e0fb518efc084dc38","features":["SEVEN_DAY_THREAD_ARCHIVE","MEMBER_PROFILES","PRIVATE_THREADS","ANIMATED_ICON","VANITY_URL","THREE_DAY_THREAD_ARCHIVE","ROLE_ICONS","AUTO_MODERATION","ANIMATED_BANNER","NEW_THREAD_PERMISSIONS","INVITE_SPLASH","THREADS_ENABLED","CHANNEL_ICON_EMOJIS_GENERATED","NON_COMMUNITY_RAID_ALERTS","BANNER","SOUNDBOARD"],"verification_level":3,"vanity_url_code":"forsen","nsfw_level":0,"nsfw":false,"premium_subscription_count":107},"guild_id":"97034666673975296","channel":{"id":"97034666673975296","type":0,"name":"readme"},"approximate_member_count":44960,"approximate_presence_count":13730}`)
17+
data_raw["qbRE8WR"] = []byte(`{"type":0,"code":"qbRE8WR","inviter":{"id":"85699361769553920","username":"pajlada","avatar":"e75df3dbe6cb04b3c9f0e090b3adb190","discriminator":"0","public_flags":512,"flags":512,"banner":null,"accent_color":13387007,"global_name":"pajlada","avatar_decoration_data":null,"banner_color":"#cc44ff","clan":null},"expires_at":null,"flags":2,"guild":{"id":"138009976613502976","name":"pajlada","splash":null,"banner":null,"description":null,"icon":"dcbac612ccdd3ffa2fbf89647e26f929","features":["CHANNEL_ICON_EMOJIS_GENERATED","INVITE_SPLASH","THREE_DAY_THREAD_ARCHIVE","COMMUNITY","ANIMATED_ICON","SOUNDBOARD","NEW_THREAD_PERMISSIONS","ACTIVITY_FEED_DISABLED_BY_USER","THREADS_ENABLED","NEWS"],"verification_level":1,"vanity_url_code":null,"nsfw_level":0,"nsfw":false,"premium_subscription_count":6},"guild_id":"138009976613502976","channel":{"id":"138009976613502976","type":0,"name":"general"},"approximate_member_count":1515,"approximate_presence_count":563}`)
18+
}
19+
20+
func testServer() *httptest.Server {
21+
r := chi.NewRouter()
22+
r.Get("/api/v9/invites/{invite}", func(w http.ResponseWriter, r *http.Request) {
23+
invite := chi.URLParam(r, "invite")
24+
25+
w.Header().Set("Content-Type", "application/json")
26+
27+
if response, ok := data_raw[invite]; ok {
28+
w.Write(response)
29+
} else {
30+
http.Error(w, http.StatusText(404), 404)
31+
}
32+
})
33+
return httptest.NewServer(r)
34+
}

internal/resolvers/discord/initialize.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ import (
1111
"github.com/Chatterino/api/internal/logger"
1212
"github.com/Chatterino/api/pkg/config"
1313
"github.com/Chatterino/api/pkg/resolver"
14+
"github.com/Chatterino/api/pkg/utils"
1415
)
1516

1617
const (
17-
discordInviteAPIURL = "https://discord.com/api/v9/invites/%s"
18-
1918
discordInviteTooltip = `<div style="text-align: left;">
2019
<b>{{.ServerName}}</b>
2120
<br>
@@ -48,5 +47,7 @@ func Initialize(ctx context.Context, cfg config.APIConfig, pool db.Pool, resolve
4847
return
4948
}
5049

51-
*resolvers = append(*resolvers, NewInviteResolver(ctx, cfg, pool))
50+
apiURL := utils.MustParseURL("https://discord.com/api/v9/invites/")
51+
52+
*resolvers = append(*resolvers, NewInviteResolver(ctx, cfg, pool, apiURL))
5253
}

internal/resolvers/discord/invite_loader.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"net/http"
1010
"net/url"
11+
"slices"
1112
"strconv"
1213
"strings"
1314
"time"
@@ -49,16 +50,37 @@ type DiscordInviteData struct {
4950
}
5051

5152
type InviteLoader struct {
53+
baseURL *url.URL
54+
5255
token string
5356
}
5457

58+
func NewInviteLoader(baseURL *url.URL, token string) *InviteLoader {
59+
l := &InviteLoader{
60+
baseURL: baseURL,
61+
62+
token: token,
63+
}
64+
65+
return l
66+
}
67+
68+
func (l *InviteLoader) buildURL(inviteCode string) *url.URL {
69+
relativeURL := &url.URL{
70+
Path: inviteCode,
71+
}
72+
finalURL := l.baseURL.ResolveReference(relativeURL)
73+
74+
return finalURL
75+
}
76+
5577
func (l *InviteLoader) Load(ctx context.Context, inviteCode string, r *http.Request) (*resolver.Response, time.Duration, error) {
5678
log := logger.FromContext(ctx)
5779
log.Debugw("[DiscordInvite] Get invite",
5880
"inviteCode", inviteCode,
5981
)
6082

61-
apiURL, _ := url.Parse(fmt.Sprintf(discordInviteAPIURL, inviteCode))
83+
apiURL := l.buildURL(inviteCode)
6284
apiURLVariables := url.Values{}
6385
apiURLVariables.Set("with_counts", "true")
6486
apiURL.RawQuery = apiURLVariables.Encode()
@@ -121,6 +143,9 @@ func (l *InviteLoader) Load(ctx context.Context, inviteCode string, r *http.Requ
121143
// An example of a server that has pretty much all the perks: https://discord.com/api/invites/test
122144
parsedPerks := ""
123145
accpetedPerks := []string{"PARTNERED", "PUBLIC", "ANIMATED_ICON", "BANNER", "INVITE_SPLASH", "VIP_REGIONS", "VANITY_URL", "COMMUNITY"}
146+
slices.SortStableFunc(jsonResponse.Guild.Features, func(a, b string) int {
147+
return strings.Compare(a, b)
148+
})
124149
for _, elem := range jsonResponse.Guild.Features {
125150
if utils.Contains(accpetedPerks, elem) {
126151
if parsedPerks != "" {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package discord
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
7+
"github.com/Chatterino/api/pkg/utils"
8+
qt "github.com/frankban/quicktest"
9+
)
10+
11+
func TestBuildURL(t *testing.T) {
12+
c := qt.New(t)
13+
14+
tests := []struct {
15+
label string
16+
baseURL *url.URL
17+
inviteCode string
18+
expected string
19+
}{
20+
{
21+
"Real URL 1",
22+
utils.MustParseURL("https://discord.com/api/v9/invites/"),
23+
"forsen",
24+
"https://discord.com/api/v9/invites/forsen",
25+
},
26+
{
27+
"Real URL 2",
28+
utils.MustParseURL("https://discord.com/api/v9/invites/"),
29+
"qbRE8WR",
30+
"https://discord.com/api/v9/invites/qbRE8WR",
31+
},
32+
{
33+
"Test URL 1",
34+
utils.MustParseURL("http://127.0.0.1:5934/api/v9/invites/"),
35+
"forsen",
36+
"http://127.0.0.1:5934/api/v9/invites/forsen",
37+
},
38+
{
39+
"Test URL 2",
40+
utils.MustParseURL("http://127.0.0.1:5934/api/v9/invites/"),
41+
"qbRE8WR",
42+
"http://127.0.0.1:5934/api/v9/invites/qbRE8WR",
43+
},
44+
}
45+
46+
for _, t := range tests {
47+
c.Run(t.label, func(c *qt.C) {
48+
loader := NewInviteLoader(t.baseURL, "fakecode")
49+
actual := loader.buildURL(t.inviteCode)
50+
c.Assert(actual.String(), qt.Equals, t.expected)
51+
})
52+
}
53+
}

internal/resolvers/discord/invite_resolver.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ func (r *InviteResolver) Name() string {
3636
return "discord:invite"
3737
}
3838

39-
func NewInviteResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool) *InviteResolver {
40-
inviteLoader := &InviteLoader{
41-
token: cfg.DiscordToken,
42-
}
39+
func NewInviteResolver(ctx context.Context, cfg config.APIConfig, pool db.Pool, baseURL *url.URL) *InviteResolver {
40+
inviteLoader := NewInviteLoader(baseURL, cfg.DiscordToken)
4341

4442
// We cache invites longer on purpose as the API is pretty strict with its rate limiting, and the information changes very seldomly anyway
4543
// TODO: Log 429 errors from the loader

0 commit comments

Comments
 (0)