Cleanup, FallbackAPIServer tests

This commit is contained in:
Evan Goode 2026-08-09 15:56:55 -04:00
parent eb0155b702
commit eccd2e2c6f
10 changed files with 455 additions and 154 deletions

View file

@ -30,7 +30,7 @@ func TestAccount(t *testing.T) {
ts.SetupAux(auxConfig)
config := testConfig()
config.FallbackAPIServers = []FallbackAPIServerConfig{ts.ToFallbackAPIServer(ts.AuxApp, "Aux")}
config.FallbackAPIServers = []FallbackAPIServerConfig{ts.ToFallbackAPIServerAuthlibInjector(ts.AuxApp, "Aux")}
ts.Setup(config)
defer ts.Teardown()

View file

@ -10,9 +10,6 @@ import (
"testing"
)
const FALLBACK_SKIN_DOMAIN_A = "a.example.com"
const FALLBACK_SKIN_DOMAIN_B = "b.example.com"
func TestAuthlibInjector(t *testing.T) {
t.Parallel()
// authlib-injector also expects a X-Authlib-Injector-API-Location header
@ -38,9 +35,9 @@ func TestAuthlibInjector(t *testing.T) {
ts.SetupAux(auxConfig)
config := testConfig()
fallback := ts.ToFallbackAPIServer(ts.AuxApp, "Aux")
fallback.SkinDomains = []string{FALLBACK_SKIN_DOMAIN_A, FALLBACK_SKIN_DOMAIN_B}
config.FallbackAPIServers = []FallbackAPIServerConfig{fallback}
config.FallbackAPIServers = []FallbackAPIServerConfig{
ts.ToFallbackAPIServerAuthlibInjector(ts.AuxApp, "Aux"),
}
ts.Setup(config)
defer ts.Teardown()
@ -71,7 +68,7 @@ func (ts *TestSuite) testAuthlibInjectorRootFallback(t *testing.T) {
var response authlibInjectorResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, []string{ts.App.Config.Domain, FALLBACK_SKIN_DOMAIN_A, FALLBACK_SKIN_DOMAIN_B}, response.SkinDomains)
assert.ElementsMatch(t, []string{ts.App.Config.Domain, ts.AuxApp.Config.Domain}, response.SkinDomains)
assert.True(t, response.Meta.FeatureNonEmailLogin)
}

View file

@ -1164,8 +1164,8 @@ func NewFallbackAPIServer(config *FallbackAPIServerConfig) (FallbackAPIServer, e
legacy := config.URLs.MustArg3()
sessionGetProfileByIDURL = legacy.SessionURL + "/session/minecraft/profile"
sessionGetProfileByIDURL = legacy.SessionURL + "/session/minecraft/hasJoined"
profilesGetManyByNameURL = legacy.AccountURL + "/api/profiles/minecraft"
sessionVerifyURL = legacy.SessionURL + "/session/minecraft/hasJoined"
profilesGetManyByNameURL = legacy.AccountURL + "/profiles/minecraft"
publicKeysURL := legacy.ServicesURL + "/publickeys"
var err error

View file

@ -786,7 +786,7 @@ func CleanConfig(rawConfig *RawConfig) (Config, []Deprecation, error) {
if rawFallbackAPIServer.AuthlibInjectorURL != nil {
if urls.IsPresent() {
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: can't supply both legacy URLs and AuthlibInjectorURL")
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: can't supply both legacy URLs and AuthlibInjectorURL", nickname)
}
authlibInjectorURL := orElse(rawFallbackAPIServer.AuthlibInjectorURL, defaultFallbackAPIServerAuthlibInjector().AuthlibInjectorURL)
@ -806,11 +806,11 @@ func CleanConfig(rawConfig *RawConfig) (Config, []Deprecation, error) {
if rawFallbackAPIServer.DiscoveryMinecraftClientURL != nil {
if u, ok := urls.Get(); ok {
if u.IsArg3() {
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: can't supply both legacy URLs and DiscoveryMinecraftClientURL")
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: can't supply both legacy URLs and DiscoveryMinecraftClientURL", nickname)
} else if u.IsArg2() {
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: can't supply both AuthlibInjectorURL and DiscoveryMinecraftClientURL")
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: can't supply both AuthlibInjectorURL and DiscoveryMinecraftClientURL", nickname)
} else {
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: unexpected error")
return Config{}, nil, fmt.Errorf("FallbackAPIServer %s: unexpected error", nickname)
}
}

View file

@ -103,8 +103,7 @@ func TestConfig(t *testing.T) {
config, deprecations, err = CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.Empty(t, deprecations)
assert.Equal(t, "https://xn--mxafwwl.example.com", config.FallbackAPIServers[0].SessionURL)
assert.Equal(t, "https://drasl.example.com", config.FallbackAPIServers[0].AccountURL)
assert.True(t, config.FallbackAPIServers[0].URLs.IsArg3())
assert.Equal(t, "https://drasl.example.com/editskin", config.FallbackAPIServers[0].SetSkinURL)
assert.True(t, config.RegistrationUsernamePassword.CreateNewPlayer.Allow)
assert.Equal(t, 1, len(config.RegistrationUsernamePassword.ImportExistingPlayer))
@ -123,62 +122,6 @@ func TestConfig(t *testing.T) {
}
assertUnclean(t, rawConfig)
rawConfig = configTestRawConfig(sd)
testFallbackAPIServer := rawFallbackAPIServerConfig{
Nickname: Ptr("Nickname"),
SessionURL: Ptr("https://δρασλ.example.com/"),
AccountURL: Ptr("https://δρασλ.example.com/"),
ServicesURL: Ptr("https://δρασλ.example.com/"),
SkinDomains: Ptr([]string{"δρασλ.example.com"}),
}
fb := testFallbackAPIServer
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
config, deprecations, err = CleanConfig(&rawConfig)
assert.Empty(t, deprecations)
assert.Nil(t, err)
assert.Equal(t, 1, len(config.FallbackAPIServers))
assert.Equal(t, *fb.Nickname, config.FallbackAPIServers[0].Nickname)
assert.Equal(t, "https://xn--mxafwwl.example.com", config.FallbackAPIServers[0].SessionURL)
assert.Equal(t, "https://xn--mxafwwl.example.com", config.FallbackAPIServers[0].AccountURL)
assert.Equal(t, "https://xn--mxafwwl.example.com", config.FallbackAPIServers[0].ServicesURL)
assert.Equal(t, []string{"xn--mxafwwl.example.com"}, config.FallbackAPIServers[0].SkinDomains)
fb = testFallbackAPIServer
fb.Nickname = Ptr("")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
fb = testFallbackAPIServer
fb.SessionURL = Ptr("")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
fb = testFallbackAPIServer
fb.SessionURL = Ptr(":invalid URL")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
fb = testFallbackAPIServer
fb.AccountURL = Ptr("")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
fb = testFallbackAPIServer
fb.AccountURL = Ptr(":invalid URL")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
fb = testFallbackAPIServer
fb.ServicesURL = Ptr("")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
fb = testFallbackAPIServer
fb.ServicesURL = Ptr(":invalid URL")
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{fb}
assertUnclean(t, rawConfig)
// New [[ImportExistingPlayer]] array-of-tables form: no deprecation.
rawConfig = configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
@ -289,10 +232,10 @@ func TestConfigDeprecations(t *testing.T) {
sd := Unwrap(os.MkdirTemp("", "tmp"))
defer os.RemoveAll(sd)
// ForwardSkins (global, deprecated in 4.0.0)
// ForwardSkins (deprecated in 4.0.0)
// Global ForwardSkins = false with a FallbackAPIServer that does not set
// per-server ForwardSkins: the global value migrates to the server.
// The global ForwardSkins is still respected if a FallbackAPIServer does
// not set FallbackAPIServer.ForwardSkins
{
rawConfig := configTestRawConfig(sd)
rawConfig.ForwardSkins = Ptr(false)
@ -305,8 +248,7 @@ func TestConfigDeprecations(t *testing.T) {
assert.False(t, config.FallbackAPIServers[0].ForwardSkins)
}
// Global ForwardSkins = false but per-server ForwardSkins = true: per-server
// wins (the deprecated global is only a fallback when per-server is unset).
// FallbackAPIServer.ForwardSkins wins over ForwardSkins
{
rawConfig := configTestRawConfig(sd)
rawConfig.ForwardSkins = Ptr(false)
@ -319,22 +261,10 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, config.FallbackAPIServers[0].ForwardSkins)
}
// No global ForwardSkins: no deprecation, per-server defaults to true.
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("Mojang"), SessionURL: Ptr("https://sessionserver.mojang.com"), AccountURL: Ptr("https://api.mojang.com"), ServicesURL: Ptr("https://api.minecraftservices.com")},
}
config, deprecations, err := CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.False(t, hasDeprecation(deprecations, "ForwardSkins"))
assert.True(t, config.FallbackAPIServers[0].ForwardSkins)
}
// AllowAddingDeletingPlayers (deprecated in 4.0.0)
// AllowAddingDeletingPlayers = false clears CreateNewPlayer.Allow and
// ImportExistingPlayer entries.
// AllowAddingDeletingPlayers = false overrides CreateNewPlayer.Allow to
// false and clears ImportExistingPlayer entries.
{
rawConfig := configTestRawConfig(sd)
rawConfig.AllowAddingDeletingPlayers = Ptr(false)
@ -405,9 +335,9 @@ func TestConfigDeprecations(t *testing.T) {
// RegistrationExistingPlayer (deprecated in 4.0.0)
// RegistrationExistingPlayer with Allow=true migrates to
// RegistrationExistingPlayer with Allow = true migrates to
// RegistrationUsernamePassword.ImportExistingPlayer only when the legacy
// [ImportExistingPlayer] form (with Allow=true) is also present, so a
// [ImportExistingPlayer] form (with Allow = true) is also present, so a
// synthesized FallbackAPIServer exists to reference.
{
rawConfig := configTestRawConfig(sd)
@ -440,8 +370,8 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, ep.RequireInvite)
}
// RegistrationExistingPlayer with Allow=false produces no ImportExistingPlayer
// entry (but deprecation is emitted).
// RegistrationExistingPlayer with Allow = false produces no
// ImportExistingPlayer entry (but deprecation is emitted).
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationExistingPlayer = &rawRegistrationExistingPlayerConfig{
@ -481,8 +411,8 @@ func TestConfigDeprecations(t *testing.T) {
// RegistrationOIDC.RequireInvite (deprecated in 4.0.0)
// OIDC RequireInvite alone (no global RegistrationNewPlayer, no explicit
// CreateNewPlayer) migrates to CreateNewPlayer.RequireInvite via OR.
// RegistrationOIDC.RequireInvite migrates to
// RegistrationOIDC.CreateNewPlayer.RequireInvite
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationOIDC = []rawRegistrationOIDCConfig{
@ -495,8 +425,9 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, config.RegistrationOIDC[0].CreateNewPlayer.RequireInvite)
}
// OR semantics for CreateNewPlayer: global RequireInvite=false + per-OIDC
// RequireInvite=true => result true.
// Invite required for OIDC registration with new player when
// RegistrationNewPlayer.RequireInvite = false and
// RegistrationOIDC.RequireInvite = true
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationNewPlayer = &rawRegistrationNewPlayerConfig{
@ -512,8 +443,9 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, config.RegistrationOIDC[0].CreateNewPlayer.RequireInvite)
}
// OR semantics for CreateNewPlayer: global RequireInvite=true + per-OIDC
// RequireInvite=false => result true.
// Invite required for OIDC registration with new player when
// RegistrationNewPlayer.RequireInvite = true and
// RegistrationOIDC.RequireInvite = false
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationNewPlayer = &rawRegistrationNewPlayerConfig{
@ -529,8 +461,8 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, config.RegistrationOIDC[0].CreateNewPlayer.RequireInvite)
}
// Explicit OIDC CreateNewPlayer.RequireInvite takes precedence; the deprecated
// per-OIDC RequireInvite is ignored for CreateNewPlayer (but still warned).
// RegistrationOIDC.CreateNewPlayer.RequireInvite wins over
// RegistrationOIDC.RequireInvite
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationOIDC = []rawRegistrationOIDCConfig{
@ -547,9 +479,9 @@ func TestConfigDeprecations(t *testing.T) {
assert.False(t, config.RegistrationOIDC[0].CreateNewPlayer.RequireInvite)
}
// OR semantics for ImportExistingPlayer: per-OIDC RequireInvite migrates into
// the synthesized ImportExistingPlayer entry, OR'd with the global
// RegistrationExistingPlayer.RequireInvite.
// Invite required for synthesized RegistrationOIDC.ImportExistingPlayer
// when RegistrationOIDC.RequireInvite = true and
// RegistrationExistingPlayer.RequireInvite = false
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationExistingPlayer = &rawRegistrationExistingPlayerConfig{
@ -578,8 +510,9 @@ func TestConfigDeprecations(t *testing.T) {
assert.Equal(t, "Mojang", oidc.ImportExistingPlayer[0].FallbackAPIServerNickname)
}
// Both global RegistrationExistingPlayer.RequireInvite=true and per-OIDC
// RequireInvite=false => OR yields true.
// Invite required for synthesized RegistrationOIDC.ImportExistingPlayer
// when RegistrationOIDC.RequireInvite = false and
// RegistrationExistingPlayer.RequireInvite = true
{
rawConfig := configTestRawConfig(sd)
rawConfig.RegistrationExistingPlayer = &rawRegistrationExistingPlayerConfig{
@ -608,8 +541,8 @@ func TestConfigDeprecations(t *testing.T) {
// Legacy [ImportExistingPlayer] single-table form (deprecated in 4.0.0)
// Legacy form with Allow=true and no matching [[FallbackAPIServers]] entry:
// synthesize one, emit a deprecation.
// Legacy form with Allow = true and no matching [[FallbackAPIServers]]
// entry: synthesize one, emit a deprecation.
{
rawConfig := configTestRawConfig(sd)
rawConfig.ImportExistingPlayer = rawImportExistingPlayer{
@ -632,16 +565,17 @@ func TestConfigDeprecations(t *testing.T) {
assert.Len(t, deprecations, 1)
assert.Equal(t, 1, len(config.FallbackAPIServers))
assert.Equal(t, "Mojang", config.FallbackAPIServers[0].Nickname)
assert.Equal(t, "https://sessionserver.mojang.com", config.FallbackAPIServers[0].SessionURL)
assert.Equal(t, "https://api.mojang.com", config.FallbackAPIServers[0].AccountURL)
legacy2 := config.FallbackAPIServers[0].URLs.MustArg3()
assert.Equal(t, "https://sessionserver.mojang.com", legacy2.SessionURL)
assert.Equal(t, "https://api.mojang.com", legacy2.AccountURL)
assert.Equal(t, "https://www.minecraft.net/msaprofile/mygames/editskin", config.FallbackAPIServers[0].SetSkinURL)
assert.Equal(t, 1, len(config.ImportExistingPlayer))
assert.Equal(t, "Mojang", config.ImportExistingPlayer[0].FallbackAPIServerNickname)
assert.True(t, config.ImportExistingPlayer[0].RequireSkinVerification)
}
// Legacy form with Allow=true and a matching [[FallbackAPIServers]] entry:
// do not duplicate the entry; reference the existing one.
// Legacy form with Allow = true and a matching [[FallbackAPIServers]]
// entry: do not duplicate the entry; reference the existing one.
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
@ -665,11 +599,13 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, hasDeprecation(deprecations, "ImportExistingPlayer"))
assert.Len(t, deprecations, 1)
assert.Equal(t, 1, len(config.FallbackAPIServers))
assert.Equal(t, "https://api.minecraftservices.com", config.FallbackAPIServers[0].ServicesURL)
legacy3 := config.FallbackAPIServers[0].URLs.MustArg3()
assert.Equal(t, "https://api.minecraftservices.com", legacy3.ServicesURL)
assert.Equal(t, 1, len(config.ImportExistingPlayer))
}
// Legacy form with Allow=false: no entries, no synthesized FallbackAPIServer.
// Legacy form with Allow = false: no entries, no synthesized
// FallbackAPIServer.
{
rawConfig := configTestRawConfig(sd)
rawConfig.ImportExistingPlayer = rawImportExistingPlayer{
@ -683,7 +619,7 @@ func TestConfigDeprecations(t *testing.T) {
assert.Equal(t, 0, len(config.ImportExistingPlayer))
}
// Legacy form with Allow=true but empty Nickname should fail.
// Legacy form with Allow = true but empty Nickname should fail.
{
rawConfig := configTestRawConfig(sd)
rawConfig.ImportExistingPlayer = rawImportExistingPlayer{
@ -748,3 +684,177 @@ func TestConfigDeprecations(t *testing.T) {
assert.True(t, config.RegistrationOIDC[0].ImportExistingPlayer[0].RequireInvite)
}
}
func TestConfigFallbackAPIServerURLs(t *testing.T) {
t.Parallel()
sd := Unwrap(os.MkdirTemp("", "tmp"))
defer os.RemoveAll(sd)
// AuthlibInjectorURL form
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("ALI"), AuthlibInjectorURL: Ptr("https://littleskin.cn/api/yggdrasil/")},
}
config, deprecations, err := CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.Empty(t, deprecations)
assert.True(t, config.FallbackAPIServers[0].URLs.IsArg2())
assert.Equal(t, "https://littleskin.cn/api/yggdrasil", config.FallbackAPIServers[0].URLs.MustArg2().AuthlibInjectorURL)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("ALI"), AuthlibInjectorURL: Ptr("https://δρασλ.example.com/")},
}
config, _, err := CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.Equal(t, "https://xn--mxafwwl.example.com", config.FallbackAPIServers[0].URLs.MustArg2().AuthlibInjectorURL)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("ALI"), AuthlibInjectorURL: Ptr(":invalid")},
}
assertUnclean(t, rawConfig)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("ALI"), AuthlibInjectorURL: Ptr("")},
}
assertUnclean(t, rawConfig)
}
// DiscoveryMinecraftClientURL form
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("Discovery"), DiscoveryMinecraftClientURL: Ptr("https://discovery.minecraftservices.com/minecraft/client/")},
}
config, deprecations, err := CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.Empty(t, deprecations)
assert.True(t, config.FallbackAPIServers[0].URLs.IsArg1())
assert.Equal(t, "https://discovery.minecraftservices.com/minecraft/client", config.FallbackAPIServers[0].URLs.MustArg1().DiscoveryMinecraftClientURL)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("Discovery"), DiscoveryMinecraftClientURL: Ptr("https://δρασλ.example.com/minecraft/client")},
}
config, _, err := CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.Equal(t, "https://xn--mxafwwl.example.com/minecraft/client", config.FallbackAPIServers[0].URLs.MustArg1().DiscoveryMinecraftClientURL)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("Discovery"), DiscoveryMinecraftClientURL: Ptr(":invalid")},
}
assertUnclean(t, rawConfig)
}
// Legacy form
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{
Nickname: Ptr("Legacy"),
SessionURL: Ptr("https://δρασλ.example.com/"),
AccountURL: Ptr("https://δρασλ.example.com/"),
ServicesURL: Ptr("https://δρασλ.example.com/"),
SkinDomains: Ptr([]string{"δρασλ.example.com"}),
},
}
config, deprecations, err := CleanConfig(&rawConfig)
assert.Nil(t, err)
assert.Empty(t, deprecations)
assert.True(t, config.FallbackAPIServers[0].URLs.IsArg3())
legacy := config.FallbackAPIServers[0].URLs.MustArg3()
assert.Equal(t, "https://xn--mxafwwl.example.com", legacy.SessionURL)
assert.Equal(t, "https://xn--mxafwwl.example.com", legacy.AccountURL)
assert.Equal(t, "https://xn--mxafwwl.example.com", legacy.ServicesURL)
assert.Equal(t, []string{"xn--mxafwwl.example.com"}, legacy.SkinDomains)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{
Nickname: Ptr("Legacy"),
SessionURL: Ptr(":invalid"),
AccountURL: Ptr("https://api.mojang.com"),
ServicesURL: Ptr("https://api.minecraftservices.com"),
},
}
assertUnclean(t, rawConfig)
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{
Nickname: Ptr(""),
SessionURL: Ptr("https://sessionserver.mojang.com"),
AccountURL: Ptr("https://api.mojang.com"),
ServicesURL: Ptr("https://api.minecraftservices.com"),
},
}
assertUnclean(t, rawConfig)
}
// The three forms are mutually exclusive.
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{
Nickname: Ptr("Both"),
SessionURL: Ptr("https://sessionserver.mojang.com"),
AccountURL: Ptr("https://api.mojang.com"),
ServicesURL: Ptr("https://api.minecraftservices.com"),
AuthlibInjectorURL: Ptr("https://littleskin.cn/api/yggdrasil"),
},
}
_, _, err := CleanConfig(&rawConfig)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "legacy URLs and AuthlibInjectorURL")
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{
Nickname: Ptr("Both"),
SessionURL: Ptr("https://sessionserver.mojang.com"),
AccountURL: Ptr("https://api.mojang.com"),
ServicesURL: Ptr("https://api.minecraftservices.com"),
DiscoveryMinecraftClientURL: Ptr("https://discovery.minecraftservices.com/minecraft/client"),
},
}
_, _, err := CleanConfig(&rawConfig)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "legacy URLs and DiscoveryMinecraftClientURL")
}
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{
Nickname: Ptr("Both"),
AuthlibInjectorURL: Ptr("https://littleskin.cn/api/yggdrasil"),
DiscoveryMinecraftClientURL: Ptr("https://discovery.minecraftservices.com/minecraft/client"),
},
}
_, _, err := CleanConfig(&rawConfig)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "AuthlibInjectorURL and DiscoveryMinecraftClientURL")
}
// None supplied
{
rawConfig := configTestRawConfig(sd)
rawConfig.FallbackAPIServers = []rawFallbackAPIServerConfig{
{Nickname: Ptr("Lonely")},
}
_, _, err := CleanConfig(&rawConfig)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "must supply either an AuthlibInjectorURL, a DiscoveryMinecraftClientURL, or legacy URLs")
}
}

