test(nano): cover the bar-list error, bad identity id and ip relay

Three paths in handleHasJoined had no test that fails when they break:

- A bar-list lookup error answers 500. Logging it and carrying on would
  admit a reclaimed squatter during a database outage.
- An identity (Mojang) id that does not parse answers 204. Ignoring the
  parse error would emit the nil UUID for every such login, so they all
  share one player's data.
- The ip parameter is relayed to each source. Dropping it turns off the
  sources' check that the session is used from the player's own address.

One subtest each. Mutants that ignore the bar-list error, ignore the id
parse error, or stop appending ip each fail their subtest.
This commit is contained in:
flyemoji committed 2026-09-22 13:35:50 +09:00
1 parent 942e9a5ff8
commit 30b4e1dfb2
1 file changed
+47
+47
View File
@@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@@ -75,6 +76,13 @@ func fakeYgg(t *testing.T, id, name string) *httptest.Server {
return srv return srv
} }
// blacklistDownRepo is a store whose bar list cannot be read.
type blacklistDownRepo struct{ *fakeRepo }
func (blacklistDownRepo) IsUsernameBlacklisted(context.Context, string) (bool, error) {
return false, errors.New("bar list unreachable")
}
func getHasJoined(h http.Handler, username, serverID string) *httptest.ResponseRecorder { func getHasJoined(h http.Handler, username, serverID string) *httptest.ResponseRecorder {
return do(h, "GET", "/session/minecraft/hasJoined?username="+username+"&serverId="+serverID, "", nil) return do(h, "GET", "/session/minecraft/hasJoined?username="+username+"&serverId="+serverID, "", nil)
} }
@@ -323,6 +331,45 @@ func TestHasJoined(t *testing.T) {
} }
}) })
// With the bar list unreadable, nobody can say the player is not barred; the login must
// not go through. (Velocity reports the 500 as the auth servers being down.)
t.Run("bar list lookup error -> not admitted", func(t *testing.T) {
mojang := fakeYgg(t, notchMojangID, "Notch")
api := newTestAPI(blacklistDownRepo{newFakeRepo()}, newFakeCluster())
api.AuthSources = []AuthSource{{Tag: "mojang", URL: mojang.URL, Identity: true}}
if w := getHasJoined(api.InternalHandler(), "Notch", "abc"); w.Code == http.StatusOK {
t.Fatalf("admitted with the bar list unreadable (%q)", w.Body.String())
}
})
// Mojang is trusted for its UUIDs, which is exactly why one that does not parse must not
// be emitted as some default: every such login would share the nil UUID.
t.Run("identity source with an unparseable id -> 204", func(t *testing.T) {
mojang := fakeYgg(t, "not-a-uuid", "Notch")
api := newTestAPI(newFakeRepo(), newFakeCluster())
api.AuthSources = []AuthSource{{Tag: "mojang", URL: mojang.URL, Identity: true}}
if w := getHasJoined(api.InternalHandler(), "Notch", "abc"); w.Code != http.StatusNoContent {
t.Fatalf("code = %d, want 204 (%q)", w.Code, w.Body.String())
}
})
// The player's address is what lets a source refuse a session relayed from another IP
// (prevent-proxy-connections); it has to reach the source unchanged.
t.Run("ip is forwarded to the source", func(t *testing.T) {
got := make(chan string, 1)
src := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got <- r.URL.Query().Get("ip")
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(src.Close)
api := newTestAPI(newFakeRepo(), newFakeCluster())
api.AuthSources = []AuthSource{{Tag: "mojang", URL: src.URL, Identity: true}}
do(api.InternalHandler(), "GET", "/session/minecraft/hasJoined?username=Notch&serverId=abc&ip=203.0.113.9", "", nil)
if ip := <-got; ip != "203.0.113.9" {
t.Fatalf("source saw ip %q, want 203.0.113.9", ip)
}
})
// A GET that declares a body it never sends must still be answered and lose its // A GET that declares a body it never sends must still be answered and lose its
// connection; otherwise each such socket stays open for as long as the client likes. // connection; otherwise each such socket stays open for as long as the client likes.
t.Run("request declaring a body is refused and closed", func(t *testing.T) { t.Run("request declaring a body is refused and closed", func(t *testing.T) {