From 30b4e1dfb2865251d97ede3c231b36be558cdf26 Mon Sep 17 00:00:00 2001 From: Minseong Choi Date: Tue, 22 Sep 2026 13:35:50 +0900 Subject: [PATCH] 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. --- internal/api/handlers_hasjoined_test.go | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/internal/api/handlers_hasjoined_test.go b/internal/api/handlers_hasjoined_test.go index 764d744..8f04db3 100644 --- a/internal/api/handlers_hasjoined_test.go +++ b/internal/api/handlers_hasjoined_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net" @@ -75,6 +76,13 @@ func fakeYgg(t *testing.T, id, name string) *httptest.Server { 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 { 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 // 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) {