diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index dcba3a99b..94142f9c0 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Service", "exa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.HeadersTest", "examples\WireMock.Net.Console.HeadersTest\WireMock.Net.Console.HeadersTest.csproj", "{B70278E7-A2C6-4A3B-BBA9-1C873CA6F03C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.NETCoreApp2", "examples\WireMock.Net.Console.NETCoreApp2\WireMock.Net.Console.NETCoreApp2.csproj", "{83645809-9E01-4E81-8733-BA9497554ABF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,6 +122,10 @@ Global {B70278E7-A2C6-4A3B-BBA9-1C873CA6F03C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B70278E7-A2C6-4A3B-BBA9-1C873CA6F03C}.Release|Any CPU.ActiveCfg = Release|Any CPU {B70278E7-A2C6-4A3B-BBA9-1C873CA6F03C}.Release|Any CPU.Build.0 = Release|Any CPU + {83645809-9E01-4E81-8733-BA9497554ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83645809-9E01-4E81-8733-BA9497554ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83645809-9E01-4E81-8733-BA9497554ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83645809-9E01-4E81-8733-BA9497554ABF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -140,6 +146,7 @@ Global {3C279524-DB73-4DE3-BEF1-F2B2958C9F65} = {F0C22C47-DF71-463C-9B04-B4E0F3B8708A} {7F0B2446-0363-4720-AF46-F47F83B557DC} = {F0C22C47-DF71-463C-9B04-B4E0F3B8708A} {B70278E7-A2C6-4A3B-BBA9-1C873CA6F03C} = {F0C22C47-DF71-463C-9B04-B4E0F3B8708A} + {83645809-9E01-4E81-8733-BA9497554ABF} = {F0C22C47-DF71-463C-9B04-B4E0F3B8708A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BF428BCC-C837-433B-87D2-15C7014B73E9} diff --git a/examples/WireMock.Net.Console.NETCoreApp/WireMock.Net.Console.NETCoreApp.csproj b/examples/WireMock.Net.Console.NETCoreApp/WireMock.Net.Console.NETCoreApp.csproj index bbe783117..dc45d93ad 100644 --- a/examples/WireMock.Net.Console.NETCoreApp/WireMock.Net.Console.NETCoreApp.csproj +++ b/examples/WireMock.Net.Console.NETCoreApp/WireMock.Net.Console.NETCoreApp.csproj @@ -7,6 +7,7 @@ + diff --git a/examples/WireMock.Net.Console.NETCoreApp2/WireMock.Net.Console.NETCoreApp2.csproj b/examples/WireMock.Net.Console.NETCoreApp2/WireMock.Net.Console.NETCoreApp2.csproj new file mode 100644 index 000000000..b3569ec46 --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp2/WireMock.Net.Console.NETCoreApp2.csproj @@ -0,0 +1,48 @@ + + + + Exe + netcoreapp2.1 + ../../WireMock.Net-Logo.ico + WireMock.Net.Console.NETCoreApp.Program + + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Never + + + + \ No newline at end of file diff --git a/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/11111110-a633-40e8-a244-5cb80bc0ab66.json b/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/11111110-a633-40e8-a244-5cb80bc0ab66.json new file mode 100644 index 000000000..9c761369d --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/11111110-a633-40e8-a244-5cb80bc0ab66.json @@ -0,0 +1,22 @@ +{ + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "/static/mapping" + } + ] + }, + "Methods": [ + "get" + ] + }, + "Response": { + "BodyAsJson": { "body": "static mapping" }, + "Headers": { + "Content-Type": "application/json", + "Test-X": [ "test 1", "test 2" ] + } + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json b/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json new file mode 100644 index 000000000..d7a6cbdd7 --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json @@ -0,0 +1,29 @@ +{ + "Guid": "791a3f31-6946-4ce7-8e6f-0237c7443275", + "Title": "", + "Priority": 0, + "Request": { + "Path": "/proxy-google-test-post", + "Methods": [ + "post" + ], + "Body": {} + }, + "Response": { + "StatusCode": 404, + "Body": "\n\n \n \n Error 404 (Not Found)!!1\n \n \n

404. That’s an error.\n

The requested URL /proxy-google-test-post was not found on this server. That’s all we know.\n", + "BodyAsBytes": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CiAgPG1ldGEgY2hhcnNldD11dGYtOD4KICA8bWV0YSBuYW1lPXZpZXdwb3J0IGNvbnRlbnQ9ImluaXRpYWwtc2NhbGU9MSwgbWluaW11bS1zY2FsZT0xLCB3aWR0aD1kZXZpY2Utd2lkdGgiPgogIDx0aXRsZT5FcnJvciA0MDQgKE5vdCBGb3VuZCkhITE8L3RpdGxlPgogIDxzdHlsZT4KICAgICp7bWFyZ2luOjA7cGFkZGluZzowfWh0bWwsY29kZXtmb250OjE1cHgvMjJweCBhcmlhbCxzYW5zLXNlcmlmfWh0bWx7YmFja2dyb3VuZDojZmZmO2NvbG9yOiMyMjI7cGFkZGluZzoxNXB4fWJvZHl7bWFyZ2luOjclIGF1dG8gMDttYXgtd2lkdGg6MzkwcHg7bWluLWhlaWdodDoxODBweDtwYWRkaW5nOjMwcHggMCAxNXB4fSogPiBib2R5e2JhY2tncm91bmQ6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2Vycm9ycy9yb2JvdC5wbmcpIDEwMCUgNXB4IG5vLXJlcGVhdDtwYWRkaW5nLXJpZ2h0OjIwNXB4fXB7bWFyZ2luOjExcHggMCAyMnB4O292ZXJmbG93OmhpZGRlbn1pbnN7Y29sb3I6Izc3Nzt0ZXh0LWRlY29yYXRpb246bm9uZX1hIGltZ3tib3JkZXI6MH1AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOjc3MnB4KXtib2R5e2JhY2tncm91bmQ6bm9uZTttYXJnaW4tdG9wOjA7bWF4LXdpZHRoOm5vbmU7cGFkZGluZy1yaWdodDowfX0jbG9nb3tiYWNrZ3JvdW5kOnVybCgvL3d3dy5nb29nbGUuY29tL2ltYWdlcy9icmFuZGluZy9nb29nbGVsb2dvLzF4L2dvb2dsZWxvZ29fY29sb3JfMTUweDU0ZHAucG5nKSBuby1yZXBlYXQ7bWFyZ2luLWxlZnQ6LTVweH1AbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4tcmVzb2x1dGlvbjoxOTJkcGkpeyNsb2dve2JhY2tncm91bmQ6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2JyYW5kaW5nL2dvb2dsZWxvZ28vMngvZ29vZ2xlbG9nb19jb2xvcl8xNTB4NTRkcC5wbmcpIG5vLXJlcGVhdCAwJSAwJS8xMDAlIDEwMCU7LW1vei1ib3JkZXItaW1hZ2U6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2JyYW5kaW5nL2dvb2dsZWxvZ28vMngvZ29vZ2xlbG9nb19jb2xvcl8xNTB4NTRkcC5wbmcpIDB9fUBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKC13ZWJraXQtbWluLWRldmljZS1waXhlbC1yYXRpbzoyKXsjbG9nb3tiYWNrZ3JvdW5kOnVybCgvL3d3dy5nb29nbGUuY29tL2ltYWdlcy9icmFuZGluZy9nb29nbGVsb2dvLzJ4L2dvb2dsZWxvZ29fY29sb3JfMTUweDU0ZHAucG5nKSBuby1yZXBlYXQ7LXdlYmtpdC1iYWNrZ3JvdW5kLXNpemU6MTAwJSAxMDAlfX0jbG9nb3tkaXNwbGF5OmlubGluZS1ibG9jaztoZWlnaHQ6NTRweDt3aWR0aDoxNTBweH0KICA8L3N0eWxlPgogIDxhIGhyZWY9Ly93d3cuZ29vZ2xlLmNvbS8+PHNwYW4gaWQ9bG9nbyBhcmlhLWxhYmVsPUdvb2dsZT48L3NwYW4+PC9hPgogIDxwPjxiPjQwNC48L2I+IDxpbnM+VGhhdOKAmXMgYW4gZXJyb3IuPC9pbnM+CiAgPHA+VGhlIHJlcXVlc3RlZCBVUkwgPGNvZGU+L3Byb3h5LWdvb2dsZS10ZXN0LXBvc3Q8L2NvZGU+IHdhcyBub3QgZm91bmQgb24gdGhpcyBzZXJ2ZXIuICA8aW5zPlRoYXTigJlzIGFsbCB3ZSBrbm93LjwvaW5zPgo=", + "BodyEncoding": { + "CodePage": 65001, + "EncodingName": "Unicode (UTF-8)", + "WebName": "utf-8" + }, + "UseTransformer": false, + "Headers": { + "Date": "Wed, 27 Oct 2017 18:57:40 GMT", + "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"39,38,37,35\"", + "Referrer-Policy": "no-referrer", + "Connection": "close" + } + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json b/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json new file mode 100644 index 000000000..dd5018000 --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp2/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json @@ -0,0 +1,19 @@ +{ + "Guid": "873d495f-940e-4b86-a1f4-4f0fc7be8b8b", + "Priority": 4, + "Request": { + "Path": {}, + "Methods": [ + "get" + ] + }, + "Response": { + "StatusCode": 200, + "BodyDestination": "SameAsSource", + "Body": "NO PATH OR URL", + "UseTransformer": false, + "Headers": { + "Content-Type": "application/json" + } + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.ConsoleApplication/CustomFileSystemFileHandler.cs b/examples/WireMock.Net.ConsoleApplication/CustomFileSystemFileHandler.cs new file mode 100644 index 000000000..c60ebecef --- /dev/null +++ b/examples/WireMock.Net.ConsoleApplication/CustomFileSystemFileHandler.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.IO; +using WireMock.Handlers; + +namespace WireMock.Net.ConsoleApplication +{ + internal class CustomFileSystemFileHandler : IFileSystemHandler + { + private static readonly string AdminMappingsFolder = Path.Combine("__admin", "mappings"); + + /// + public bool FolderExists(string path) + { + return Directory.Exists(path); + } + + /// + public void CreateFolder(string path) + { + Directory.CreateDirectory(path); + } + + /// + public IEnumerable EnumerateFiles(string path) + { + return Directory.EnumerateFiles(path); + } + + /// + public string GetMappingFolder() + { + return Path.Combine(@"c:\temp-wiremock", AdminMappingsFolder); + } + + /// + public string ReadMappingFile(string path) + { + return File.ReadAllText(path); + } + + /// + public void WriteMappingFile(string path, string text) + { + File.WriteAllText(path, text); + } + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.ConsoleApplication/MainApp.cs b/examples/WireMock.Net.ConsoleApplication/MainApp.cs index 965b058b7..09f12274a 100644 --- a/examples/WireMock.Net.ConsoleApplication/MainApp.cs +++ b/examples/WireMock.Net.ConsoleApplication/MainApp.cs @@ -30,7 +30,9 @@ public static void Run() //}, PreWireMockMiddlewareInit = app => { System.Console.WriteLine($"PreWireMockMiddlewareInit : {app.GetType()}"); }, PostWireMockMiddlewareInit = app => { System.Console.WriteLine($"PostWireMockMiddlewareInit : {app.GetType()}"); }, - Logger = new WireMockConsoleLogger() + Logger = new WireMockConsoleLogger(), + + FileSystemHandler = new CustomFileSystemFileHandler() }); System.Console.WriteLine("FluentMockServer listening at {0}", string.Join(",", server.Urls)); diff --git a/examples/WireMock.Net.ConsoleApplication/WireMock.Net.Console.NET452.csproj b/examples/WireMock.Net.ConsoleApplication/WireMock.Net.Console.NET452.csproj index 86f6fd3ac..4d689ec76 100644 --- a/examples/WireMock.Net.ConsoleApplication/WireMock.Net.Console.NET452.csproj +++ b/examples/WireMock.Net.ConsoleApplication/WireMock.Net.Console.NET452.csproj @@ -54,6 +54,7 @@ + diff --git a/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj b/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj index 09dd215bc..ae57d4d18 100644 --- a/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj +++ b/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj @@ -3,7 +3,7 @@ Lightweight StandAlone Http Mocking Server for .Net. WireMock.Net.StandAlone - 1.0.4.9 + 1.0.4.10 Stef Heyenrath net452;net46;netstandard1.3;netstandard2.0 true diff --git a/src/WireMock.Net/Handlers/IFileSystemHandler.cs b/src/WireMock.Net/Handlers/IFileSystemHandler.cs new file mode 100644 index 000000000..ad6105c68 --- /dev/null +++ b/src/WireMock.Net/Handlers/IFileSystemHandler.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace WireMock.Handlers +{ + ///

+ /// Handler to interact with the file system to handle folders and read and write static mapping files. + /// + public interface IFileSystemHandler + { + /// + /// Gets the folder where the static mappings are located. For local file system, this would be `{CurrentFolder}/__admin/mappings`. + /// + /// The foldername. + string GetMappingFolder(); + + /// + /// Determines whether the given path refers to an existing directory on disk. + /// + /// The path. + /// true if path refers to an existing directory; false if the directory does not exist or an error occurs when trying to determine if the specified directory exists. + bool FolderExists(string path); + + /// + /// Creates all directories and subdirectories in the specified path unless they already exist. + /// + /// The path. + void CreateFolder(string path); + + /// + /// Returns an enumerable collection of file names in a specified path. + /// + /// The path. + /// An enumerable collection of the full names (including paths) for the files in the directory specified by path. + IEnumerable EnumerateFiles(string path); + + /// + /// Read a static mapping file as text. + /// + /// The path (folder + filename with .json extension). + string ReadMappingFile(string path); + + /// + /// Write the static mapping. + /// + /// The path (folder + filename with .json extension). + /// The text. + void WriteMappingFile(string path, string text); + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs b/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs new file mode 100644 index 000000000..89ee17c13 --- /dev/null +++ b/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; + +namespace WireMock.Handlers +{ + /// + /// Default implementation for a handler to interact with the local file system to read and write static mapping files. + /// + public class LocalFileSystemHandler : IFileSystemHandler + { + private static readonly string AdminMappingsFolder = Path.Combine("__admin", "mappings"); + + /// + public bool FolderExists(string path) + { + return Directory.Exists(path); + } + + /// + public void CreateFolder(string path) + { + Directory.CreateDirectory(path); + } + + /// + public IEnumerable EnumerateFiles(string path) + { + return Directory.EnumerateFiles(path); + } + + /// + public string GetMappingFolder() + { + return Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); + } + + /// + public string ReadMappingFile(string path) + { + return File.ReadAllText(path); + } + + /// + public void WriteMappingFile(string path, string text) + { + File.WriteAllText(path, text); + } + } +} diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index fd176d739..c987878c0 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -30,12 +30,13 @@ namespace WireMock.Server /// public partial class FluentMockServer { - private static readonly string AdminMappingsFolder = Path.Combine("__admin", "mappings"); private const string ContentTypeJson = "application/json"; + private const string AdminMappings = "/__admin/mappings"; private const string AdminRequests = "/__admin/requests"; private const string AdminSettings = "/__admin/settings"; private const string AdminScenarios = "/__admin/scenarios"; + private readonly RegexMatcher _adminMappingsGuidPathMatcher = new RegexMatcher(MatchBehaviour.AcceptOnMatch, @"^\/__admin\/mappings\/(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$"); private readonly RegexMatcher _adminRequestsGuidPathMatcher = new RegexMatcher(MatchBehaviour.AcceptOnMatch, @"^\/__admin\/requests\/(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$"); @@ -100,25 +101,39 @@ private void InitAdmin() } #endregion - #region StaticMappings + #region StaticMappings + /// + /// Saves the static mappings. + /// + /// The optional folder. If not defined, use {CurrentFolder}/__admin/mappings + [PublicAPI] + public void SaveStaticMappings([CanBeNull] string folder = null) + { + foreach (var mapping in Mappings.Where(m => !m.IsAdminInterface)) + { + SaveMappingToFile(mapping, folder); + } + } + /// /// Reads the static mappings from a folder. /// - /// The optional folder. If not defined, use \__admin\mappings\ + /// The optional folder. If not defined, use {CurrentFolder}/__admin/mappings [PublicAPI] public void ReadStaticMappings([CanBeNull] string folder = null) { if (folder == null) { - folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); + folder = _fileSystemHandler.GetMappingFolder(); } - if (!Directory.Exists(folder)) + if (!_fileSystemHandler.FolderExists(folder)) { + _logger.Info("The Static Mapping folder '{0}' does not exist, reading Static MappingFiles will be skipped.", folder); return; } - foreach (string filename in Directory.EnumerateFiles(folder).OrderBy(f => f)) + foreach (string filename in _fileSystemHandler.EnumerateFiles(folder).OrderBy(f => f)) { _logger.Info("Reading Static MappingFile : '{0}'", filename); @@ -136,16 +151,16 @@ public void ReadStaticMappings([CanBeNull] string folder = null) /// /// Watches the static mappings for changes. /// - /// The optional folder. If not defined, use \__admin\mappings\ + /// The optional folder. If not defined, use {CurrentFolder}/__admin/mappings [PublicAPI] public void WatchStaticMappings([CanBeNull] string folder = null) { if (folder == null) { - folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); + folder = _fileSystemHandler.GetMappingFolder(); } - if (!Directory.Exists(folder)) + if (!_fileSystemHandler.FolderExists(folder)) { return; } @@ -192,7 +207,7 @@ public void ReadStaticMappingAndAddOrUpdate([NotNull] string path) string filenameWithoutExtension = Path.GetFileNameWithoutExtension(path); - MappingModel mappingModel = JsonConvert.DeserializeObject(FileHelper.ReadAllText(path)); + MappingModel mappingModel = JsonConvert.DeserializeObject(_fileSystemHandler.ReadMappingFile(path)); if (Guid.TryParse(filenameWithoutExtension, out Guid guidFromFilename)) { DeserializeAndAddOrUpdateMapping(mappingModel, guidFromFilename, path); @@ -345,29 +360,31 @@ private ResponseMessage MappingDelete(RequestMessage requestMessage) #region Mappings private ResponseMessage MappingsSave(RequestMessage requestMessage) { - foreach (var mapping in Mappings.Where(m => !m.IsAdminInterface)) - { - SaveMappingToFile(mapping); - } + SaveStaticMappings(); return ResponseMessageBuilder.Create("Mappings saved to disk"); } - private void SaveMappingToFile(Mapping mapping) + private void SaveMappingToFile(Mapping mapping, string folder = null) { - string folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); - if (!Directory.Exists(folder)) + if (folder == null) { - Directory.CreateDirectory(folder); + folder = _fileSystemHandler.GetMappingFolder(); + } + + if (!_fileSystemHandler.FolderExists(folder)) + { + _fileSystemHandler.CreateFolder(folder); } var model = MappingConverter.ToMappingModel(mapping); - string filename = !string.IsNullOrEmpty(mapping.Title) ? SanitizeFileName(mapping.Title) : mapping.Guid.ToString(); + string filename = (!string.IsNullOrEmpty(mapping.Title) ? SanitizeFileName(mapping.Title) : mapping.Guid.ToString()) + ".json"; + + string path = Path.Combine(folder, filename); - string filePath = Path.Combine(folder, filename + ".json"); - _logger.Info("Saving Mapping to file {0}", filePath); + _logger.Info("Saving Mapping file {0}", filename); - File.WriteAllText(filePath, JsonConvert.SerializeObject(model, _settings)); + _fileSystemHandler.WriteMappingFile(path, JsonConvert.SerializeObject(model, _settings)); } private static string SanitizeFileName(string name, char replaceChar = '_') diff --git a/src/WireMock.Net/Server/FluentMockServer.cs b/src/WireMock.Net/Server/FluentMockServer.cs index 04ad7e396..f7d261d4f 100644 --- a/src/WireMock.Net/Server/FluentMockServer.cs +++ b/src/WireMock.Net/Server/FluentMockServer.cs @@ -1,11 +1,12 @@ using JetBrains.Annotations; -using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; +using Newtonsoft.Json; +using WireMock.Handlers; using WireMock.Http; using WireMock.Logging; using WireMock.Matchers; @@ -14,7 +15,6 @@ using WireMock.RequestBuilders; using WireMock.ResponseProviders; using WireMock.Settings; -using WireMock.Transformers; using WireMock.Validation; namespace WireMock.Server @@ -25,6 +25,8 @@ namespace WireMock.Server public partial class FluentMockServer : IDisposable { private readonly IWireMockLogger _logger; + private readonly IFileSystemHandler _fileSystemHandler; + private const int ServerStartDelay = 100; private readonly IOwinSelfHost _httpServer; private readonly WireMockMiddlewareOptions _options = new WireMockMiddlewareOptions(); @@ -183,7 +185,9 @@ public static FluentMockServer StartWithAdminInterfaceAndReadStaticMappings(para private FluentMockServer(IFluentMockServerSettings settings) { settings.Logger = settings.Logger ?? new WireMockConsoleLogger(); + _logger = settings.Logger; + _fileSystemHandler = settings.FileSystemHandler ?? new LocalFileSystemHandler(); _logger.Info("WireMock.Net by Stef Heyenrath (https://github.com/WireMock-Net/WireMock.Net)"); _logger.Debug("WireMock.Net server settings {0}", JsonConvert.SerializeObject(settings, Formatting.Indented)); diff --git a/src/WireMock.Net/Settings/FluentMockServerSettings.cs b/src/WireMock.Net/Settings/FluentMockServerSettings.cs index 2e45abb40..c773ff9d7 100644 --- a/src/WireMock.Net/Settings/FluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/FluentMockServerSettings.cs @@ -1,6 +1,7 @@ using System; using JetBrains.Annotations; using Newtonsoft.Json; +using WireMock.Handlers; using WireMock.Logging; namespace WireMock.Settings @@ -77,5 +78,10 @@ public class FluentMockServerSettings : IFluentMockServerSettings [PublicAPI] [JsonIgnore] public IWireMockLogger Logger { get; set; } = new WireMockNullLogger(); + + /// + [PublicAPI] + [JsonIgnore] + public IFileSystemHandler FileSystemHandler { get; set; } = new LocalFileSystemHandler(); } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs index 1030b656c..373eda26c 100644 --- a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs @@ -1,5 +1,6 @@ using System; using JetBrains.Annotations; +using WireMock.Handlers; using WireMock.Logging; namespace WireMock.Settings @@ -105,5 +106,11 @@ public interface IFluentMockServerSettings /// [PublicAPI] IWireMockLogger Logger { get; set; } + + /// + /// Handler to interact with the file system to read and write static mapping files. + /// + [PublicAPI] + IFileSystemHandler FileSystemHandler { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/FileHelper.cs b/src/WireMock.Net/Util/FileHelper.cs index 18c0aad24..5af196611 100644 --- a/src/WireMock.Net/Util/FileHelper.cs +++ b/src/WireMock.Net/Util/FileHelper.cs @@ -1,5 +1,8 @@ using System.IO; using System.Threading; +using JetBrains.Annotations; +using WireMock.Handlers; +using WireMock.Validation; namespace WireMock.Util { @@ -8,17 +11,19 @@ internal static class FileHelper private const int NumberOfRetries = 3; private const int DelayOnRetry = 500; - public static string ReadAllText(string path) + public static string ReadAllTextWithRetryAndDelay([NotNull] IFileSystemHandler filehandler, [NotNull] string path) { + Check.NotNull(filehandler, nameof(filehandler)); + Check.NotNullOrEmpty(path, nameof(path)); + for (int i = 1; i <= NumberOfRetries; ++i) { try { - return File.ReadAllText(path); + return filehandler.ReadMappingFile(path); } catch { - // You may check error code to filter some exceptions, not every error can be recovered. Thread.Sleep(DelayOnRetry); } } diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index cd18c438d..6be2c57bd 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -3,7 +3,7 @@ Lightweight Http Mocking Server for .Net, inspired by WireMock from the Java landscape. WireMock.Net - 1.0.4.9 + 1.0.4.10 Stef Heyenrath net452;net46;netstandard1.3;netstandard2.0 true diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index 572483daa..c30657957 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Moq; using NFluent; using WireMock.Matchers; using WireMock.RequestBuilders; @@ -14,6 +15,10 @@ using WireMock.Server; using Xunit; using Newtonsoft.Json; +using WireMock.Handlers; +using WireMock.Logging; +using WireMock.Settings; +using WireMock.Admin.Mappings; namespace WireMock.Net.Tests { @@ -42,6 +47,35 @@ public void FluentMockServer_StartStop() server2.Stop(); } + [Fact] + public void FluentMockServer_SaveStaticMappings() + { + // Assign + string guid = "791a3f31-6946-aaaa-8e6f-0237c7441111"; + var _staticMappingHandlerMock = new Mock(); + _staticMappingHandlerMock.Setup(m => m.GetMappingFolder()).Returns("folder"); + _staticMappingHandlerMock.Setup(m => m.FolderExists(It.IsAny())).Returns(true); + _staticMappingHandlerMock.Setup(m => m.WriteMappingFile(It.IsAny(), It.IsAny())); + + _server = FluentMockServer.Start(new FluentMockServerSettings + { + FileSystemHandler = _staticMappingHandlerMock.Object + }); + + _server + .Given(Request.Create().WithPath($"/foo_{Guid.NewGuid()}")) + .WithGuid(guid) + .RespondWith(Response.Create().WithBody("save test")); + + // Act + _server.SaveStaticMappings(); + + // Assert and Verify + _staticMappingHandlerMock.Verify(m => m.GetMappingFolder(), Times.Once); + _staticMappingHandlerMock.Verify(m => m.FolderExists("folder"), Times.Once); + _staticMappingHandlerMock.Verify(m => m.WriteMappingFile(Path.Combine("folder", guid + ".json"), It.IsAny()), Times.Once); + } + [Fact] public void FluentMockServer_ReadStaticMapping_WithNonGuidFilename() { @@ -108,6 +142,49 @@ public void FluentMockServer_ReadStaticMapping_WithResponseBodyFromFile() Check.That(mappings.First().Title).IsNullOrEmpty(); } + [Fact] + public void FluentMockServer_ReadStaticMappings_FolderExistsIsTrue() + { + // Assign + var _staticMappingHandlerMock = new Mock(); + _staticMappingHandlerMock.Setup(m => m.GetMappingFolder()).Returns("folder"); + _staticMappingHandlerMock.Setup(m => m.FolderExists(It.IsAny())).Returns(true); + _staticMappingHandlerMock.Setup(m => m.EnumerateFiles(It.IsAny())).Returns(new string[0]); + + _server = FluentMockServer.Start(new FluentMockServerSettings + { + FileSystemHandler = _staticMappingHandlerMock.Object + }); + + // Act + _server.ReadStaticMappings(); + + // Assert and Verify + _staticMappingHandlerMock.Verify(m => m.GetMappingFolder(), Times.Once); + _staticMappingHandlerMock.Verify(m => m.FolderExists("folder"), Times.Once); + _staticMappingHandlerMock.Verify(m => m.EnumerateFiles("folder"), Times.Once); + } + + [Fact] + public void FluentMockServer_ReadStaticMappingAndAddOrUpdate() + { + // Assign + string mapping = "{\"Request\": {\"Path\": {\"Matchers\": [{\"Name\": \"WildcardMatcher\",\"Pattern\": \"/static/mapping\"}]},\"Methods\": [\"get\"]},\"Response\": {\"BodyAsJson\": { \"body\": \"static mapping\" }}}"; + var _staticMappingHandlerMock = new Mock(); + _staticMappingHandlerMock.Setup(m => m.ReadMappingFile(It.IsAny())).Returns(mapping); + + _server = FluentMockServer.Start(new FluentMockServerSettings + { + FileSystemHandler = _staticMappingHandlerMock.Object + }); + + // Act + _server.ReadStaticMappingAndAddOrUpdate(@"c:\test.json"); + + // Assert and Verify + _staticMappingHandlerMock.Verify(m => m.ReadMappingFile(@"c:\test.json"), Times.Once); + } + [Fact] public void FluentMockServer_ReadStaticMappings() { @@ -142,7 +219,7 @@ public void FluentMockServer_Admin_Mappings_WithGuidAsString_Get() string guid = "90356dba-b36c-469a-a17e-669cd84f1f05"; _server = FluentMockServer.Start(); - _server.Given(Request.Create().WithPath("/foo1").UsingGet()).WithGuid(guid) + _server.Given(Request.Create().WithPath("/foo100").UsingGet()).WithGuid(guid) .RespondWith(Response.Create().WithStatusCode(201).WithBody("1")); var mappings = _server.Mappings.ToArray(); @@ -219,13 +296,14 @@ public async Task FluentMockServer_Admin_Requests_Get() public async Task FluentMockServer_Should_respond_to_request_methodPatch() { // given + string path = $"/foo_{Guid.NewGuid()}"; _server = FluentMockServer.Start(); - _server.Given(Request.Create().WithPath("/foo").UsingMethod("patch")) + _server.Given(Request.Create().WithPath(path).UsingMethod("patch")) .RespondWith(Response.Create().WithBody("hello patch")); // when - var msg = new HttpRequestMessage(new HttpMethod("patch"), new Uri("http://localhost:" + _server.Ports[0] + "/foo")) + var msg = new HttpRequestMessage(new HttpMethod("patch"), new Uri("http://localhost:" + _server.Ports[0] + path)) { Content = new StringContent("{\"data\": {\"attr\":\"value\"}}") }; @@ -338,13 +416,14 @@ public async Task FluentMockServer_Should_respond_to_request_bodyAsBase64() public async Task FluentMockServer_Should_respond_to_request_bodyAsBytes() { // given + string path = $"/foo_{Guid.NewGuid()}"; _server = FluentMockServer.Start(); - _server.Given(Request.Create().WithPath("/foo").UsingGet()).RespondWith(Response.Create().WithBody(new byte[] { 48, 49 })); + _server.Given(Request.Create().WithPath(path).UsingGet()).RespondWith(Response.Create().WithBody(new byte[] { 48, 49 })); // when - var responseAsString = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/foo"); - var responseAsBytes = await new HttpClient().GetByteArrayAsync("http://localhost:" + _server.Ports[0] + "/foo"); + var responseAsString = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + path); + var responseAsBytes = await new HttpClient().GetByteArrayAsync("http://localhost:" + _server.Ports[0] + path); // then Check.That(responseAsString).IsEqualTo("01"); @@ -371,13 +450,14 @@ public async Task FluentMockServer_Should_respond_to_valid_matchers_when_sent_js foreach (var item in validMatchersForHelloServerJsonMessage) { + string path = $"/foo_{Guid.NewGuid()}"; _server - .Given(Request.Create().WithPath("/foo").WithBody((IMatcher)item[0])) + .Given(Request.Create().WithPath(path).WithBody((IMatcher)item[0])) .RespondWith(Response.Create().WithBody("Hello client")); // Act var content = new StringContent(jsonRequestMessage, Encoding.UTF8, (string)item[1]); - var response = await new HttpClient().PostAsync("http://localhost:" + _server.Ports[0] + "/foo", content); + var response = await new HttpClient().PostAsync("http://localhost:" + _server.Ports[0] + path, content); // Assert var responseString = await response.Content.ReadAsStringAsync(); @@ -392,10 +472,11 @@ public async Task FluentMockServer_Should_respond_to_valid_matchers_when_sent_js public async Task FluentMockServer_Should_respond_404_for_unexpected_request() { // given + string path = $"/foo{Guid.NewGuid()}"; _server = FluentMockServer.Start(); // when - var response = await new HttpClient().GetAsync("http://localhost:" + _server.Ports[0] + "/foo"); + var response = await new HttpClient().GetAsync("http://localhost:" + _server.Ports[0] + path); // then Check.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound); @@ -405,20 +486,21 @@ public async Task FluentMockServer_Should_respond_404_for_unexpected_request() [Fact] public async Task FluentMockServer_Should_find_a_request_satisfying_a_request_spec() { - // given + // Assign + string path = $"/bar_{Guid.NewGuid()}"; _server = FluentMockServer.Start(); // when await new HttpClient().GetAsync("http://localhost:" + _server.Ports[0] + "/foo"); - await new HttpClient().GetAsync("http://localhost:" + _server.Ports[0] + "/bar"); + await new HttpClient().GetAsync("http://localhost:" + _server.Ports[0] + path); // then var result = _server.FindLogEntries(Request.Create().WithPath(new RegexMatcher("^/b.*"))).ToList(); Check.That(result).HasSize(1); var requestLogged = result.First(); - Check.That(requestLogged.RequestMessage.Path).IsEqualTo("/bar"); - Check.That(requestLogged.RequestMessage.Url).IsEqualTo("http://localhost:" + _server.Ports[0] + "/bar"); + Check.That(requestLogged.RequestMessage.Path).IsEqualTo(path); + Check.That(requestLogged.RequestMessage.Url).IsEqualTo("http://localhost:" + _server.Ports[0] + path); } [Fact] @@ -439,11 +521,12 @@ public async Task FluentMockServer_Should_reset_requestlogs() public void FluentMockServer_Should_reset_mappings() { // given + string path = $"/foo_{Guid.NewGuid()}"; _server = FluentMockServer.Start(); _server .Given(Request.Create() - .WithPath("/foo") + .WithPath(path) .UsingGet()) .RespondWith(Response.Create() .WithBody(@"{ msg: ""Hello world!""}")); @@ -453,35 +536,38 @@ public void FluentMockServer_Should_reset_mappings() // then Check.That(_server.Mappings).IsEmpty(); - Check.ThatAsyncCode(() => new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/foo")) + Check.ThatAsyncCode(() => new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + path)) .ThrowsAny(); } [Fact] public async Task FluentMockServer_Should_respond_a_redirect_without_body() { - // given + // Assign + string path = $"/foo_{Guid.NewGuid()}"; + string pathToRedirect = $"/bar_{Guid.NewGuid()}"; + _server = FluentMockServer.Start(); _server .Given(Request.Create() - .WithPath("/foo") + .WithPath(path) .UsingGet()) .RespondWith(Response.Create() .WithStatusCode(307) - .WithHeader("Location", "/bar")); + .WithHeader("Location", pathToRedirect)); _server .Given(Request.Create() - .WithPath("/bar") + .WithPath(pathToRedirect) .UsingGet()) .RespondWith(Response.Create() .WithStatusCode(200) .WithBody("REDIRECT SUCCESSFUL")); - // when - var response = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/foo"); + // Act + var response = await new HttpClient().GetStringAsync($"http://localhost:{_server.Ports[0]}{path}"); - // then + // Assert Check.That(response).IsEqualTo("REDIRECT SUCCESSFUL"); } diff --git a/test/WireMock.Net.Tests/StatefulBehaviorTests.cs b/test/WireMock.Net.Tests/StatefulBehaviorTests.cs index 53738249b..3d47ec8e1 100644 --- a/test/WireMock.Net.Tests/StatefulBehaviorTests.cs +++ b/test/WireMock.Net.Tests/StatefulBehaviorTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -16,16 +17,17 @@ public class StatefulBehaviorTests public async Task Scenarios_Should_skip_non_relevant_states() { // given + string path = $"/foo_{Guid.NewGuid()}"; var server = FluentMockServer.Start(); server - .Given(Request.Create().WithPath("/foo").UsingGet()) + .Given(Request.Create().WithPath(path).UsingGet()) .InScenario("s") .WhenStateIs("Test state") .RespondWith(Response.Create()); // when - var response = await new HttpClient().GetAsync("http://localhost:" + server.Ports[0] + "/foo"); + var response = await new HttpClient().GetAsync("http://localhost:" + server.Ports[0] + path); // then Check.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound); @@ -37,23 +39,24 @@ public async Task Scenarios_Should_skip_non_relevant_states() public async Task Scenarios_Should_process_request_if_equals_state_and_single_state_defined() { // given + string path = $"/foo_{Guid.NewGuid()}"; var server = FluentMockServer.Start(); server - .Given(Request.Create().WithPath("/foo").UsingGet()) + .Given(Request.Create().WithPath(path).UsingGet()) .InScenario("s") .WillSetStateTo("Test state") .RespondWith(Response.Create().WithBody("No state msg")); server - .Given(Request.Create().WithPath("/foo").UsingGet()) + .Given(Request.Create().WithPath(path).UsingGet()) .InScenario("s") .WhenStateIs("Test state") .RespondWith(Response.Create().WithBody("Test state msg")); // when - var responseNoState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/foo"); - var responseWithState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/foo"); + var responseNoState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path); + var responseWithState = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + path); // then Check.That(responseNoState).Equals("No state msg"); diff --git a/test/WireMock.Net.Tests/Util/FileHelperTests.cs b/test/WireMock.Net.Tests/Util/FileHelperTests.cs new file mode 100644 index 000000000..c5837a7c8 --- /dev/null +++ b/test/WireMock.Net.Tests/Util/FileHelperTests.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using Moq; +using NFluent; +using WireMock.Handlers; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util +{ + public class FileHelperTests + { + [Fact] + public void FileHelper_ReadAllTextWithRetryAndDelay() + { + // Assign + var _staticMappingHandlerMock = new Mock(); + _staticMappingHandlerMock.Setup(m => m.ReadMappingFile(It.IsAny())).Returns("text"); + + // Act + string result = FileHelper.ReadAllTextWithRetryAndDelay(_staticMappingHandlerMock.Object, @"c:\temp"); + + // Assert + Check.That(result).Equals("text"); + + // Verify + _staticMappingHandlerMock.Verify(m => m.ReadMappingFile(@"c:\temp"), Times.Once); + } + + [Fact] + public void FileHelper_ReadAllTextWithRetryAndDelay_Throws() + { + // Assign + var _staticMappingHandlerMock = new Mock(); + _staticMappingHandlerMock.Setup(m => m.ReadMappingFile(It.IsAny())).Throws(); + + // Act + Check.ThatCode(() => FileHelper.ReadAllTextWithRetryAndDelay(_staticMappingHandlerMock.Object, @"c:\temp")).Throws(); + + // Verify + _staticMappingHandlerMock.Verify(m => m.ReadMappingFile(@"c:\temp"), Times.Exactly(3)); + } + } +}