View file

@ -24,7 +24,7 @@ func TestDiscoveryMinecraftClient(t *testing.T) {
rec := ts.Get(t, ts.Server, path, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
var response discoveryResponse
var response DiscoveryResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, "prod", response.Environment)
assert.Equal(t, "minecraft", response.Product)

172
fallback_api_server_test.go Normal file
View file

@ -0,0 +1,172 @@
package main
import (
"crypto/rsa"
"net/http"
"testing"
"github.com/samber/mo"
"github.com/stretchr/testify/assert"
)
// fallbackKeyContains checks whether a set of rsa.PublicKey contains a key
// equal to target, using rsa.PublicKey.Equal for value comparison (since
// mapset uses == which compares *big.Int pointers, not values).
func fallbackKeyContains(set interface{ ToSlice() []rsa.PublicKey }, target rsa.PublicKey) bool {
for _, k := range set.ToSlice() {
if k.Equal(&target) {
return true
}
}
return false
}
// TestFallbackAPIServerLegacy verifies that a FallbackAPIServer configured
// with legacy SessionURL/AccountURL/ServicesURL/SkinDomains correctly derives
// its endpoint URLs, public keys, skin domains, and texture valid URIs.
func TestFallbackAPIServerLegacy(t *testing.T) {
t.Parallel()
ts := &TestSuite{}
auxConfig := testConfig()
ts.SetupAux(auxConfig)
const legacySkinDomain = "legacy.example.com"
config := testConfig()
fallback := ts.ToFallbackAPIServerLegacy(ts.AuxApp, "Aux")
legacy := fallback.URLs.MustArg3()
legacy.SkinDomains = []string{legacySkinDomain}
fallback.URLs = mo.NewEither3Arg3[
fallbackAPIServerDiscoveryConfig,
fallbackAPIServerAuthlibInjectorConfig,
fallbackAPIServerLegacyConfig,
](legacy)
config.FallbackAPIServers = []FallbackAPIServerConfig{fallback}
ts.Setup(config)
defer ts.Teardown()
ts.CreateTestUser(t, ts.AuxApp, ts.AuxServer, TEST_USERNAME)
fb, ok := ts.App.FallbackAPIServers["Aux"]
assert.True(t, ok, "FallbackAPIServer 'Aux' should be registered")
assert.Equal(t, ts.AuxApp.SessionURL+"/session/minecraft/profile", fb.SessionGetProfileByIDURL)
assert.Equal(t, ts.AuxApp.SessionURL+"/session/minecraft/hasJoined", fb.SessionVerifyURL)
assert.Equal(t, ts.AuxApp.AccountURL+"/profiles/minecraft", fb.ProfilesGetManyByNameURL)
// The aux's public key should be present in both key sets, fetched from
// the aux's /publickeys endpoint.
assert.Equal(t, 1, fb.ProfilePropertyKeys.Cardinality())
assert.Equal(t, 1, fb.PlayerCertificateKeys.Cardinality())
assert.True(t, fallbackKeyContains(fb.ProfilePropertyKeys, ts.AuxApp.PrivateKey.PublicKey))
assert.True(t, fallbackKeyContains(fb.PlayerCertificateKeys, ts.AuxApp.PrivateKey.PublicKey))
// Skin domains and texture valid URIs derived from the configured
// SkinDomains list.
assert.True(t, fb.SkinDomains.Contains(legacySkinDomain))
assert.True(t, fb.GetTextureValidURIs.Contains("https://"+legacySkinDomain+"/"))
assert.True(t, fb.GetTextureValidURIs.Contains("http://"+legacySkinDomain+"/"))
// Look up an aux player by UUID through the main server.
{
var player Player
assert.Nil(t, ts.AuxApp.DB.First(&player, "name = ?", TEST_USERNAME).Error)
rec := ts.Get(t, ts.Server, "/minecraft/profile/lookup/"+player.UUID, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
}
}
// TestFallbackAPIServerDiscovery verifies that a FallbackAPIServer configured
// with a DiscoveryMinecraftClientURL correctly derives its endpoint URLs and
// public keys from the aux server's discovery document.
func TestFallbackAPIServerDiscovery(t *testing.T) {
t.Parallel()
ts := &TestSuite{}
auxConfig := testConfig()
ts.SetupAux(auxConfig)
config := testConfig()
config.FallbackAPIServers = []FallbackAPIServerConfig{
ts.ToFallbackAPIServerDiscovery(ts.AuxApp, "Aux"),
}
ts.Setup(config)
defer ts.Teardown()
ts.CreateTestUser(t, ts.AuxApp, ts.AuxServer, TEST_USERNAME)
fb, ok := ts.App.FallbackAPIServers["Aux"]
assert.True(t, ok, "FallbackAPIServer 'Aux' should be registered")
// URLs derived from the aux's discovery document.
assert.Equal(t, ts.AuxApp.SessionURL+"/session/minecraft/profile", fb.SessionGetProfileByIDURL)
assert.Equal(t, ts.AuxApp.SessionURL+"/session/minecraft/hasJoined", fb.SessionVerifyURL)
assert.Equal(t, ts.AuxApp.AccountURL+"/profiles/minecraft", fb.ProfilesGetManyByNameURL)
// The aux's public key should be present in both key sets.
assert.Equal(t, 1, fb.ProfilePropertyKeys.Cardinality())
assert.Equal(t, 1, fb.PlayerCertificateKeys.Cardinality())
assert.True(t, fallbackKeyContains(fb.ProfilePropertyKeys, ts.AuxApp.PrivateKey.PublicKey))
assert.True(t, fallbackKeyContains(fb.PlayerCertificateKeys, ts.AuxApp.PrivateKey.PublicKey))
// Look up an aux player by UUID through the main server.
{
var player Player
assert.Nil(t, ts.AuxApp.DB.First(&player, "name = ?", TEST_USERNAME).Error)
rec := ts.Get(t, ts.Server, "/minecraft/profile/lookup/"+player.UUID, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
}
}
// TestFallbackAPIServerAuthlibInjector verifies that a FallbackAPIServer
// configured with an AuthlibInjectorURL correctly derives its endpoint URLs,
// public keys, skin domains, and texture valid URIs from the aux server's
// authlib-injector root endpoint.
func TestFallbackAPIServerAuthlibInjector(t *testing.T) {
t.Parallel()
ts := &TestSuite{}
auxConfig := testConfig()
ts.SetupAux(auxConfig)
config := testConfig()
config.FallbackAPIServers = []FallbackAPIServerConfig{
ts.ToFallbackAPIServerAuthlibInjector(ts.AuxApp, "Aux"),
}
ts.Setup(config)
defer ts.Teardown()
ts.CreateTestUser(t, ts.AuxApp, ts.AuxServer, TEST_USERNAME)
fb, ok := ts.App.FallbackAPIServers["Aux"]
assert.True(t, ok, "FallbackAPIServer 'Aux' should be registered")
// URLs derived from the aux's authlib-injector location.
assert.Equal(t, ts.AuxApp.AuthlibInjectorURL+"/sessionserver/session/minecraft/profile", fb.SessionGetProfileByIDURL)
assert.Equal(t, ts.AuxApp.AuthlibInjectorURL+"/sessionserver/session/minecraft/hasJoined", fb.SessionVerifyURL)
assert.Equal(t, ts.AuxApp.AuthlibInjectorURL+"/api/profiles/minecraft", fb.ProfilesGetManyByNameURL)
// The aux's public key should be present in both key sets (ALI form
// populates both from the single SignaturePublickey).
assert.Equal(t, 1, fb.ProfilePropertyKeys.Cardinality())
assert.Equal(t, 1, fb.PlayerCertificateKeys.Cardinality())
assert.True(t, fallbackKeyContains(fb.ProfilePropertyKeys, ts.AuxApp.PrivateKey.PublicKey))
assert.True(t, fallbackKeyContains(fb.PlayerCertificateKeys, ts.AuxApp.PrivateKey.PublicKey))
// Skin domains and texture valid URIs derived from the aux's
// authlib-injector response.
assert.True(t, fb.SkinDomains.Contains(ts.AuxApp.Config.Domain))
assert.True(t, fb.GetTextureValidURIs.Contains("https://"+ts.AuxApp.Config.Domain+"/"))
assert.True(t, fb.GetTextureValidURIs.Contains("http://"+ts.AuxApp.Config.Domain+"/"))
// Functional: look up an aux player by UUID through the main server.
{
var player Player
assert.Nil(t, ts.AuxApp.DB.First(&player, "name = ?", TEST_USERNAME).Error)
rec := ts.Get(t, ts.Server, "/minecraft/profile/lookup/"+player.UUID, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
}
}

View file

@ -35,12 +35,7 @@ func setupExistingPlayerTS(t *testing.T, requireSkinVerification bool, requireIn
config.CreateNewPlayer.Allow = false
config.RegistrationUsernamePassword.CreateNewPlayer.Allow = false
config.FallbackAPIServers = []FallbackAPIServerConfig{
{
Nickname: "Aux",
SessionURL: ts.AuxApp.SessionURL,
AccountURL: ts.AuxApp.AccountURL,
ServicesURL: ts.AuxApp.ServicesURL,
},
ts.ToFallbackAPIServerAuthlibInjector(ts.AuxApp, "Aux"),
}
config.RegistrationUsernamePassword.ImportExistingPlayer = []regImportExistingPlayerConfig{
{

View file

@ -26,15 +26,9 @@ func TestServices(t *testing.T) {
ts.SetupAux(auxConfig)
config := testConfig()
config.FallbackAPIServers = []FallbackAPIServerConfig{
{
Nickname: "Aux",
SessionURL: ts.AuxApp.SessionURL,
AccountURL: ts.AuxApp.AccountURL,
ServicesURL: ts.AuxApp.ServicesURL,
ForwardSkins: false,
},
}
fallback := ts.ToFallbackAPIServerAuthlibInjector(ts.AuxApp, "Aux")
fallback.ForwardSkins = false
config.FallbackAPIServers = []FallbackAPIServerConfig{fallback}
ts.Setup(config)
defer ts.Teardown()

View file

@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"github.com/labstack/echo/v5"
"github.com/samber/mo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"io"
@ -21,7 +22,6 @@ import (
"os"
"strings"
"testing"
"time"
)
var _ = os.Setenv("DRASL_TEST", "1")
@ -114,6 +114,14 @@ func (ts *TestSuite) SetupAux(config *Config) {
config.StateDirectory = tempStateDirectory
config.DataDirectory = "."
// Bind the listener first so we know the port before setup, allowing
// the aux server's discovery handler (which precomputes its response at
// MakeServer time) to bake in reachable localhost URLs.
listener := Unwrap(net.Listen("tcp", ":0"))
auxServerAddr := listener.Addr().(*net.TCPAddr)
baseURL := fmt.Sprintf("http://localhost:%d", auxServerAddr.Port)
config.BaseURL = baseURL
auxConfig := *config
ts.AuxConfig = &auxConfig
ts.AuxApp = setup(config)
@ -122,43 +130,68 @@ func (ts *TestSuite) SetupAux(config *Config) {
ts.AuxServer = ts.AuxApp.MakeServer()
// Get the aux server's address
addrChan := make(chan net.Addr, 1)
ctx, cancel := context.WithCancel(context.Background())
ts.AuxServerCancel = cancel
go func() {
Ignore(echo.StartConfig{
Address: ":0",
HideBanner: true,
HidePort: true,
ListenerAddrFunc: func(addr net.Addr) { addrChan <- addr },
Listener: listener,
HideBanner: true,
HidePort: true,
}.Start(ctx, ts.AuxServer))
}()
select {
case addr := <-addrChan:
ts.AuxServerAddr = addr.(*net.TCPAddr)
case <-time.After(1 * time.Second):
panic("timeout waiting for aux server to start")
}
baseURL := fmt.Sprintf("http://localhost:%d", ts.AuxServerAddr.Port)
ts.AuxApp.Config.BaseURL = baseURL
ts.AuxServerAddr = auxServerAddr
ts.AuxApp.FrontEndURL = baseURL
ts.AuxApp.AccountURL = Unwrap(url.JoinPath(baseURL, "account"))
ts.AuxApp.AuthURL = Unwrap(url.JoinPath(baseURL, "auth"))
ts.AuxApp.ServicesURL = Unwrap(url.JoinPath(baseURL, "services"))
ts.AuxApp.SessionURL = Unwrap(url.JoinPath(baseURL, "session"))
ts.AuxApp.TexturesURL = Unwrap(url.JoinPath(baseURL, "textures"))
ts.AuxApp.AuthlibInjectorURL = Unwrap(url.JoinPath(baseURL, "authlib-injector"))
ts.AuxApp.DiscoveryURL = Unwrap(url.JoinPath(baseURL, "discovery"))
}
func (ts *TestSuite) ToFallbackAPIServer(app *App, nickname string) FallbackAPIServerConfig {
func (ts *TestSuite) ToFallbackAPIServerLegacy(app *App, nickname string) FallbackAPIServerConfig {
return FallbackAPIServerConfig{
Nickname: nickname,
SessionURL: app.SessionURL,
AccountURL: app.AccountURL,
ServicesURL: app.ServicesURL,
Nickname: nickname,
URLs: mo.NewEither3Arg3[
fallbackAPIServerDiscoveryConfig,
fallbackAPIServerAuthlibInjectorConfig,
fallbackAPIServerLegacyConfig,
](fallbackAPIServerLegacyConfig{
SessionURL: app.SessionURL,
AccountURL: app.AccountURL,
ServicesURL: app.ServicesURL,
SkinDomains: []string{},
}),
CacheTTLSeconds: 0,
}
}
func (ts *TestSuite) ToFallbackAPIServerAuthlibInjector(app *App, nickname string) FallbackAPIServerConfig {
return FallbackAPIServerConfig{
Nickname: nickname,
URLs: mo.NewEither3Arg2[
fallbackAPIServerDiscoveryConfig,
fallbackAPIServerAuthlibInjectorConfig,
fallbackAPIServerLegacyConfig,
](fallbackAPIServerAuthlibInjectorConfig{
AuthlibInjectorURL: app.AuthlibInjectorURL,
}),
CacheTTLSeconds: 0,
}
}
func (ts *TestSuite) ToFallbackAPIServerDiscovery(app *App, nickname string) FallbackAPIServerConfig {
return FallbackAPIServerConfig{
Nickname: nickname,
URLs: mo.NewEither3Arg1[
fallbackAPIServerDiscoveryConfig,
fallbackAPIServerAuthlibInjectorConfig,
fallbackAPIServerLegacyConfig,
](fallbackAPIServerDiscoveryConfig{
DiscoveryMinecraftClientURL: app.DiscoveryURL + "/minecraft/client",
}),
CacheTTLSeconds: 0,
}
}