From 5d4e210a2611806affbe446a1dc5d21b40321e91 Mon Sep 17 00:00:00 2001 From: stevenh Date: Thu, 16 Feb 2012 11:51:10 +0000 Subject: [PATCH 1/4] Removed EnableBanOnUsernames code from FindBanTest as its no longer applicable --- UnitTests/BanManagerTest.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/UnitTests/BanManagerTest.cs b/UnitTests/BanManagerTest.cs index bcd4146b..2f4daa67 100644 --- a/UnitTests/BanManagerTest.cs +++ b/UnitTests/BanManagerTest.cs @@ -61,11 +61,7 @@ namespace UnitTests public void FindBanTest() { Assert.IsNotNull(Bans.GetBanByIp("127.0.0.1")); - TShock.Config.EnableBanOnUsernames = true; Assert.IsNotNull(Bans.GetBanByName("BanTest")); - // Disabled this this for now as its currently expected behavour - //TShock.Config.EnableBanOnUsernames = false; - //Assert.IsNull(Bans.GetBanByName("BanTest")); } } } From c258406bcbf96cf27bb61363d723a1b1aeee5c3c Mon Sep 17 00:00:00 2001 From: stevenh Date: Thu, 16 Feb 2012 12:01:02 +0000 Subject: [PATCH 2/4] Added parameterless constructor for RestObject required for JavaScriptSerializer.Deserialize --- TShockAPI/Rest/RestObject.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/TShockAPI/Rest/RestObject.cs b/TShockAPI/Rest/RestObject.cs index e63233dc..130e2ec8 100644 --- a/TShockAPI/Rest/RestObject.cs +++ b/TShockAPI/Rest/RestObject.cs @@ -41,7 +41,14 @@ namespace Rests set { this["response"] = value; } } - public RestObject(string status = "200") + // Parameterless constructor for deseralisation required by JavaScriptSerializer.Deserialize in TShockRestTestPlugin + // Note: The constructor with all defaults isn't good enough :( + public RestObject() + { + Status = "200"; + } + + public RestObject(string status = "200") { Status = status; } From dee2c1a9f4f81bf603bc35a40ee8e010d7c86c09 Mon Sep 17 00:00:00 2001 From: stevenh Date: Thu, 16 Feb 2012 12:03:08 +0000 Subject: [PATCH 3/4] Refactored output processing to always return RestObject's Refactored method names to be consistent with function e.g. Off -> ServerOff Added new endpoints:- /v2/server/status - Status query including player and rules matching common query tool requirements such as qstat. The json returned uses native types e.g. int's where possible /v2/groups/list - Lists groups /v2/groups/read - Query group detail /v2/groups/destroy - Remove a group /v2/groups/create - Create a group /v2/players/list - Lists players including basic details /v2/users/create - Create a user /v2/users/list - Lists users basic user details applying a filter if specified. Refactored missing parameter and error handling to use common utility methods RestError, RestMissingParam which ensures consistency across all functions and protects against null object references Removed maxplayers from /status to ensure 100% compatibility, this and more details can be found in the new end point /v2/server/status Protected against null names in user/activelist Refactored variable checks to use string.IsNullOrWhiteSpace where applicable to capture errors early in the RestAPI Added the ability to location users by "id" matching the underlying API. Refactored locating players, bans & users to utility methods to ensure consitency across the api and eliminate duplicate code making for easier maintainence NOTE: Duplicate and invalid DB entries currently can still be made if the DB format hasn't been updated to with missing unique and not null constaints Removed { RequiresToken = true } from RestCommand constructors as this is the default anyway. Optimised function calls so that tests are processed sequentially avoiding unnessasary operations in failure cases Cleaned up formatting ensuring consistent line endings, indentation and single line if layout Fixed ServerCommand duplicate key issue for multi line returns, now uses a single "response" string with embeded newlines Fixed PlayerKill output using verb instead of parameters for "from" option --- TShockAPI/Rest/RestManager.cs | 1041 +++++++++++++++++++-------------- 1 file changed, 588 insertions(+), 453 deletions(-) diff --git a/TShockAPI/Rest/RestManager.cs b/TShockAPI/Rest/RestManager.cs index 4fd20273..ed79492e 100644 --- a/TShockAPI/Rest/RestManager.cs +++ b/TShockAPI/Rest/RestManager.cs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using HttpServer; @@ -36,395 +37,393 @@ namespace TShockAPI public void RegisterRestfulCommands() { - Rest.Register(new RestCommand("/status", Status) {RequiresToken = false}); - Rest.Register(new RestCommand("/tokentest", TokenTest) {RequiresToken = true}); + // Server Commands + Rest.Register(new RestCommand("/v2/server/broadcast", ServerBroadcast)); + Rest.Register(new RestCommand("/v2/server/off", ServerOff)); + Rest.Register(new RestCommand("/v2/server/rawcmd", ServerCommand)); + Rest.Register(new RestCommand("/v2/server/status", ServerStatusV2) { RequiresToken = false }); + Rest.Register(new RestCommand("/tokentest", ServerTokenTest)); + Rest.Register(new RestCommand("/status", ServerStatus) { RequiresToken = false }); - Rest.Register(new RestCommand("/v2/users/activelist", UserListV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/users/read", UserInfoV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/users/destroy", UserDestroyV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/users/update", UserUpdateV2) { RequiresToken = true }); + // User Commands + Rest.Register(new RestCommand("/v2/users/activelist", UserActiveListV2)); + Rest.Register(new RestCommand("/v2/users/create", UserCreateV2)); + Rest.Register(new RestCommand("/v2/users/list", UserListV2)); + Rest.Register(new RestCommand("/v2/users/read", UserInfoV2)); + Rest.Register(new RestCommand("/v2/users/destroy", UserDestroyV2)); + Rest.Register(new RestCommand("/v2/users/update", UserUpdateV2)); - Rest.Register(new RestCommand("/bans/create", BanCreate) {RequiresToken = true}); - Rest.Register(new RestCommand("/v2/bans/read", BanInfoV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/bans/destroy", BanDestroyV2) { RequiresToken = true }); + // Ban Commands + Rest.Register(new RestCommand("/bans/create", BanCreate)); + Rest.Register(new RestCommand("/v2/bans/list", BanListV2)); + Rest.Register(new RestCommand("/v2/bans/read", BanInfoV2)); + Rest.Register(new RestCommand("/v2/bans/destroy", BanDestroyV2)); - Rest.Register(new RestCommand("/v2/lists/bans", BanListIPs) { RequiresToken = true }); - Rest.Register(new RestCommand("/lists/players", PlayerList) {RequiresToken = true}); - Rest.Register(new RestCommand("/v2/lists/players", PlayerListV2) { RequiresToken = true }); + // World Commands + Rest.Register(new RestCommand("/world/read", WorldRead)); + Rest.Register(new RestCommand("/world/meteor", WorldMeteor)); + Rest.Register(new RestCommand("/world/bloodmoon/{bool}", WorldBloodmoon)); + Rest.Register(new RestCommand("/v2/world/save", WorldSave)); + Rest.Register(new RestCommand("/v2/world/autosave/state/{bool}", WorldChangeSaveSettings)); + Rest.Register(new RestCommand("/v2/world/butcher", WorldButcher)); - Rest.Register(new RestCommand("/world/read", WorldRead) {RequiresToken = true}); - Rest.Register(new RestCommand("/world/meteor", WorldMeteor) {RequiresToken = true}); - Rest.Register(new RestCommand("/world/bloodmoon/{bool}", WorldBloodmoon) {RequiresToken = true}); - Rest.Register(new RestCommand("/v2/world/save", WorldSave) { RequiresToken = true}); - Rest.Register(new RestCommand("/v2/world/autosave/state/{bool}", ChangeWorldSaveSettings) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/world/butcher", Butcher) {RequiresToken = true}); + // Player Commands + Rest.Register(new RestCommand("/lists/players", PlayerList)); + Rest.Register(new RestCommand("/v2/players/list", PlayerListV2)); + Rest.Register(new RestCommand("/v2/players/read", PlayerReadV2)); + Rest.Register(new RestCommand("/v2/players/kick", PlayerKickV2)); + Rest.Register(new RestCommand("/v2/players/ban", PlayerBanV2)); + Rest.Register(new RestCommand("/v2/players/kill", PlayerKill)); + Rest.Register(new RestCommand("/v2/players/mute", PlayerMute)); + Rest.Register(new RestCommand("/v2/players/unmute", PlayerUnMute)); - Rest.Register(new RestCommand("/v2/players/read", PlayerReadV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/players/kick", PlayerKickV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/players/ban", PlayerBanV2) { RequiresToken = true }); - Rest.Register(new RestCommand("/v2/players/kill", PlayerKill) {RequiresToken = true}); - Rest.Register(new RestCommand("/v2/players/mute", PlayerMute) {RequiresToken = true}); - Rest.Register(new RestCommand("/v2/players/unmute", PlayerUnMute) {RequiresToken = true}); - - Rest.Register(new RestCommand("/v2/server/broadcast", Broadcast) { RequiresToken = true}); - Rest.Register(new RestCommand("/v2/server/off", Off) {RequiresToken = true}); - Rest.Register(new RestCommand("/v2/server/rawcmd", ServerCommand) {RequiresToken = true}); + // Group Commands + Rest.Register(new RestCommand("/v2/groups/list", GroupList)); + Rest.Register(new RestCommand("/v2/groups/read", GroupInfo)); + Rest.Register(new RestCommand("/v2/groups/destroy", GroupDestroy)); + Rest.Register(new RestCommand("/v2/groups/create", GroupCreate)); + Rest.Register(new RestCommand("/v2/groups/update", GroupUpdate)); } #region RestServerMethods private object ServerCommand(RestVerbs verbs, IParameterCollection parameters) { - if (parameters["cmd"] != null && parameters["cmd"].Trim() != "") - { - TSRestPlayer tr = new TSRestPlayer(); - RestObject ro = new RestObject("200"); - Commands.HandleCommand(tr, parameters["cmd"]); - foreach (string s in tr.GetCommandOutput()) - { - ro.Add("response", s); - } - return ro; - } - RestObject fail = new RestObject("400"); - fail["response"] = "Missing or blank cmd parameter."; - return fail; + if (string.IsNullOrWhiteSpace(parameters["cmd"])) + return RestMissingParam("cmd"); + + TSRestPlayer tr = new TSRestPlayer(); + Commands.HandleCommand(tr, parameters["cmd"]); + return RestResponse(string.Join("\n", tr.GetCommandOutput())); } - private object Off(RestVerbs verbs, IParameterCollection parameters) + private object ServerOff(RestVerbs verbs, IParameterCollection parameters) { - bool confirm; - bool.TryParse(parameters["confirm"], out confirm); - bool nosave; - bool.TryParse(parameters["nosave"], out nosave); + if (!GetBool(parameters["confirm"], false)) + return RestInvalidParam("confirm"); - if (confirm == true) + if (!GetBool(parameters["nosave"], false)) + WorldGen.saveWorld(); + Netplay.disconnect = true; + + // Inform players the server is shutting down + var msg = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; + foreach (TSPlayer player in TShock.Players.Where(p => null != p)) { - if (!nosave) - WorldGen.saveWorld(); - Netplay.disconnect = true; - RestObject reply = new RestObject("200"); - reply["response"] = "The server is shutting down."; - return reply; + TShock.Utils.ForceKick(player, msg); } - RestObject fail = new RestObject("400"); - fail["response"] = "Invalid/missing confirm switch, and/or missing nosave switch."; - return fail; + return RestResponse("The server is shutting down"); } - private object Broadcast(RestVerbs verbs, IParameterCollection parameters) + private object ServerBroadcast(RestVerbs verbs, IParameterCollection parameters) { - if (parameters["msg"] != null && parameters["msg"].Trim() != "") - { - TShock.Utils.Broadcast(parameters["msg"]); - RestObject reply = new RestObject("200"); - reply["response"] = "The message was broadcasted successfully."; - return reply; - } - RestObject fail = new RestObject("400"); - fail["response"] = "Broadcast failed."; - return fail; + var msg = parameters["msg"]; + if (string.IsNullOrWhiteSpace(msg)) + return RestMissingParam("msg"); + TShock.Utils.Broadcast(msg); + return RestResponse("The message was broadcasted successfully"); } - #endregion - - #region RestMethods - - private object TokenTest(RestVerbs verbs, IParameterCollection parameters) - { - return new Dictionary - {{"status", "200"}, {"response", "Token is valid and was passed through correctly."}}; - } - - private object Status(RestVerbs verbs, IParameterCollection parameters) + private object ServerStatus(RestVerbs verbs, IParameterCollection parameters) { if (TShock.Config.EnableTokenEndpointAuthentication) - return new RestObject("403") {Error = "Server settings require a token for this API call."}; + return RestError("Server settings require a token for this API call"); - var activeplayers = Main.player.Where(p => p != null && p.active).ToList(); - string currentPlayers = string.Join(", ", activeplayers.Select(p => p.name)); + var activeplayers = Main.player.Where(p => null != p && p.active).ToList(); + return new RestObject() + { + {"name", TShock.Config.ServerNickname}, + {"port", Convert.ToString(TShock.Config.ServerPort)}, + {"playercount", Convert.ToString(activeplayers.Count())}, + {"players", string.Join(", ", activeplayers.Select(p => p.name))}, + }; + } - var ret = new RestObject("200"); - ret["name"] = TShock.Config.ServerNickname; - ret["port"] = Convert.ToString(TShock.Config.ServerPort); - ret["playercount"] = Convert.ToString(activeplayers.Count()); - ret["players"] = currentPlayers; + private object ServerStatusV2(RestVerbs verbs, IParameterCollection parameters) + { + if (TShock.Config.EnableTokenEndpointAuthentication) + return RestError("Server settings require a token for this API call"); - ret["maxplayers"] = TShock.Config.MaxSlots; + var ret = new RestObject() + { + {"name", TShock.Config.ServerNickname}, + {"port", TShock.Config.ServerPort}, + {"playercount", Main.player.Where(p => null != p && p.active).Count()}, + {"maxplayers", TShock.Config.MaxSlots}, + {"world", Main.worldName} + }; + + if (GetBool(parameters["players"], false)) + { + var players = new ArrayList(); + foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p)) + { + var p = PlayerFilter(tsPlayer, parameters); + if (null != p) + players.Add(p); + } + ret.Add("players", players); + } + + if (GetBool(parameters["rules"], false)) + { + var rules = new Dictionary(); + rules.Add("AutoSave", TShock.Config.AutoSave); + rules.Add("DisableBuild", TShock.Config.DisableBuild); + rules.Add("DisableClownBombs", TShock.Config.DisableClownBombs); + rules.Add("DisableDungeonGuardian", TShock.Config.DisableDungeonGuardian); + rules.Add("DisableInvisPvP", TShock.Config.DisableInvisPvP); + rules.Add("DisableSnowBalls", TShock.Config.DisableSnowBalls); + rules.Add("DisableTombstones", TShock.Config.DisableTombstones); + rules.Add("EnableWhitelist", TShock.Config.EnableWhitelist); + rules.Add("HardcoreOnly", TShock.Config.HardcoreOnly); + rules.Add("PvPMode", TShock.Config.PvPMode); + rules.Add("SpawnProtection", TShock.Config.SpawnProtection); + rules.Add("SpawnProtectionRadius", TShock.Config.SpawnProtectionRadius); + + ret.Add("rules", rules); + } return ret; } + private object ServerTokenTest(RestVerbs verbs, IParameterCollection parameters) + { + return RestResponse("Token is valid and was passed through correctly"); + } + #endregion #region RestUserMethods + private object UserActiveListV2(RestVerbs verbs, IParameterCollection parameters) + { + return new RestObject() { { "activeusers", string.Join("\t", TShock.Players.Where(p => null != p && null != p.UserAccountName && p.Active).Select(p => p.UserAccountName)) } }; + } + private object UserListV2(RestVerbs verbs, IParameterCollection parameters) { - string playerlist = ""; - foreach (var TSPlayer in TShock.Players) + return new RestObject() { { "users", TShock.Users.GetUsers().Select(p => new Dictionary(){ + {"name", p.Name}, + {"id", p.ID}, + {"group", p.Group}, + {"ip", p.Address}, + }) } }; + } + + private object UserCreateV2(RestVerbs verbs, IParameterCollection parameters) + { + var username = parameters["user"]; + if (string.IsNullOrWhiteSpace(username)) + return RestMissingParam("user"); + + var group = parameters["group"]; + if (string.IsNullOrWhiteSpace(group)) + return RestMissingParam("group"); + + var password = parameters["password"]; + if (string.IsNullOrWhiteSpace(password)) + return RestMissingParam("password"); + + // NOTE: ip can be blank + User user = new User(parameters["ip"], username, password, group); + try { - if (TSPlayer == null) - { - continue; - } - playerlist += playerlist == "" ? TSPlayer.UserAccountName : "\t" + TSPlayer.UserAccountName; + TShock.Users.AddUser(user); } - var returnBlock = new Dictionary(); - returnBlock.Add("status", "200"); - returnBlock.Add("activeusers", playerlist); - return returnBlock; + catch (Exception e) + { + return RestError(e.Message); + } + + return RestResponse("User was successfully created"); } private object UserUpdateV2(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); + var ret = UserFind(parameters); + if (ret is RestObject) + return ret; + var password = parameters["password"]; var group = parameters["group"]; + if (string.IsNullOrWhiteSpace(group) && string.IsNullOrWhiteSpace(password)) + return RestMissingParam("group", "password"); - if (group == null && password == null) + User user = (User)ret; + var response = new RestObject(); + if (!string.IsNullOrWhiteSpace(password)) { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "No parameters were passed."); - return returnBlock; + try + { + TShock.Users.SetUserPassword(user, password); + response.Add("password-response", "Password updated successfully"); + } + catch (Exception e) + { + return RestError("Failed to update user password (" + e.Message + ")"); + } } - var user = TShock.Users.GetUserByName(parameters["user"]); - if (user == null) + if (!string.IsNullOrWhiteSpace(group)) { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "The specefied user doesn't exist."); - return returnBlock; + try + { + TShock.Users.SetUserGroup(user, group); + response.Add("group-response", "Group updated successfully"); + } + catch (Exception e) + { + return RestError("Failed to update user group (" + e.Message + ")"); + } } - if (password != null) - { - TShock.Users.SetUserPassword(user, password); - returnBlock.Add("password-response", "Password updated successfully."); - } - - if (group != null) - { - TShock.Users.SetUserGroup(user, group); - returnBlock.Add("group-response", "Group updated successfully."); - } - - returnBlock.Add("status", "200"); - return returnBlock; + return response; } private object UserDestroyV2(RestVerbs verbs, IParameterCollection parameters) { - var user = TShock.Users.GetUserByName(parameters["user"]); - if (user == null) - { - return new Dictionary {{"status", "400"}, {"error", "The specified user account does not exist."}}; - } - var returnBlock = new Dictionary(); + var ret = UserFind(parameters); + if (ret is RestObject) + return ret; + try { - TShock.Users.RemoveUser(user); + TShock.Users.RemoveUser((User)ret); } - catch (Exception) + catch (Exception e) { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "The specified user was unable to be removed."); - return returnBlock; + return RestError(e.Message); } - returnBlock.Add("status", "200"); - returnBlock.Add("response", "User deleted successfully."); - return returnBlock; + + return RestResponse("User deleted successfully"); } private object UserInfoV2(RestVerbs verbs, IParameterCollection parameters) { - var user = TShock.Users.GetUserByName(parameters["user"]); - if (user == null) - { - return new Dictionary {{"status", "400"}, {"error", "The specified user account does not exist."}}; - } + var ret = UserFind(parameters); + if (ret is RestObject) + return ret; - var returnBlock = new Dictionary(); - returnBlock.Add("status", "200"); - returnBlock.Add("group", user.Group); - returnBlock.Add("id", user.ID.ToString()); - return returnBlock; + User user = (User)ret; + return new RestObject() { { "group", user.Group }, { "id", user.ID.ToString() } }; } #endregion #region RestBanMethods - private object BanListIPs(RestVerbs verbs, IParameterCollection parameters) - { - RestObject returnItem = new RestObject("200"); - returnItem.Add("bans", TShock.Bans.GetBans()); - - return returnItem; - } - private object BanCreate(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); var ip = parameters["ip"]; var name = parameters["name"]; - var reason = parameters["reason"]; - if (ip == null && name == null) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Required parameters were missing from this API endpoint."); - return returnBlock; - } - - if (ip == null) - { - ip = ""; - } - - if (name == null) - { - name = ""; - } - - if (reason == null) - { - reason = ""; - } + if (string.IsNullOrWhiteSpace(ip) && string.IsNullOrWhiteSpace(name)) + return RestMissingParam("ip", "name"); try { - TShock.Bans.AddBan(ip, name, reason); + TShock.Bans.AddBan(ip, name, parameters["reason"], true); } - catch (Exception) + catch (Exception e) { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "The specified ban was unable to be created."); - return returnBlock; + return RestError(e.Message); } - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Ban created successfully."); - return returnBlock; + return RestResponse("Ban created successfully"); } private object BanDestroyV2(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - - var type = parameters["type"]; - if (type == null) - { - returnBlock.Add("Error", "Invalid Type"); - return returnBlock; - } - - var ban = new Ban(); - if (type == "ip") ban = TShock.Bans.GetBanByIp(parameters["user"]); - else if (type == "name") ban = TShock.Bans.GetBanByName(parameters["user"]); - else - { - returnBlock.Add("Error", "Invalid Type"); - return returnBlock; - } - - if (ban == null) - { - return new Dictionary {{"status", "400"}, {"error", "The specified ban does not exist."}}; - } + var ret = BanFind(parameters); + if (ret is RestObject) + return ret; try { - TShock.Bans.RemoveBan(ban.IP); + Ban ban = (Ban)ret; + switch (parameters["type"]) + { + case "ip": + if (!TShock.Bans.RemoveBan(ban.IP, false, false, true)) + return RestResponse("Failed to delete ban (already deleted?)"); + break; + case "name": + if (!TShock.Bans.RemoveBan(ban.Name, true, GetBool(parameters["caseinsensitive"], true))) + return RestResponse("Failed to delete ban (already deleted?)"); + break; + default: + return RestError("Invalid Type: '" + parameters["type"] + "'"); + } + } - catch (Exception) + catch (Exception e) { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "The specified ban was unable to be removed."); - return returnBlock; + return RestError(e.Message); } - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Ban deleted successfully."); - return returnBlock; + + return RestResponse("Ban deleted successfully"); } private object BanInfoV2(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); + var ret = BanFind(parameters); + if (ret is RestObject) + return ret; - var type = parameters["type"]; - if (type == null) + Ban ban = (Ban)ret; + return new RestObject() { + {"name", null == ban.Name ? "" : ban.Name}, + {"ip", null == ban.IP ? "" : ban.IP}, + {"reason", null == ban.Reason ? "" : ban.Reason}, + }; + } + + private object BanListV2(RestVerbs verbs, IParameterCollection parameters) + { + var banList = new ArrayList(); + foreach (var ban in TShock.Bans.GetBans()) { - returnBlock.Add("Error", "Invalid Type"); - return returnBlock; + banList.Add( + new Dictionary + { + {"name", null == ban.Name ? "" : ban.Name}, + {"ip", null == ban.IP ? "" : ban.IP}, + {"reason", null == ban.Reason ? "" : ban.Reason}, + } + ); } - var ban = new Ban(); - if (type == "ip") ban = TShock.Bans.GetBanByIp(parameters["user"]); - else if (type == "name") ban = TShock.Bans.GetBanByName(parameters["user"]); - else - { - returnBlock.Add("Error", "Invalid Type"); - return returnBlock; - } - - if (ban == null) - { - return new Dictionary { { "status", "400" }, { "error", "The specified ban does not exist." } }; - } - - returnBlock.Add("status", "200"); - returnBlock.Add("name", ban.Name); - returnBlock.Add("ip", ban.IP); - returnBlock.Add("reason", ban.Reason); - return returnBlock; + return new RestObject() { { "bans", banList } }; } #endregion #region RestWorldMethods - private object ChangeWorldSaveSettings(RestVerbs verbs, IParameterCollection parameters) + private object WorldChangeSaveSettings(RestVerbs verbs, IParameterCollection parameters) { - bool state; - bool.TryParse(verbs["state"], out state); + bool autoSave; + if (!bool.TryParse(verbs["bool"], out autoSave)) + return RestInvalidParam("state"); + TShock.Config.AutoSave = autoSave; - if (state == true) - { - TShock.Config.AutoSave = true; - } - else - { - TShock.Config.AutoSave = false; - } - - RestObject rj = new RestObject("200"); - rj["response"] = "Value changed"; - rj["state"] = state; - - return rj; + return RestResponse("AutoSave has been set to " + autoSave); } private object WorldSave(RestVerbs verbs, IParameterCollection parameters) { TShock.Utils.SaveWorld(); - RestObject rj = new RestObject("200"); - rj["response"] = "World saved."; - return rj; + return RestResponse("World saved"); } - private object Butcher(RestVerbs verbs, IParameterCollection parameters) + private object WorldButcher(RestVerbs verbs, IParameterCollection parameters) { bool killFriendly; if (!bool.TryParse(parameters["killfriendly"], out killFriendly)) - { - RestObject fail = new RestObject("400"); - fail["response"] = "The given value for killfriendly wasn't a boolean value."; - return fail; - } + return RestInvalidParam("killfriendly"); + if (killFriendly) - { killFriendly = !killFriendly; - } int killcount = 0; for (int i = 0; i < Main.npc.Length; i++) @@ -436,54 +435,38 @@ namespace TShockAPI } } - RestObject rj = new RestObject("200"); - rj["response"] = killcount + " NPCs have been killed."; - return rj; + return RestResponse(killcount + " NPCs have been killed"); } private object WorldRead(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - returnBlock.Add("status", "200"); - returnBlock.Add("name", Main.worldName); - returnBlock.Add("size", Main.maxTilesX + "*" + Main.maxTilesY); - returnBlock.Add("time", Main.time); - returnBlock.Add("daytime", Main.dayTime); - returnBlock.Add("bloodmoon", Main.bloodMoon); - returnBlock.Add("invasionsize", Main.invasionSize); - return returnBlock; + return new RestObject() + { + {"name", Main.worldName}, + {"size", Main.maxTilesX + "*" + Main.maxTilesY}, + {"time", Main.time}, + {"daytime", Main.dayTime}, + {"bloodmoon", Main.bloodMoon}, + {"invasionsize", Main.invasionSize} + }; } private object WorldMeteor(RestVerbs verbs, IParameterCollection parameters) { - if (WorldGen.genRand == null) + if (null == WorldGen.genRand) WorldGen.genRand = new Random(); WorldGen.dropMeteor(); - var returnBlock = new Dictionary {{"status", "200"}, {"response", "Meteor has been spawned."}}; - return returnBlock; + return RestResponse("Meteor has been spawned"); } private object WorldBloodmoon(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var bloodmoonVerb = verbs["bool"]; bool bloodmoon; - if (bloodmoonVerb == null) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "No parameter was passed."); - return returnBlock; - } - if (!bool.TryParse(bloodmoonVerb, out bloodmoon)) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Unable to parse parameter."); - return returnBlock; - } + if (!bool.TryParse(verbs["bool"], out bloodmoon)) + return RestInvalidParam("bloodmoon"); Main.bloodMoon = bloodmoon; - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Blood Moon has been set to " + bloodmoon); - return returnBlock; + + return RestResponse("Blood Moon has been set to " + bloodmoon); } #endregion @@ -492,190 +475,342 @@ namespace TShockAPI private object PlayerUnMute(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var playerParam = parameters["player"]; - var found = TShock.Utils.FindPlayer(playerParam); - var reason = parameters["reason"]; - if (found.Count == 0) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " was not found"); - } - else if (found.Count > 1) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " matches " + playerParam.Count() + " players"); - } - else if (found.Count == 1) - { - var player = found[0]; - player.mute = false; - player.SendMessage("You have been remotely unmuted."); - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Player " + player.Name + " was muted."); - } - return returnBlock; + return PlayerSetMute(parameters, false); } private object PlayerMute(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var playerParam = parameters["player"]; - var found = TShock.Utils.FindPlayer(playerParam); - var reason = parameters["reason"]; - if (found.Count == 0) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " was not found"); - } - else if (found.Count > 1) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " matches " + playerParam.Count() + " players"); - } - else if (found.Count == 1) - { - var player = found[0]; - player.mute = true; - player.SendMessage("You have been remotely muted."); - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Player " + player.Name + " was muted."); - } - return returnBlock; - } - - private object PlayerListV2(RestVerbs verbs, IParameterCollection parameters) - { - RestObject returnBlock = new RestObject("200"); - returnBlock.Add("players", TShock.Players.Where(p => p != null && p.Active)); - return returnBlock; + return PlayerSetMute(parameters, true); } private object PlayerList(RestVerbs verbs, IParameterCollection parameters) { - var activeplayers = Main.player.Where(p => p != null && p.active).ToList(); - string currentPlayers = string.Join(", ", activeplayers.Select(p => p.name)); - var ret = new RestObject("200"); - ret["players"] = currentPlayers; - ret.Add("deprecated", "This endpoint is deprecated and has been replaced with /v2/lists/players."); - return ret; + var activeplayers = Main.player.Where(p => null != p && p.active).ToList(); + return new RestObject() { { "players", string.Join(", ", activeplayers.Select(p => p.name)) } }; + } + + private object PlayerListV2(RestVerbs verbs, IParameterCollection parameters) + { + var playerList = new ArrayList(); + foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p)) + { + var p = PlayerFilter(tsPlayer, parameters); + if (null != p) + playerList.Add(p); + } + return new RestObject() { { "players", playerList } }; } private object PlayerReadV2(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var playerParam = parameters["player"]; - var found = TShock.Utils.FindPlayer(playerParam); - if (found.Count == 0) + var ret = PlayerFind(parameters); + if (ret is RestObject) + return ret; + + TSPlayer player = (TSPlayer)ret; + var activeItems = player.TPlayer.inventory.Where(p => p.active).ToList(); + return new RestObject() { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " was not found"); - } - else if (found.Count > 1) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " matches " + playerParam.Count() + " players"); - } - else if (found.Count == 1) - { - var player = found[0]; - returnBlock.Add("status", "200"); - returnBlock.Add("nickname", player.Name); - returnBlock.Add("username", player.UserAccountName == null ? "" : player.UserAccountName); - returnBlock.Add("ip", player.IP); - returnBlock.Add("group", player.Group.Name); - returnBlock.Add("position", player.TileX + "," + player.TileY); - var activeItems = player.TPlayer.inventory.Where(p => p.active).ToList(); - returnBlock.Add("inventory", string.Join(", ", activeItems.Select(p => (p.name + ":" + p.stack)))); - returnBlock.Add("buffs", string.Join(", ", player.TPlayer.buffType)); - } - return returnBlock; + {"nickname", player.Name}, + {"username", null == player.UserAccountName ? "" : player.UserAccountName}, + {"ip", player.IP}, + {"group", player.Group.Name}, + {"position", player.TileX + "," + player.TileY}, + {"inventory", string.Join(", ", activeItems.Select(p => (p.name + ":" + p.stack)))}, + {"buffs", string.Join(", ", player.TPlayer.buffType)} + }; } private object PlayerKickV2(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var playerParam = parameters["player"]; - if (playerParam == null) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Unspecified name."); - return returnBlock; - } - var found = TShock.Utils.FindPlayer(playerParam); - var reason = parameters["reason"]; - if (found.Count == 0) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " was not found"); - } - else if (found.Count > 1) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " matches " + playerParam.Count() + " players"); - } - else if (found.Count == 1) - { - var player = found[0]; - TShock.Utils.ForceKick(player, reason == null ? "Kicked via web" : reason); - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Player " + player.Name + " was kicked"); - } - return returnBlock; + var ret = PlayerFind(parameters); + if (ret is RestObject) + return ret; + + TSPlayer player = (TSPlayer)ret; + TShock.Utils.ForceKick(player, null == parameters["reason"] ? "Kicked via web" : parameters["reason"]); + return RestResponse("Player " + player.Name + " was kicked"); } private object PlayerBanV2(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var playerParam = parameters["player"]; - var found = TShock.Utils.FindPlayer(playerParam); - var reason = parameters["reason"]; - if (found.Count == 0) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " was not found"); - } - else if (found.Count > 1) - { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " matches " + playerParam.Count() + " players"); - } - else if (found.Count == 1) - { - var player = found[0]; - TShock.Bans.AddBan(player.IP, player.Name, reason == null ? "Banned via web" : reason); - TShock.Utils.ForceKick(player, reason == null ? "Banned via web" : reason); - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Player " + player.Name + " was banned"); - } - return returnBlock; + var ret = PlayerFind(parameters); + if (ret is RestObject) + return ret; + + TSPlayer player = (TSPlayer)ret; + var reason = null == parameters["reason"] ? "Banned via web" : parameters["reason"]; + TShock.Bans.AddBan(player.IP, player.Name, reason); + TShock.Utils.ForceKick(player, reason); + return RestResponse("Player " + player.Name + " was banned"); } private object PlayerKill(RestVerbs verbs, IParameterCollection parameters) { - var returnBlock = new Dictionary(); - var playerParam = parameters["player"]; - var found = TShock.Utils.FindPlayer(playerParam); - var from = verbs["from"]; - if (found.Count == 0) + var ret = PlayerFind(parameters); + if (ret is RestObject) + return ret; + + TSPlayer player = (TSPlayer)ret; + player.DamagePlayer(999999); + var from = string.IsNullOrWhiteSpace(parameters["from"]) ? "Server Admin" : parameters["from"]; + player.SendMessage(string.Format("{0} just killed you!", from)); + return RestResponse("Player " + player.Name + " was killed"); + } + + #endregion + + #region RestGroupMethods + + private object GroupList(RestVerbs verbs, IParameterCollection parameters) + { + var groups = new ArrayList(); + foreach (Group group in TShock.Groups) { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " was not found"); + groups.Add(new Dictionary {{"name", group.Name}, {"parent", group.ParentName}, {"chatcolor", group.ChatColor}}); } - else if (found.Count > 1) + return new RestObject() { { "groups", groups } }; + } + + private object GroupInfo(RestVerbs verbs, IParameterCollection parameters) + { + var ret = GroupFind(parameters); + if (ret is RestObject) + return ret; + + Group group = (Group)ret; + return new RestObject() { + {"name", group.Name}, + {"parent", group.ParentName}, + {"chatcolor", group.ChatColor}, + {"permissions", group.permissions}, + {"negatedpermissions", group.negatedpermissions} + }; + } + + private object GroupDestroy(RestVerbs verbs, IParameterCollection parameters) + { + var ret = GroupFind(parameters); + if (ret is RestObject) + return ret; + + Group group = (Group)ret; + try { - returnBlock.Add("status", "400"); - returnBlock.Add("error", "Name " + playerParam + " matches " + playerParam.Count() + " players"); + TShock.Groups.DeleteGroup(group.Name, true); } - else if (found.Count == 1) + catch (Exception e) { - var player = found[0]; - player.DamagePlayer(999999); - player.SendMessage(string.Format("{0} just killed you!", from)); - returnBlock.Add("status", "200"); - returnBlock.Add("response", "Player " + player.Name + " was killed."); + return RestError(e.Message); } - return returnBlock; + + return RestResponse("Group '" + group.Name + "' deleted successfully"); + } + + private object GroupCreate(RestVerbs verbs, IParameterCollection parameters) + { + var name = parameters["group"]; + if (string.IsNullOrWhiteSpace(name)) + return RestMissingParam("group"); + try + { + TShock.Groups.AddGroup(name, parameters["parent"], parameters["permissions"], parameters["chatcolor"], true); + } + catch (Exception e) + { + return RestError(e.Message); + } + + return RestResponse("Group '" + name + "' created successfully"); + } + + private object GroupUpdate(RestVerbs verbs, IParameterCollection parameters) + { + var ret = GroupFind(parameters); + if (ret is RestObject) + return ret; + + Group group = (Group)ret; + var parent = (null == parameters["parent"]) ? group.ParentName : parameters["parent"]; + var chatcolor = (null == parameters["chatcolor"]) ? group.ChatColor : parameters["chatcolor"]; + var permissions = (null == parameters["permissions"]) ? group.Permissions : parameters["permissions"]; + try + { + TShock.Groups.UpdateGroup(group.Name, parent, permissions, chatcolor); + } + catch (Exception e) + { + return RestError(e.Message); + } + + return RestResponse("Group '" + group.Name + "' updated successfully"); + } + + #endregion + + #region Utility Methods + + private RestObject RestError(string message, string status = "400") + { + return new RestObject(status) {Error = message}; + } + + private RestObject RestResponse(string message, string status = "200") + { + return new RestObject(status) {Response = message}; + } + + private RestObject RestMissingParam(string var) + { + return RestError("Missing or empty " + var + " parameter"); + } + + private RestObject RestMissingParam(params string[] vars) + { + return RestMissingParam(string.Join(", ", vars)); + } + + private RestObject RestInvalidParam(string var) + { + return RestError("Missing or invalid " + var + " parameter"); + } + + private bool GetBool(string val, bool def) + { + bool ret; + return bool.TryParse(val, out ret) ? ret : def; + } + + private object PlayerFind(IParameterCollection parameters) + { + string name = parameters["player"]; + if (string.IsNullOrWhiteSpace(name)) + return RestMissingParam("player"); + + var found = TShock.Utils.FindPlayer(name); + switch(found.Count) + { + case 1: + return found[0]; + case 0: + return RestError("Player " + name + " was not found"); + default: + return RestError("Player " + name + " matches " + found.Count + " players"); + } + } + + private object UserFind(IParameterCollection parameters) + { + string name = parameters["user"]; + if (string.IsNullOrWhiteSpace(name)) + return RestMissingParam("user"); + + User user; + string type = parameters["type"]; + try + { + switch (type) + { + case null: + case "name": + type = "name"; + user = TShock.Users.GetUserByName(name); + break; + case "id": + user = TShock.Users.GetUserByID(Convert.ToInt32(name)); + break; + case "ip": + user = TShock.Users.GetUserByIP(name); + + break; + default: + return RestError("Invalid Type: '" + type + "'"); + } + } + catch (Exception e) + { + return RestError(e.Message); + } + + if (null == user) + return RestError(String.Format("User {0} '{1}' doesn't exist", type, name)); + + return user; + } + + private object BanFind(IParameterCollection parameters) + { + string name = parameters["ban"]; + if (string.IsNullOrWhiteSpace(name)) + return RestMissingParam("ban"); + + string type = parameters["type"]; + if (string.IsNullOrWhiteSpace(type)) + return RestMissingParam("type"); + + Ban ban; + switch (type) + { + case "ip": + ban = TShock.Bans.GetBanByIp(name); + break; + case "name": + ban = TShock.Bans.GetBanByName(name, GetBool(parameters["caseinsensitive"], true)); + break; + default: + return RestError("Invalid Type: '" + type + "'"); + } + + if (null == ban) + return RestError("Ban " + type + " '" + name + "' doesn't exist"); + + return ban; + } + + private object GroupFind(IParameterCollection parameters) + { + var name = parameters["group"]; + if (string.IsNullOrWhiteSpace(name)) + return RestMissingParam("group"); + + var group = TShock.Groups.GetGroupByName(name); + if (null == group) + return RestError("Group '" + name + "' doesn't exist"); + + return group; + } + + private Dictionary PlayerFilter(TSPlayer tsPlayer, IParameterCollection parameters) + { + var player = new Dictionary + { + {"nickname", tsPlayer.Name}, + {"username", null == tsPlayer.UserAccountName ? "" : tsPlayer.UserAccountName}, + {"ip", tsPlayer.IP}, + {"group", tsPlayer.Group.Name}, + {"active", tsPlayer.Active}, + {"state", tsPlayer.State}, + {"team", tsPlayer.Team}, + }; + foreach (IParameter filter in parameters) + { + if (player.ContainsKey(filter.Name) && !player[filter.Name].Equals(filter.Value)) + return null; + } + return player; + } + + private object PlayerSetMute(IParameterCollection parameters, bool mute) + { + var ret = PlayerFind(parameters); + if (ret is RestObject) + return ret; + + TSPlayer player = (TSPlayer)ret; + player.mute = mute; + var verb = mute ? "muted" : "unmuted"; + player.SendMessage("You have been remotely " + verb); + return RestResponse("Player " + player.Name + " was " + verb); } #endregion From a1d3132138f277d4ff850099c2d504acb6b5cde0 Mon Sep 17 00:00:00 2001 From: stevenh Date: Thu, 16 Feb 2012 12:33:10 +0000 Subject: [PATCH 4/4] Added full test suite for RestAPI --- TShock.sln | 12 + .../Properties/AssemblyInfo.cs | 36 + TShockRestTestPlugin/TShockRestTestPlugin.cs | 192 ++ .../TShockRestTestPlugin.csproj | 62 + UnitTests/RestApiTests.webtest | 1542 +++++++++++++++++ UnitTests/UnitTests.csproj | 8 + 6 files changed, 1852 insertions(+) create mode 100644 TShockRestTestPlugin/Properties/AssemblyInfo.cs create mode 100644 TShockRestTestPlugin/TShockRestTestPlugin.cs create mode 100644 TShockRestTestPlugin/TShockRestTestPlugin.csproj create mode 100644 UnitTests/RestApiTests.webtest diff --git a/TShock.sln b/TShock.sln index 85d6a0f9..db301cae 100644 --- a/TShock.sln +++ b/TShock.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Terraria.vsmdi = Terraria.vsmdi EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TShockRestTestPlugin", "TShockRestTestPlugin\TShockRestTestPlugin.csproj", "{F2FEDAFB-58DE-4611-9168-A86112C346C7}" +EndProject Global GlobalSection(TestCaseManagementSettings) = postSolution CategoryFile = Terraria.vsmdi @@ -52,6 +54,16 @@ Global {F3742F51-D7BF-4754-A68A-CD944D2A21FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3742F51-D7BF-4754-A68A-CD944D2A21FF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F3742F51-D7BF-4754-A68A-CD944D2A21FF}.Release|x86.ActiveCfg = Release|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Release|Any CPU.Build.0 = Release|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F2FEDAFB-58DE-4611-9168-A86112C346C7}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TShockRestTestPlugin/Properties/AssemblyInfo.cs b/TShockRestTestPlugin/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..17cb9a46 --- /dev/null +++ b/TShockRestTestPlugin/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ClassLibrary1")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Multiplay")] +[assembly: AssemblyProduct("ClassLibrary1")] +[assembly: AssemblyCopyright("Copyright © Multiplay 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c6aed7ee-6282-49a2-8177-b79cad20d6d3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TShockRestTestPlugin/TShockRestTestPlugin.cs b/TShockRestTestPlugin/TShockRestTestPlugin.cs new file mode 100644 index 00000000..403b6f8f --- /dev/null +++ b/TShockRestTestPlugin/TShockRestTestPlugin.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using System.Web; +using System.Web.Script.Serialization; +using System.Text.RegularExpressions; +using Microsoft.VisualStudio.TestTools.WebTesting; +using Microsoft.VisualStudio.TestTools.WebTesting.Rules; +using Rests; + +namespace TshockRestTestPlugin +{ + [DisplayName("JSON Status")] + [Description("Checks to see the that the JSON response has the specified status response")] + public class JsonValidateStatus : JsonValidate + { + public override void Validate(object sender, ValidationEventArgs e) + { + if (null != ValidateJson(sender, e)) + e.IsValid = true; + } + } + + [DisplayName("JSON Regexp Property")] + [Description("Checks to see the that the JSON response contains the specified property and is matches the specified regexp")] + public class JsonValidateRegexpProperty : JsonValidateProperty + { + // The name of the desired JSON property + [DisplayName("Regexp")] + [DefaultValue(true)] + public new bool UseRegularExpression { get { return base.UseRegularExpression; } set { base.UseRegularExpression = value; } } + } + + [DisplayName("JSON Error")] + [Description("Checks to see the that the JSON response contains the specified error")] + public class JsonValidateError : JsonValidateProperty + { + // The status of the JSON request + [DisplayName("JSON Status")] + [DefaultValue("400")] + public new string JSonStatus { get { return base.JSonStatus; } set { base.JSonStatus = value; } } + + // The name of the desired JSON property + [DisplayName("Property")] + [DefaultValue("error")] + public new string PropertyName { get { return base.PropertyName; } set { base.PropertyName = value; } } + } + + [DisplayName("JSON Missing Parameter")] + [Description("Checks to see the that the JSON response indicates a missing or invalid parameter")] + public class JsonValidateMissingParameter : JsonValidateError + { + // The value of the desired JSON property + [DisplayName("Missing Value")] + public new string PropertyValue { get { return base.PropertyValue; } set { base.PropertyValue = String.Format("Missing or empty {0} parameter", value); } } + } + + [DisplayName("JSON Invalid Parameter")] + [Description("Checks to see the that the JSON response indicates a missing or invalid parameter")] + public class JsonValidateInvalidParameter : JsonValidateError + { + // The value of the desired JSON property + [DisplayName("Invalid Value")] + public new string PropertyValue { get { return base.PropertyValue; } set { base.PropertyValue = String.Format("Missing or invalid {0} parameter", value); } } + } + + [DisplayName("JSON Response")] + [Description("Checks to see the that the JSON response contains the specified message")] + public class JsonValidateResponse : JsonValidateProperty + { + // The name of the desired JSON property + [DisplayName("Response")] + [DefaultValue("response")] + public new string PropertyName { get { return base.PropertyName; } set { base.PropertyName = value; } } + } + + [DisplayName("JSON Property")] + [Description("Checks to see the that the JSON response contains the specified property and is set to the specified value")] + public class JsonValidateProperty : JsonValidate + { + // The name of the desired JSON property + [DisplayName("Property")] + public string PropertyName { get; set; } + + // The value of the desired JSON property + [DisplayName("Value")] + public string PropertyValue { get; set; } + + // Is the value a regexp of the desired JSON property + [DisplayName("Regexp")] + [DefaultValue(false)] + public bool UseRegularExpression { get; set; } + + public override void Validate(object sender, ValidationEventArgs e) + { + RestObject response = ValidateJson(sender, e); + if (null == response) + return; + + if (null == response[PropertyName]) + { + e.Message = String.Format("{0} Not Found", PropertyName); + e.IsValid = false; + return; + } + + if (UseRegularExpression) + { + var re = new Regex(PropertyValue); + if (!re.IsMatch((string)response[PropertyName])) + { + e.Message = String.Format("{0} => '{1}' !~ '{2}'", PropertyName, response[PropertyName], PropertyValue); + e.IsValid = false; + return; + } + } + else + { + if (PropertyValue != (string)response[PropertyName]) + { + e.Message = String.Format("{0} => '{1}' != '{2}'", PropertyName, response[PropertyName], PropertyValue); + e.IsValid = false; + return; + } + } + + e.IsValid = true; + //e.WebTest.Context.Add(ContextParameterName, propertyValue); + } + } + + [DisplayName("JSON Has Properties")] + [Description("Checks to see the that the JSON response contains the specified properties (comma seperated)")] + public class JsonHasProperties : JsonValidate + { + // The name of the desired JSON properties to check + [DisplayName("Properties")] + [Description("A comma seperated list of property names to check exist")] + public string PropertyNames { get; set; } + + //--------------------------------------------------------------------- + public override void Validate(object sender, ValidationEventArgs e) + { + RestObject response = ValidateJson(sender, e); + if (null == response) + return; + foreach (var p in PropertyNames.Split(',')) + { + if (null == response[p]) + { + e.Message = String.Format("'{0}' Not Found", p); + e.IsValid = false; + return; + } + } + e.IsValid = true; + + //e.WebTest.Context.Add(ContextParameterName, propertyValue); + } + } + + public abstract class JsonValidate : ValidationRule + { + // The status of the JSON request + [DisplayName("JSON Status")] + [DefaultValue("200")] + public string JSonStatus { get; set; } + + public RestObject ValidateJson(object sender, ValidationEventArgs e) + { + if (string.IsNullOrWhiteSpace(e.Response.BodyString)) + { + e.IsValid = false; + e.Message = String.Format("Empty or null response {0}", e.Response.StatusCode); + return null; + } + JavaScriptSerializer serialiser = new JavaScriptSerializer(); + //dynamic data = serialiser.Deserialize(e.Response.BodyString); + RestObject response = serialiser.Deserialize(e.Response.BodyString); + + if (JSonStatus != response.Status) + { + e.IsValid = false; + e.Message = String.Format("Response Status '{0}' not '{1}'", response.Status, JSonStatus); + return null; + } + + return response; + } + } +} \ No newline at end of file diff --git a/TShockRestTestPlugin/TShockRestTestPlugin.csproj b/TShockRestTestPlugin/TShockRestTestPlugin.csproj new file mode 100644 index 00000000..bf2dd997 --- /dev/null +++ b/TShockRestTestPlugin/TShockRestTestPlugin.csproj @@ -0,0 +1,62 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {F2FEDAFB-58DE-4611-9168-A86112C346C7} + Library + Properties + TshockRestTestPlugin + TshockRestTestPlugin + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + {49606449-072B-4CF5-8088-AA49DA586694} + TShockAPI + + + + + \ No newline at end of file diff --git a/UnitTests/RestApiTests.webtest b/UnitTests/RestApiTests.webtest new file mode 100644 index 00000000..84057748 --- /dev/null +++ b/UnitTests/RestApiTests.webtest @@ -0,0 +1,1542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index d9b9c7f9..923856ac 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -49,6 +49,7 @@ + ..\SqlBins\Mono.Data.Sqlite.dll @@ -87,6 +88,10 @@ {49606449-072B-4CF5-8088-AA49DA586694} TShockAPI + + {F2FEDAFB-58DE-4611-9168-A86112C346C7} + TShockRestTestPlugin + @@ -102,6 +107,9 @@ Always + + Always +