package tw_test import ( "context" "encoding/csv" "errors" "fmt" "net/http" "net/http/httptest" "net/url" "os" "slices" "strconv" "testing" "time" "github.com/stretchr/testify/require" "gitea.dwysokinski.me/twhelp/corev3/internal/tw" "github.com/stretchr/testify/assert" ) const testUserAgent = "tribalwarshelp.com/test" func TestClient_GetOpenServers(t *testing.T) { t.Parallel() resp, err := os.ReadFile("./testdata/get_servers_resp.txt") require.NoError(t, err) mux := http.NewServeMux() mux.HandleFunc("/backend/get_servers.php", newPlainTextHandler(resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) expectedServerKeys := []string{ "plp8", "pl170", "pl171", "pl172", "pl173", "plc1", "plp7", "pl168", "pl174", "pl159", "pl167", "pl165", "pl169", "pls1", "pl161", "pl164", } servers, err := newClient(srv.Client()).GetOpenServers(context.Background(), parseURL(t, srv.URL)) require.NoError(t, err) assert.Len(t, servers, len(expectedServerKeys)) for _, key := range expectedServerKeys { assert.True(t, slices.ContainsFunc(servers, func(server tw.Server) bool { return server.Key == key && *server.URL == url.URL{ Scheme: "https", Host: key + ".plemiona.pl", } }), "key '%s' not found", key) } } func TestClient_GetServerConfig(t *testing.T) { t.Parallel() resp, err := os.ReadFile("./testdata/server_config.xml") require.NoError(t, err) mux := http.NewServeMux() mux.HandleFunc("/interface.php", newInterfaceXMLHandler("get_config", resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) cfg, err := newClient(srv.Client()).GetServerConfig(context.Background(), parseURL(t, srv.URL)) require.NoError(t, err) assert.InDelta(t, 4.5, cfg.Speed, 0.01) assert.InDelta(t, 0.5, cfg.UnitSpeed, 0.01) assert.Equal(t, 1, cfg.Moral) assert.Equal(t, 1, cfg.Build.Destroy) assert.Equal(t, 2, cfg.Misc.KillRanking) assert.Equal(t, 0, cfg.Misc.Tutorial) assert.Equal(t, 300, cfg.Misc.TradeCancelTime) assert.Equal(t, 0, cfg.Commands.MillisArrival) assert.Equal(t, 600, cfg.Commands.CommandCancelTime) assert.Equal(t, 3, cfg.Newbie.Days) assert.Equal(t, 15, cfg.Newbie.RatioDays) assert.Equal(t, 10, cfg.Newbie.Ratio) assert.Equal(t, 1, cfg.Newbie.RemoveNewbieVillages) assert.Equal(t, 2, cfg.Game.BuildtimeFormula) assert.Equal(t, 0, cfg.Game.Knight) assert.Equal(t, 1, cfg.Game.KnightNewItems.Int()) assert.Equal(t, 1, cfg.Game.Archer) assert.Equal(t, 2, cfg.Game.Tech) assert.Equal(t, 0, cfg.Game.FarmLimit) assert.Equal(t, 0, cfg.Game.Church) assert.Equal(t, 0, cfg.Game.Watchtower) assert.Equal(t, 0, cfg.Game.Stronghold) assert.InDelta(t, 0.5, cfg.Game.FakeLimit, 0.01) assert.InDelta(t, 0.001, cfg.Game.BarbarianRise, 0) assert.Equal(t, 1, cfg.Game.BarbarianShrink) assert.Equal(t, 800, cfg.Game.BarbarianMaxPoints) assert.Equal(t, 1, cfg.Game.Scavenging) assert.Equal(t, 2, cfg.Game.Hauls) assert.Equal(t, 5000, cfg.Game.HaulsBase) assert.Equal(t, 1000000, cfg.Game.HaulsMax) assert.Equal(t, 0, cfg.Game.Event) assert.Equal(t, 1, cfg.Game.SuppressEvents) assert.Equal(t, -1, cfg.Buildings.CustomMain) assert.Equal(t, -1, cfg.Buildings.CustomFarm) assert.Equal(t, -1, cfg.Buildings.CustomStorage) assert.Equal(t, -1, cfg.Buildings.CustomPlace) assert.Equal(t, -1, cfg.Buildings.CustomBarracks) assert.Equal(t, -1, cfg.Buildings.CustomChurch) assert.Equal(t, -1, cfg.Buildings.CustomSmith) assert.Equal(t, -1, cfg.Buildings.CustomWood) assert.Equal(t, -1, cfg.Buildings.CustomStone) assert.Equal(t, -1, cfg.Buildings.CustomIron) assert.Equal(t, -1, cfg.Buildings.CustomMarket) assert.Equal(t, -1, cfg.Buildings.CustomStable) assert.Equal(t, -1, cfg.Buildings.CustomWall) assert.Equal(t, -1, cfg.Buildings.CustomGarage) assert.Equal(t, -1, cfg.Buildings.CustomHide) assert.Equal(t, -1, cfg.Buildings.CustomSnob) assert.Equal(t, -1, cfg.Buildings.CustomStatue) assert.Equal(t, -1, cfg.Buildings.CustomWatchtower) assert.Equal(t, 1, cfg.Snob.Gold) assert.Equal(t, 0, cfg.Snob.CheapRebuild) assert.Equal(t, 2, cfg.Snob.Rise) assert.Equal(t, 50, cfg.Snob.MaxDist) assert.InDelta(t, 1.0, cfg.Snob.Factor, 0.01) assert.Equal(t, 28000, cfg.Snob.CoinWood) assert.Equal(t, 30000, cfg.Snob.CoinStone) assert.Equal(t, 25000, cfg.Snob.CoinIron) assert.Equal(t, 0, cfg.Snob.NoBarbConquer) assert.Equal(t, 0, cfg.Ally.NoHarm) assert.Equal(t, 0, cfg.Ally.NoOtherSupport) assert.Equal(t, 0, cfg.Ally.NoOtherSupportType) assert.Equal(t, 0, cfg.Ally.AllytimeSupport) assert.Equal(t, 0, cfg.Ally.NoLeave) assert.Equal(t, 0, cfg.Ally.NoJoin) assert.Equal(t, 15, cfg.Ally.Limit) assert.Equal(t, 0, cfg.Ally.FixedAllies) assert.Equal(t, 5, cfg.Ally.WarsMemberRequirement) assert.Equal(t, 15000, cfg.Ally.WarsPointsRequirement) assert.Equal(t, 7, cfg.Ally.WarsAutoacceptDays) assert.Equal(t, 1, cfg.Ally.Levels) assert.Equal(t, "v1", cfg.Ally.XpRequirements) assert.Equal(t, 1000, cfg.Coord.MapSize) assert.Equal(t, 4, cfg.Coord.Func) assert.Equal(t, 11, cfg.Coord.EmptyVillages) assert.Equal(t, 1, cfg.Coord.BonusVillages) assert.Equal(t, 550, cfg.Coord.Inner) assert.Equal(t, 0, cfg.Coord.SelectStart) assert.Equal(t, 336, cfg.Coord.VillageMoveWait) assert.Equal(t, 1, cfg.Coord.NobleRestart) assert.Equal(t, 1, cfg.Coord.StartVillages) assert.Equal(t, 1, cfg.Sitter.Allow) assert.Equal(t, 0, cfg.Sleep.Active) assert.Equal(t, 60, cfg.Sleep.Delay) assert.Equal(t, 6, cfg.Sleep.Min) assert.Equal(t, 10, cfg.Sleep.Max) assert.Equal(t, 12, cfg.Sleep.MinAwake) assert.Equal(t, 36, cfg.Sleep.MaxAwake) assert.Equal(t, 10, cfg.Sleep.WarnTime) assert.Equal(t, 2, cfg.Night.Active) assert.Equal(t, 23, cfg.Night.StartHour) assert.Equal(t, 7, cfg.Night.EndHour) assert.InDelta(t, 3.5, cfg.Night.DefFactor, 0.01) assert.Equal(t, 14, cfg.Night.Duration) assert.Equal(t, 3, cfg.Win.Check) } func TestClient_GetUnitInfo(t *testing.T) { t.Parallel() resp, err := os.ReadFile("./testdata/unit_info.xml") require.NoError(t, err) mux := http.NewServeMux() mux.HandleFunc("/interface.php", newInterfaceXMLHandler("get_unit_info", resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) unitInfo, err := newClient(srv.Client()).GetUnitInfo(context.Background(), parseURL(t, srv.URL)) require.NoError(t, err) assert.InDelta(t, 226.66666666667, unitInfo.Spear.BuildTime, 0.01) assert.Equal(t, 1, unitInfo.Spear.Pop) assert.InDelta(t, 8.0, unitInfo.Spear.Speed, 0.01) assert.Equal(t, 10, unitInfo.Spear.Attack) assert.Equal(t, 15, unitInfo.Spear.Defense) assert.Equal(t, 45, unitInfo.Spear.DefenseCavalry) assert.Equal(t, 20, unitInfo.Spear.DefenseArcher) assert.Equal(t, 25, unitInfo.Spear.Carry) assert.InDelta(t, 333.33333333333, unitInfo.Sword.BuildTime, 0.01) assert.Equal(t, 1, unitInfo.Sword.Pop) assert.InDelta(t, 9.7777777777778, unitInfo.Sword.Speed, 0.01) assert.Equal(t, 25, unitInfo.Sword.Attack) assert.Equal(t, 50, unitInfo.Sword.Defense) assert.Equal(t, 15, unitInfo.Sword.DefenseCavalry) assert.Equal(t, 40, unitInfo.Sword.DefenseArcher) assert.Equal(t, 15, unitInfo.Sword.Carry) assert.InDelta(t, 293.33333333333, unitInfo.Axe.BuildTime, 0.01) assert.Equal(t, 1, unitInfo.Axe.Pop) assert.InDelta(t, 8.0, unitInfo.Axe.Speed, 0.01) assert.Equal(t, 40, unitInfo.Axe.Attack) assert.Equal(t, 10, unitInfo.Axe.Defense) assert.Equal(t, 5, unitInfo.Axe.DefenseCavalry) assert.Equal(t, 10, unitInfo.Axe.DefenseArcher) assert.Equal(t, 10, unitInfo.Axe.Carry) assert.InDelta(t, 200.0, unitInfo.Spy.BuildTime, 0.01) assert.Equal(t, 2, unitInfo.Spy.Pop) assert.InDelta(t, 4.0, unitInfo.Spy.Speed, 0.01) assert.Equal(t, 0, unitInfo.Spy.Attack) assert.Equal(t, 2, unitInfo.Spy.Defense) assert.Equal(t, 1, unitInfo.Spy.DefenseCavalry) assert.Equal(t, 2, unitInfo.Spy.DefenseArcher) assert.Equal(t, 0, unitInfo.Spy.Carry) assert.InDelta(t, 400.0, unitInfo.Light.BuildTime, 0.01) assert.Equal(t, 4, unitInfo.Light.Pop) assert.InDelta(t, 4.4444444444444, unitInfo.Light.Speed, 0.01) assert.Equal(t, 130, unitInfo.Light.Attack) assert.Equal(t, 30, unitInfo.Light.Defense) assert.Equal(t, 40, unitInfo.Light.DefenseCavalry) assert.Equal(t, 30, unitInfo.Light.DefenseArcher) assert.Equal(t, 80, unitInfo.Light.Carry) assert.InDelta(t, 600.0, unitInfo.Marcher.BuildTime, 0.01) assert.Equal(t, 5, unitInfo.Marcher.Pop) assert.InDelta(t, 4.4444444444444, unitInfo.Marcher.Speed, 0.01) assert.Equal(t, 120, unitInfo.Marcher.Attack) assert.Equal(t, 40, unitInfo.Marcher.Defense) assert.Equal(t, 30, unitInfo.Marcher.DefenseCavalry) assert.Equal(t, 50, unitInfo.Marcher.DefenseArcher) assert.Equal(t, 50, unitInfo.Marcher.Carry) assert.InDelta(t, 800.0, unitInfo.Heavy.BuildTime, 0.01) assert.Equal(t, 6, unitInfo.Heavy.Pop) assert.InDelta(t, 4.8888888888889, unitInfo.Heavy.Speed, 0.01) assert.Equal(t, 150, unitInfo.Heavy.Attack) assert.Equal(t, 200, unitInfo.Heavy.Defense) assert.Equal(t, 80, unitInfo.Heavy.DefenseCavalry) assert.Equal(t, 180, unitInfo.Heavy.DefenseArcher) assert.Equal(t, 50, unitInfo.Heavy.Carry) assert.InDelta(t, 1066.6666666667, unitInfo.Ram.BuildTime, 0.01) assert.Equal(t, 5, unitInfo.Ram.Pop) assert.InDelta(t, 13.333333333333, unitInfo.Ram.Speed, 0.01) assert.Equal(t, 2, unitInfo.Ram.Attack) assert.Equal(t, 20, unitInfo.Ram.Defense) assert.Equal(t, 50, unitInfo.Ram.DefenseCavalry) assert.Equal(t, 20, unitInfo.Ram.DefenseArcher) assert.Equal(t, 0, unitInfo.Ram.Carry) assert.InDelta(t, 1600.0, unitInfo.Catapult.BuildTime, 0.01) assert.Equal(t, 8, unitInfo.Catapult.Pop) assert.InDelta(t, 13.333333333333, unitInfo.Catapult.Speed, 0.01) assert.Equal(t, 100, unitInfo.Catapult.Attack) assert.Equal(t, 100, unitInfo.Catapult.Defense) assert.Equal(t, 50, unitInfo.Catapult.DefenseCavalry) assert.Equal(t, 100, unitInfo.Catapult.DefenseArcher) assert.Equal(t, 0, unitInfo.Catapult.Carry) assert.InDelta(t, 4000.0, unitInfo.Snob.BuildTime, 0.01) assert.Equal(t, 100, unitInfo.Snob.Pop) assert.InDelta(t, 15.555555555556, unitInfo.Snob.Speed, 0.01) assert.Equal(t, 30, unitInfo.Snob.Attack) assert.Equal(t, 100, unitInfo.Snob.Defense) assert.Equal(t, 50, unitInfo.Snob.DefenseCavalry) assert.Equal(t, 100, unitInfo.Snob.DefenseArcher) assert.Equal(t, 0, unitInfo.Snob.Carry) assert.InDelta(t, 1.0, unitInfo.Militia.BuildTime, 0.01) assert.Equal(t, 0, unitInfo.Militia.Pop) assert.InDelta(t, 0.016666666666667, unitInfo.Militia.Speed, 0.01) assert.Equal(t, 0, unitInfo.Militia.Attack) assert.Equal(t, 15, unitInfo.Militia.Defense) assert.Equal(t, 45, unitInfo.Militia.DefenseCavalry) assert.Equal(t, 25, unitInfo.Militia.DefenseArcher) assert.Equal(t, 0, unitInfo.Militia.Carry) } func TestClient_GetBuildingInfo(t *testing.T) { t.Parallel() resp, err := os.ReadFile("./testdata/building_info.xml") require.NoError(t, err) mux := http.NewServeMux() mux.HandleFunc("/interface.php", newInterfaceXMLHandler("get_building_info", resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) buildingInfo, err := newClient(srv.Client()).GetBuildingInfo(context.Background(), parseURL(t, srv.URL)) require.NoError(t, err) assert.Equal(t, 30, buildingInfo.Main.MaxLevel) assert.Equal(t, 1, buildingInfo.Main.MinLevel) assert.Equal(t, 90, buildingInfo.Main.Wood) assert.Equal(t, 80, buildingInfo.Main.Stone) assert.Equal(t, 70, buildingInfo.Main.Iron) assert.Equal(t, 5, buildingInfo.Main.Pop) assert.InDelta(t, 1.26, buildingInfo.Main.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Main.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Main.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Main.PopFactor, 0.01) assert.InDelta(t, 900.0, buildingInfo.Main.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Main.BuildTimeFactor, 0.01) assert.Equal(t, 25, buildingInfo.Barracks.MaxLevel) assert.Equal(t, 0, buildingInfo.Barracks.MinLevel) assert.Equal(t, 200, buildingInfo.Barracks.Wood) assert.Equal(t, 170, buildingInfo.Barracks.Stone) assert.Equal(t, 90, buildingInfo.Barracks.Iron) assert.Equal(t, 7, buildingInfo.Barracks.Pop) assert.InDelta(t, 1.26, buildingInfo.Barracks.WoodFactor, 0.01) assert.InDelta(t, 1.28, buildingInfo.Barracks.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Barracks.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Barracks.PopFactor, 0.01) assert.InDelta(t, 1800.0, buildingInfo.Barracks.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Barracks.BuildTimeFactor, 0.01) assert.Equal(t, 20, buildingInfo.Stable.MaxLevel) assert.Equal(t, 0, buildingInfo.Stable.MinLevel) assert.Equal(t, 270, buildingInfo.Stable.Wood) assert.Equal(t, 240, buildingInfo.Stable.Stone) assert.Equal(t, 260, buildingInfo.Stable.Iron) assert.Equal(t, 8, buildingInfo.Stable.Pop) assert.InDelta(t, 1.26, buildingInfo.Stable.WoodFactor, 0.01) assert.InDelta(t, 1.28, buildingInfo.Stable.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Stable.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Stable.PopFactor, 0.01) assert.InDelta(t, 6000.0, buildingInfo.Stable.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Stable.BuildTimeFactor, 0.01) assert.Equal(t, 15, buildingInfo.Garage.MaxLevel) assert.Equal(t, 0, buildingInfo.Garage.MinLevel) assert.Equal(t, 300, buildingInfo.Garage.Wood) assert.Equal(t, 240, buildingInfo.Garage.Stone) assert.Equal(t, 260, buildingInfo.Garage.Iron) assert.Equal(t, 8, buildingInfo.Garage.Pop) assert.InDelta(t, 1.26, buildingInfo.Garage.WoodFactor, 0.01) assert.InDelta(t, 1.28, buildingInfo.Garage.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Garage.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Garage.PopFactor, 0.01) assert.InDelta(t, 6000.0, buildingInfo.Garage.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Garage.BuildTimeFactor, 0.01) assert.Equal(t, 20, buildingInfo.Watchtower.MaxLevel) assert.Equal(t, 0, buildingInfo.Watchtower.MinLevel) assert.Equal(t, 12000, buildingInfo.Watchtower.Wood) assert.Equal(t, 14000, buildingInfo.Watchtower.Stone) assert.Equal(t, 10000, buildingInfo.Watchtower.Iron) assert.Equal(t, 500, buildingInfo.Watchtower.Pop) assert.InDelta(t, 1.17, buildingInfo.Watchtower.WoodFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Watchtower.StoneFactor, 0.01) assert.InDelta(t, 1.18, buildingInfo.Watchtower.IronFactor, 0.01) assert.InDelta(t, 1.18, buildingInfo.Watchtower.PopFactor, 0.01) assert.InDelta(t, 13200.0, buildingInfo.Watchtower.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Watchtower.BuildTimeFactor, 0.01) assert.Equal(t, 1, buildingInfo.Snob.MaxLevel) assert.Equal(t, 0, buildingInfo.Snob.MinLevel) assert.Equal(t, 15000, buildingInfo.Snob.Wood) assert.Equal(t, 25000, buildingInfo.Snob.Stone) assert.Equal(t, 10000, buildingInfo.Snob.Iron) assert.Equal(t, 80, buildingInfo.Snob.Pop) assert.InDelta(t, 2.0, buildingInfo.Snob.WoodFactor, 0.01) assert.InDelta(t, 2.0, buildingInfo.Snob.StoneFactor, 0.01) assert.InDelta(t, 2.0, buildingInfo.Snob.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Snob.PopFactor, 0.01) assert.InDelta(t, 586800.0, buildingInfo.Snob.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Snob.BuildTimeFactor, 0.01) assert.Equal(t, 20, buildingInfo.Smith.MaxLevel) assert.Equal(t, 0, buildingInfo.Smith.MinLevel) assert.Equal(t, 220, buildingInfo.Smith.Wood) assert.Equal(t, 180, buildingInfo.Smith.Stone) assert.Equal(t, 240, buildingInfo.Smith.Iron) assert.Equal(t, 20, buildingInfo.Smith.Pop) assert.InDelta(t, 1.26, buildingInfo.Smith.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Smith.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Smith.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Smith.PopFactor, 0.01) assert.InDelta(t, 6000.0, buildingInfo.Smith.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Smith.BuildTimeFactor, 0.01) assert.Equal(t, 1, buildingInfo.Place.MaxLevel) assert.Equal(t, 0, buildingInfo.Place.MinLevel) assert.Equal(t, 10, buildingInfo.Place.Wood) assert.Equal(t, 40, buildingInfo.Place.Stone) assert.Equal(t, 30, buildingInfo.Place.Iron) assert.Equal(t, 0, buildingInfo.Place.Pop) assert.InDelta(t, 1.26, buildingInfo.Place.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Place.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Place.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Place.PopFactor, 0.01) assert.InDelta(t, 10860.0, buildingInfo.Place.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Place.BuildTimeFactor, 0.01) assert.Equal(t, 1, buildingInfo.Statue.MaxLevel) assert.Equal(t, 0, buildingInfo.Statue.MinLevel) assert.Equal(t, 220, buildingInfo.Statue.Wood) assert.Equal(t, 220, buildingInfo.Statue.Stone) assert.Equal(t, 220, buildingInfo.Statue.Iron) assert.Equal(t, 10, buildingInfo.Statue.Pop) assert.InDelta(t, 1.26, buildingInfo.Statue.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Statue.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Statue.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Statue.PopFactor, 0.01) assert.InDelta(t, 1500.0, buildingInfo.Statue.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Statue.BuildTimeFactor, 0.01) assert.Equal(t, 25, buildingInfo.Market.MaxLevel) assert.Equal(t, 0, buildingInfo.Market.MinLevel) assert.Equal(t, 100, buildingInfo.Market.Wood) assert.Equal(t, 100, buildingInfo.Market.Stone) assert.Equal(t, 100, buildingInfo.Market.Iron) assert.Equal(t, 20, buildingInfo.Market.Pop) assert.InDelta(t, 1.26, buildingInfo.Market.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Market.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Market.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Market.PopFactor, 0.01) assert.InDelta(t, 2700.0, buildingInfo.Market.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Market.BuildTimeFactor, 0.01) assert.Equal(t, 30, buildingInfo.Wood.MaxLevel) assert.Equal(t, 0, buildingInfo.Wood.MinLevel) assert.Equal(t, 50, buildingInfo.Wood.Wood) assert.Equal(t, 60, buildingInfo.Wood.Stone) assert.Equal(t, 40, buildingInfo.Wood.Iron) assert.Equal(t, 5, buildingInfo.Wood.Pop) assert.InDelta(t, 1.25, buildingInfo.Wood.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Wood.StoneFactor, 0.01) assert.InDelta(t, 1.245, buildingInfo.Wood.IronFactor, 0.01) assert.InDelta(t, 1.155, buildingInfo.Wood.PopFactor, 0.01) assert.InDelta(t, 900.0, buildingInfo.Wood.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Wood.BuildTimeFactor, 0.01) assert.Equal(t, 30, buildingInfo.Stone.MaxLevel) assert.Equal(t, 0, buildingInfo.Stone.MinLevel) assert.Equal(t, 65, buildingInfo.Stone.Wood) assert.Equal(t, 50, buildingInfo.Stone.Stone) assert.Equal(t, 40, buildingInfo.Stone.Iron) assert.Equal(t, 10, buildingInfo.Stone.Pop) assert.InDelta(t, 1.27, buildingInfo.Stone.WoodFactor, 0.01) assert.InDelta(t, 1.265, buildingInfo.Stone.StoneFactor, 0.01) assert.InDelta(t, 1.24, buildingInfo.Stone.IronFactor, 0.01) assert.InDelta(t, 1.14, buildingInfo.Stone.PopFactor, 0.01) assert.InDelta(t, 900.0, buildingInfo.Stone.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Stone.BuildTimeFactor, 0.01) assert.Equal(t, 30, buildingInfo.Iron.MaxLevel) assert.Equal(t, 0, buildingInfo.Iron.MinLevel) assert.Equal(t, 75, buildingInfo.Iron.Wood) assert.Equal(t, 65, buildingInfo.Iron.Stone) assert.Equal(t, 70, buildingInfo.Iron.Iron) assert.Equal(t, 10, buildingInfo.Iron.Pop) assert.InDelta(t, 1.252, buildingInfo.Iron.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Iron.StoneFactor, 0.01) assert.InDelta(t, 1.24, buildingInfo.Iron.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Iron.PopFactor, 0.01) assert.InDelta(t, 1080.0, buildingInfo.Iron.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Iron.BuildTimeFactor, 0.01) assert.Equal(t, 30, buildingInfo.Farm.MaxLevel) assert.Equal(t, 1, buildingInfo.Farm.MinLevel) assert.Equal(t, 45, buildingInfo.Farm.Wood) assert.Equal(t, 40, buildingInfo.Farm.Stone) assert.Equal(t, 30, buildingInfo.Farm.Iron) assert.Equal(t, 0, buildingInfo.Farm.Pop) assert.InDelta(t, 1.3, buildingInfo.Farm.WoodFactor, 0.01) assert.InDelta(t, 1.32, buildingInfo.Farm.StoneFactor, 0.01) assert.InDelta(t, 1.29, buildingInfo.Farm.IronFactor, 0.01) assert.InDelta(t, 1.0, buildingInfo.Farm.PopFactor, 0.01) assert.InDelta(t, 1200.0, buildingInfo.Farm.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Farm.BuildTimeFactor, 0.01) assert.Equal(t, 30, buildingInfo.Storage.MaxLevel) assert.Equal(t, 1, buildingInfo.Storage.MinLevel) assert.Equal(t, 60, buildingInfo.Storage.Wood) assert.Equal(t, 50, buildingInfo.Storage.Stone) assert.Equal(t, 40, buildingInfo.Storage.Iron) assert.Equal(t, 0, buildingInfo.Storage.Pop) assert.InDelta(t, 1.265, buildingInfo.Storage.WoodFactor, 0.01) assert.InDelta(t, 1.27, buildingInfo.Storage.StoneFactor, 0.01) assert.InDelta(t, 1.245, buildingInfo.Storage.IronFactor, 0.01) assert.InDelta(t, 1.15, buildingInfo.Storage.PopFactor, 0.01) assert.InDelta(t, 1020.0, buildingInfo.Storage.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Storage.BuildTimeFactor, 0.01) assert.Equal(t, 10, buildingInfo.Hide.MaxLevel) assert.Equal(t, 0, buildingInfo.Hide.MinLevel) assert.Equal(t, 50, buildingInfo.Hide.Wood) assert.Equal(t, 60, buildingInfo.Hide.Stone) assert.Equal(t, 50, buildingInfo.Hide.Iron) assert.Equal(t, 2, buildingInfo.Hide.Pop) assert.InDelta(t, 1.25, buildingInfo.Hide.WoodFactor, 0.01) assert.InDelta(t, 1.25, buildingInfo.Hide.StoneFactor, 0.01) assert.InDelta(t, 1.25, buildingInfo.Hide.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Hide.PopFactor, 0.01) assert.InDelta(t, 1800.0, buildingInfo.Hide.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Hide.BuildTimeFactor, 0.01) assert.Equal(t, 20, buildingInfo.Wall.MaxLevel) assert.Equal(t, 0, buildingInfo.Wall.MinLevel) assert.Equal(t, 50, buildingInfo.Wall.Wood) assert.Equal(t, 100, buildingInfo.Wall.Stone) assert.Equal(t, 20, buildingInfo.Wall.Iron) assert.Equal(t, 5, buildingInfo.Wall.Pop) assert.InDelta(t, 1.26, buildingInfo.Wall.WoodFactor, 0.01) assert.InDelta(t, 1.275, buildingInfo.Wall.StoneFactor, 0.01) assert.InDelta(t, 1.26, buildingInfo.Wall.IronFactor, 0.01) assert.InDelta(t, 1.17, buildingInfo.Wall.PopFactor, 0.01) assert.InDelta(t, 3600.0, buildingInfo.Wall.BuildTime, 0.01) assert.InDelta(t, 1.2, buildingInfo.Wall.BuildTimeFactor, 0.01) } func TestClient_GetTribes(t *testing.T) { t.Parallel() tests := []struct { name string respTribe string respOD string respODA string respODD string checkErr func(err error) bool expectedTribes func(t *testing.T, url string) []tw.Tribe }{ { name: "OK", respTribe: "1,name%201,tag%201,100,101,102,103,104\n2,name2,tag2,200,202,202,203,204\n3,name3,tag3,300,303,302,303,304\n4,name4,tag4,400,404,402,403,404", //nolint:lll respOD: "1,1,1\n2,2,2\n3,3,3", respODA: "1,1,1\n2,2,2\n3,3,3", respODD: "1,1,1\n2,2,2\n3,3,3", expectedTribes: func(t *testing.T, baseURL string) []tw.Tribe { t.Helper() return []tw.Tribe{ { ID: 1, Name: "name 1", Tag: "tag 1", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=1"), NumMembers: 100, NumVillages: 101, Points: 102, AllPoints: 103, Rank: 104, OpponentsDefeated: tw.OpponentsDefeated{ RankAtt: 1, ScoreAtt: 1, RankDef: 1, ScoreDef: 1, RankSup: 0, ScoreSup: 0, RankTotal: 1, ScoreTotal: 1, }, }, { ID: 2, Name: "name2", Tag: "tag2", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=2"), NumMembers: 200, NumVillages: 202, Points: 202, AllPoints: 203, Rank: 204, OpponentsDefeated: tw.OpponentsDefeated{ RankAtt: 2, ScoreAtt: 2, RankDef: 2, ScoreDef: 2, RankSup: 0, ScoreSup: 0, RankTotal: 2, ScoreTotal: 2, }, }, { ID: 3, Name: "name3", Tag: "tag3", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=3"), NumMembers: 300, NumVillages: 303, Points: 302, AllPoints: 303, Rank: 304, OpponentsDefeated: tw.OpponentsDefeated{ RankAtt: 3, ScoreAtt: 3, RankDef: 3, ScoreDef: 3, RankSup: 0, ScoreSup: 0, RankTotal: 3, ScoreTotal: 3, }, }, { ID: 4, Name: "name4", Tag: "tag4", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=4"), NumMembers: 400, NumVillages: 404, Points: 402, AllPoints: 403, Rank: 404, OpponentsDefeated: tw.OpponentsDefeated{}, }, } }, }, { name: "ERR: OD.Rank can't be converted to int64", respOD: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: OD.ID -can't be converted to int64", respOD: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: OD.Score can't be converted to int64", respOD: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODA.Rank can't be converted to int64", respODA: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODA.ID can't be converted to int64", respODA: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODA.Score can't be converted to int64", respODA: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODD.Rank can't be converted to int64", respODD: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODD.ID can't be converted to int64", respODD: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODD.Score can't be converted to int64", respODD: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: too few fields", respTribe: "1,name%201,tag%201,100,101,102,103", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: too many fields", respTribe: "1,name%201,tag%201,100,101,102,103,104,105", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: Tribe.ID can't be converted to int64", respTribe: "1.23,name1,tag1,100,101,102,103,104", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Tribe.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Tribe.NumMembers can't be converted to int64", respTribe: "1,name1,tag1,100.23,101,102,103,104", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Tribe.NumMembers" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Tribe.NumVillages can't be converted to int64", respTribe: "1,name1,tag1,100,101.23,102,103,104", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Tribe.NumVillages" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Tribe.Points can't be converted to int64", respTribe: "1,name1,tag1,100,101,102.23,103,104", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Tribe.Points" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Tribe.AllPoints can't be converted to int64", respTribe: "1,name1,tag1,100,101,102,103.23,104", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Tribe.AllPoints" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Tribe.Rank can't be converted to int64", respTribe: "1,name1,tag1,100,101,102,103,104.23", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Tribe.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc("/map/kill_all_tribe.txt", newPlainTextHandlerString(tt.respOD)) mux.HandleFunc("/map/kill_att_tribe.txt", newPlainTextHandlerString(tt.respODA)) mux.HandleFunc("/map/kill_def_tribe.txt", newPlainTextHandlerString(tt.respODD)) mux.HandleFunc("/map/ally.txt", newPlainTextHandlerString(tt.respTribe)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) tribes, err := newClient(srv.Client()).GetTribes(context.Background(), parseURL(t, srv.URL)) checkErr := func(err error) bool { return errors.Is(err, nil) } if tt.checkErr != nil { checkErr = tt.checkErr } var expectedTribes []tw.Tribe if tt.expectedTribes != nil { expectedTribes = tt.expectedTribes(t, srv.URL) } assert.True(t, checkErr(err)) require.Len(t, tribes, len(expectedTribes)) for i, tribe := range tribes { assert.Equal(t, expectedTribes[i], tribe) } }) } } func TestClient_GetPlayers(t *testing.T) { t.Parallel() tests := []struct { name string respPlayer string respOD string respODA string respODD string respODS string checkErr func(err error) bool expectedPlayers func(t *testing.T, url string) []tw.Player }{ { name: "OK", respPlayer: "1,name%201,123,124,125,126\n2,name2,256,257,258,259\n3,name3,356,357,358,359\n4,name4,456,457,458,459", respOD: "1,1,1001\n2,2,252\n3,3,125", respODA: "1,1,1000\n2,2,253\n3,3,100", respODD: "1,1,1002\n2,2,251\n3,3,155", respODS: "1,1,1003\n2,2,250\n3,3,166", expectedPlayers: func(t *testing.T, baseURL string) []tw.Player { t.Helper() return []tw.Player{ { ID: 1, Name: "name 1", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=1"), TribeID: 123, NumVillages: 124, Points: 125, Rank: 126, OpponentsDefeated: tw.OpponentsDefeated{ RankAtt: 1, ScoreAtt: 1000, RankDef: 1, ScoreDef: 1002, RankSup: 1, ScoreSup: 1003, RankTotal: 1, ScoreTotal: 1001, }, }, { ID: 2, Name: "name2", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=2"), TribeID: 256, NumVillages: 257, Points: 258, Rank: 259, OpponentsDefeated: tw.OpponentsDefeated{ RankAtt: 2, ScoreAtt: 253, RankDef: 2, ScoreDef: 251, RankSup: 2, ScoreSup: 250, RankTotal: 2, ScoreTotal: 252, }, }, { ID: 3, Name: "name3", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=3"), TribeID: 356, NumVillages: 357, Points: 358, Rank: 359, OpponentsDefeated: tw.OpponentsDefeated{ RankAtt: 3, ScoreAtt: 100, RankDef: 3, ScoreDef: 155, RankSup: 3, ScoreSup: 166, RankTotal: 3, ScoreTotal: 125, }, }, { ID: 4, Name: "name4", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=4"), TribeID: 456, NumVillages: 457, Points: 458, Rank: 459, OpponentsDefeated: tw.OpponentsDefeated{}, }, } }, }, { name: "ERR: OD.Rank can't be converted to int64", respOD: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: OD.ID can't be converted to int64", respOD: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: OD.Score can't be converted to int64", respOD: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODA.Rank can't be converted to int64", respODA: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODA.ID can't be converted to int64", respODA: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODA.Score can't be converted to int64", respODA: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODD.Rank can't be converted to int64", respODD: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODD.ID can't be converted to int64", respODD: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODD.Score can't be converted to int64", respODD: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODS.Rank can't be converted to int64", respODS: "test,1,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODS.ID can't be converted to int64", respODS: "1,test,1001", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: ODS.Score can't be converted to int64", respODS: "1,1,test", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "odRecord.Score" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: too few fields", respPlayer: "1,name%201,123,124,125", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: too many fields", respPlayer: "1,name%201,123,124,125,126,127", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: Player.ID can't be converted to int64", respPlayer: "1.25,name,123,124,125,126", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Player.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Player.TribeID can't be converted to int64", respPlayer: "1,name,1.23,124,125,126", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Player.TribeID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Player.NumVillages can't be converted to int64", respPlayer: "1,name,123,1.24,125,126", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Player.NumVillages" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Player.Points can't be converted to int64", respPlayer: "1,name,123,124,1.25,126", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Player.Points" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Player.Rank can't be converted to int64", respPlayer: "1,name,123,124,125,1.26", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Player.Rank" && errors.Is(parseErr, strconv.ErrSyntax) }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc("/map/kill_all.txt", newPlainTextHandlerString(tt.respOD)) mux.HandleFunc("/map/kill_att.txt", newPlainTextHandlerString(tt.respODA)) mux.HandleFunc("/map/kill_def.txt", newPlainTextHandlerString(tt.respODD)) mux.HandleFunc("/map/kill_sup.txt", newPlainTextHandlerString(tt.respODS)) mux.HandleFunc("/map/player.txt", newPlainTextHandlerString(tt.respPlayer)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) players, err := newClient(srv.Client()).GetPlayers(context.Background(), parseURL(t, srv.URL)) checkErr := func(err error) bool { return errors.Is(err, nil) } if tt.checkErr != nil { checkErr = tt.checkErr } var expectedPlayers []tw.Player if tt.expectedPlayers != nil { expectedPlayers = tt.expectedPlayers(t, srv.URL) } assert.True(t, checkErr(err)) require.Len(t, players, len(expectedPlayers)) for i, player := range players { assert.Equal(t, expectedPlayers[i], player) } }) } } func TestClient_GetVillages(t *testing.T) { t.Parallel() tests := []struct { name string resp string checkErr func(err error) bool expectedVillages func(t *testing.T, baseURL string) []tw.Village }{ { name: "OK", //nolint:lll resp: "123,village%201,500,501,502,503,504\n124,village 2,100,201,102,103,104\n125,village%203,1,501,502,503,504\n126,village%204,1,1,502,503,504\n127,village%205,103,1,502,503,504", expectedVillages: func(t *testing.T, baseURL string) []tw.Village { t.Helper() return []tw.Village{ { ID: 123, Name: "village 1", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=123"), X: 500, Y: 501, Continent: "K55", PlayerID: 502, Points: 503, Bonus: 504, }, { ID: 124, Name: "village 2", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=124"), X: 100, Y: 201, Continent: "K21", PlayerID: 102, Points: 103, Bonus: 104, }, { ID: 125, Name: "village 3", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=125"), X: 1, Y: 501, Continent: "K50", PlayerID: 502, Points: 503, Bonus: 504, }, { ID: 126, Name: "village 4", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=126"), X: 1, Y: 1, Continent: "K0", PlayerID: 502, Points: 503, Bonus: 504, }, { ID: 127, Name: "village 5", ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=127"), X: 103, Y: 1, Continent: "K1", PlayerID: 502, Points: 503, Bonus: 504, }, } }, }, { name: "ERR: too few fields", resp: "123,village 1,500,501,502,503", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: too many fields", resp: "123,village 1,500,501,502,503,504,505", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: Village.ID can't be converted to int64", resp: "123.23,village 1,500,501,502,503,504", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Village.ID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Village.X can't be converted to int64", resp: "123,village 1,123.23,501,502,503,504", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Village.X" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Village.Y can't be converted to int64", resp: "123,village 1,500,123.23,502,503,504", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Village.Y" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Village.PlayerID can't be converted to int64", resp: "123,village 1,500,501,123.23,503,504", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Village.PlayerID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Village.Points can't be converted to int64", resp: "123,village 1,500,501,502,123.23,504", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Village.Points" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Village.Bonus can't be converted to int64", resp: "123,village 1,500,501,502,503,123.23", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Village.Bonus" && errors.Is(parseErr, strconv.ErrSyntax) }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc("/map/village.txt", newPlainTextHandlerString(tt.resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) villages, err := newClient(srv.Client()).GetVillages(context.Background(), parseURL(t, srv.URL)) checkErr := func(err error) bool { return errors.Is(err, nil) } if tt.checkErr != nil { checkErr = tt.checkErr } var expectedVillages []tw.Village if tt.expectedVillages != nil { expectedVillages = tt.expectedVillages(t, srv.URL) } assert.True(t, checkErr(err)) require.Len(t, villages, len(expectedVillages)) for i, village := range villages { assert.Equal(t, expectedVillages[i], village) } }) } } func TestClient_GetEnnoblements(t *testing.T) { t.Parallel() t.Run("map/conquer_extended.txt", func(t *testing.T) { t.Parallel() tests := []struct { name string since time.Time resp string checkErr func(err error) bool expectedEnnoblements []tw.Ennoblement }{ { name: "OK", since: time.Unix(1657842401, 0), //nolint:lll resp: "1424,1657842406,698953084,699589674,0,122,380\n3150,1657882879,6461674,0,0,179,111\n1025,1657947400,9157005,698942601,0,52,461\n1025,1657842400,9157005,698942601,0,52,461", checkErr: nil, expectedEnnoblements: []tw.Ennoblement{ // the last one should be skipped (1657842401 > 1657842400) { VillageID: 1424, NewOwnerID: 698953084, NewTribeID: 122, OldOwnerID: 699589674, OldTribeID: 0, Points: 380, CreatedAt: time.Unix(1657842406, 0), }, { VillageID: 3150, NewOwnerID: 6461674, NewTribeID: 179, OldOwnerID: 0, OldTribeID: 0, Points: 111, CreatedAt: time.Unix(1657882879, 0), }, { VillageID: 1025, NewOwnerID: 9157005, NewTribeID: 52, OldOwnerID: 698942601, OldTribeID: 0, Points: 461, CreatedAt: time.Unix(1657947400, 0), }, }, }, { name: "ERR: too few fields", resp: "1424,1657842406,698953084,699589674,0,122", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: too many fields", resp: "1424,1657842406,698953084,699589674,0,122,380,0", checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: Ennoblement.VillageID can't be converted to int64", resp: "asd,1657842406,698953084,699589674,0,122,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.VillageID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.VillageID can't be converted to int64", resp: "1424,asd,698953084,699589674,0,122,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.CreatedAt" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.NewOwnerID can't be converted to int64", resp: "1424,1657842406,asd,699589674,0,122,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.NewOwnerID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.OldOwnerID can't be converted to int64", resp: "1424,1657842406,698953084,asd,0,122,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.OldOwnerID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.OldTribeID can't be converted to int64", resp: "1424,1657842406,698953084,699589674,asd,122,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.OldTribeID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.NewTribeID can't be converted to int64", resp: "1424,1657842406,698953084,699589674,0,asd,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.NewTribeID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.Points can't be converted to int64", resp: "1424,1657842406,698953084,699589674,0,122,asd", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.Points" && errors.Is(parseErr, strconv.ErrSyntax) }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc("/map/conquer_extended.txt", newPlainTextHandlerString(tt.resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) ennoblements, err := newClient(srv.Client()).GetEnnoblements(context.Background(), parseURL(t, srv.URL), tt.since) checkErr := func(err error) bool { return errors.Is(err, nil) } if tt.checkErr != nil { checkErr = tt.checkErr } assert.True(t, checkErr(err)) require.Len(t, ennoblements, len(tt.expectedEnnoblements)) for i, ennoblement := range ennoblements { assert.Equal(t, tt.expectedEnnoblements[i], ennoblement) } }) } }) t.Run("interface.php?func=get_conquer_extended", func(t *testing.T) { t.Parallel() now := time.Now() tests := []struct { name string since time.Time resp string checkErr func(err error) bool clientOpts []tw.ClientOption expectedEnnoblements []tw.Ennoblement }{ { name: "OK: without custom ennoblementsUseInterfaceFunc", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf( "1424,%d,698953084,699589674,0,122,380\n3150,%d,6461674,0,0,179,111\n1025,%d,9157005,698942601,0,52,461", now.Add(-21*time.Hour).Unix(), now.Add(-15*time.Minute).Unix(), now.Add(-15*time.Second).Unix(), ), checkErr: nil, expectedEnnoblements: []tw.Ennoblement{ { VillageID: 1424, NewOwnerID: 698953084, NewTribeID: 122, OldOwnerID: 699589674, OldTribeID: 0, Points: 380, CreatedAt: time.Unix(now.Add(-21*time.Hour).Unix(), 0), }, { VillageID: 3150, NewOwnerID: 6461674, NewTribeID: 179, OldOwnerID: 0, OldTribeID: 0, Points: 111, CreatedAt: time.Unix(now.Add(-15*time.Minute).Unix(), 0), }, { VillageID: 1025, NewOwnerID: 9157005, NewTribeID: 52, OldOwnerID: 698942601, OldTribeID: 0, Points: 461, CreatedAt: time.Unix(now.Add(-15*time.Second).Unix(), 0), }, }, }, { name: "OK: with custom ennoblementsUseInterfaceFunc", since: now.Add(-47 * time.Hour), resp: fmt.Sprintf( "1424,%d,698953084,699589674,0,122,380\n3150,%d,6461674,0,0,179,111\n1025,%d,9157005,698942601,0,52,461", now.Add(-44*time.Hour).Unix(), now.Add(-40*time.Minute).Unix(), now.Add(-40*time.Second).Unix(), ), checkErr: nil, clientOpts: []tw.ClientOption{ tw.WithEnnoblementsUseInterfaceFunc(func(since time.Time) bool { return since.After(time.Now().Add(-48 * time.Hour)) }), }, expectedEnnoblements: []tw.Ennoblement{ { VillageID: 1424, NewOwnerID: 698953084, NewTribeID: 122, OldOwnerID: 699589674, OldTribeID: 0, Points: 380, CreatedAt: time.Unix(now.Add(-44*time.Hour).Unix(), 0), }, { VillageID: 3150, NewOwnerID: 6461674, NewTribeID: 179, OldOwnerID: 0, OldTribeID: 0, Points: 111, CreatedAt: time.Unix(now.Add(-40*time.Minute).Unix(), 0), }, { VillageID: 1025, NewOwnerID: 9157005, NewTribeID: 52, OldOwnerID: 698942601, OldTribeID: 0, Points: 461, CreatedAt: time.Unix(now.Add(-40*time.Second).Unix(), 0), }, }, }, { name: "ERR: too few fields", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,698953084,699589674,0,122", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: too many fields", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,698953084,699589674,0,122,380,0", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { return errors.Is(err, csv.ErrFieldCount) }, }, { name: "ERR: Ennoblement.VillageID can't be converted to int64", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("asd,%d,698953084,699589674,0,122,380", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.VillageID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.CreatedAt can't be converted to int64", since: now.Add(-22 * time.Hour), resp: "1424,asd,698953084,699589674,0,122,380", checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.CreatedAt" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.NewOwnerID can't be converted to int64", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,asd,699589674,0,122,380", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.NewOwnerID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.OldOwnerID can't be converted to int64", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,698953084,asd,0,122,380", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.OldOwnerID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.OldTribeID can't be converted to int64", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,698953084,699589674,asd,122,380", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.OldTribeID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.NewTribeID can't be converted to int64", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,698953084,699589674,0,asd,380", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.NewTribeID" && errors.Is(parseErr, strconv.ErrSyntax) }, }, { name: "ERR: Ennoblement.Points can't be converted to int64", since: now.Add(-22 * time.Hour), resp: fmt.Sprintf("1424,%d,698953084,699589674,0,122,asd", now.Add(-21*time.Hour).Unix()), checkErr: func(err error) bool { var parseErr tw.ParseError return errors.As(err, &parseErr) && parseErr.Field == "Ennoblement.Points" && errors.Is(parseErr, strconv.ErrSyntax) }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc("/interface.php", newInterfacePlainTextHandlerString("get_conquer_extended", tt.resp)) srv := httptest.NewTLSServer(mux) t.Cleanup(srv.Close) client := newClient(srv.Client(), tt.clientOpts...) ennoblements, err := client.GetEnnoblements(context.Background(), parseURL(t, srv.URL), tt.since) checkErr := func(err error) bool { return errors.Is(err, nil) } if tt.checkErr != nil { checkErr = tt.checkErr } assert.True(t, checkErr(err)) require.Len(t, ennoblements, len(tt.expectedEnnoblements)) for i, ennoblement := range ennoblements { assert.Equal(t, tt.expectedEnnoblements[i], ennoblement) } }) } }) } func parseURL(tb testing.TB, rawURL string) *url.URL { tb.Helper() u, err := url.ParseRequestURI(rawURL) require.NoError(tb, err) return u } // user agent and http client can't be overridden via opts func newClient(hc *http.Client, opts ...tw.ClientOption) *tw.Client { return tw.NewClient(append(opts, tw.WithHTTPClient(hc), tw.WithUserAgent(testUserAgent))...) } func newInterfaceXMLHandler(funcName string, result []byte) func(w http.ResponseWriter, r *http.Request) { return newInterfaceHandler(funcName, "text/xml;charset=utf-8", result) } func newInterfacePlainTextHandler(funcName string, result []byte) func(w http.ResponseWriter, r *http.Request) { return newInterfaceHandler(funcName, "text/plain", result) } func newInterfacePlainTextHandlerString(funcName, result string) func(w http.ResponseWriter, r *http.Request) { return newInterfacePlainTextHandler(funcName, []byte(result)) } func newInterfaceHandler(funcName, contentType string, result []byte) func(w http.ResponseWriter, r *http.Request) { next := newHandler(contentType, result) return func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("func") != funcName { w.WriteHeader(http.StatusNotFound) return } next(w, r) } } func newPlainTextHandler(result []byte) func(w http.ResponseWriter, r *http.Request) { return newHandler("text/plain", result) } func newPlainTextHandlerString(result string) func(w http.ResponseWriter, r *http.Request) { return newPlainTextHandler([]byte(result)) } func newHandler(contentType string, result []byte) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } if r.Header.Get("User-Agent") != testUserAgent { w.WriteHeader(http.StatusBadRequest) return } w.Header().Set("Content-Type", contentType) _, _ = w.Write(result) } }