From b248c8c6e56fe2885028964c8cbdfa0f4f22ccc6 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 3 Feb 2018 23:22:29 +0100 Subject: [PATCH] EnhancedFileSystemWatcher (#86) --- .../791a3f31-6946-4ce7-8e6f-0237c7443275.json | 2 +- .../MainApp.cs | 1 + src/WireMock.Net.StandAlone/StandAloneApp.cs | 1 + .../Admin/Mappings/MappingModel.cs | 25 +-- src/WireMock.Net/Mapping.cs | 11 +- .../Server/FluentMockServer.Admin.cs | 171 ++++++++++++------ src/WireMock.Net/Server/FluentMockServer.cs | 19 +- .../Server/IRespondWithAProvider.cs | 7 + .../Server/RespondWithAProvider.cs | 50 ++--- .../Settings/FluentMockServerSettings.cs | 4 + .../Settings/IFluentMockServerSettings.cs | 5 + src/WireMock.Net/Util/FileHelper.cs | 29 +++ src/WireMock.Net/WireMock.Net.csproj | 3 +- .../FluentMockServerTests.cs | 4 +- 14 files changed, 213 insertions(+), 119 deletions(-) create mode 100644 src/WireMock.Net/Util/FileHelper.cs diff --git a/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json b/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json index f360d25d..35a1c17b 100644 --- a/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json +++ b/examples/WireMock.Net.Console.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json @@ -27,7 +27,7 @@ }, "UseTransformer": false, "Headers": { - "Date": "Wed, 25 Oct 2017 18:57:40 GMT", + "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" diff --git a/examples/WireMock.Net.ConsoleApplication/MainApp.cs b/examples/WireMock.Net.ConsoleApplication/MainApp.cs index c5a23ffa..6549b171 100644 --- a/examples/WireMock.Net.ConsoleApplication/MainApp.cs +++ b/examples/WireMock.Net.ConsoleApplication/MainApp.cs @@ -21,6 +21,7 @@ namespace WireMock.Net.ConsoleApplication Urls = new[] { url1, url2, url3 }, StartAdminInterface = true, ReadStaticMappings = true, + WatchStaticMappings = true, //ProxyAndRecordSettings = new ProxyAndRecordSettings //{ // SaveMapping = true diff --git a/src/WireMock.Net.StandAlone/StandAloneApp.cs b/src/WireMock.Net.StandAlone/StandAloneApp.cs index a2b365cd..aedea35d 100644 --- a/src/WireMock.Net.StandAlone/StandAloneApp.cs +++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs @@ -43,6 +43,7 @@ namespace WireMock.Net.StandAlone { StartAdminInterface = parser.GetBoolValue("StartAdminInterface", true), ReadStaticMappings = parser.GetBoolValue("ReadStaticMappings"), + WatchStaticMappings = parser.GetBoolValue("WatchStaticMappings"), AllowPartialMapping = parser.GetBoolValue("AllowPartialMapping", true), AdminUsername = parser.GetStringValue("AdminUsername"), AdminPassword = parser.GetStringValue("AdminPassword"), diff --git a/src/WireMock.Net/Admin/Mappings/MappingModel.cs b/src/WireMock.Net/Admin/Mappings/MappingModel.cs index 62ddd6d8..200b5297 100644 --- a/src/WireMock.Net/Admin/Mappings/MappingModel.cs +++ b/src/WireMock.Net/Admin/Mappings/MappingModel.cs @@ -10,29 +10,20 @@ namespace WireMock.Admin.Mappings /// /// Gets or sets the unique identifier. /// - /// - /// The unique identifier. - /// public Guid? Guid { get; set; } /// - /// Gets or sets the unique title. - /// - /// /// The unique title. - /// + /// public string Title { get; set; } /// - /// Gets or sets the priority. - /// - /// /// The priority. - /// + /// public int? Priority { get; set; } /// - /// Scenario. + /// The Scenario. /// public string Scenario { get; set; } @@ -48,19 +39,13 @@ namespace WireMock.Admin.Mappings public object SetStateTo { get; set; } /// - /// Gets or sets the request. - /// - /// /// The request. - /// + /// public RequestModel Request { get; set; } /// - /// Gets or sets the response. - /// - /// /// The response. - /// + /// public ResponseModel Response { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index 72d9a136..d0d9b352 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -20,6 +20,11 @@ namespace WireMock /// public string Title { get; } + /// + /// The full filename path for this mapping (only defined for static mappings). + /// + public string Path { get; set; } + /// /// Gets the priority. /// @@ -63,17 +68,19 @@ namespace WireMock /// Initializes a new instance of the class. /// /// The unique identifier. - /// The unique title (can be null_. + /// The unique title (can be null). + /// The full file path from this mapping title (can be null). /// The request matcher. /// The provider. /// The priority for this mapping. /// The scenario. [Optional] /// State in which the current mapping can occur. [Optional] /// The next state which will occur after the current mapping execution. [Optional] - public Mapping(Guid guid, [CanBeNull] string title, IRequestMatcher requestMatcher, IResponseProvider provider, int priority, [CanBeNull] string scenario, [CanBeNull] object executionConditionState, [CanBeNull] object nextState) + public Mapping(Guid guid, [CanBeNull] string title, [CanBeNull] string path, IRequestMatcher requestMatcher, IResponseProvider provider, int priority, [CanBeNull] string scenario, [CanBeNull] object executionConditionState, [CanBeNull] object nextState) { Guid = guid; Title = title; + Path = path; RequestMatcher = requestMatcher; Provider = provider; Priority = priority; diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 73c71c1f..46928b6b 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -43,46 +43,7 @@ namespace WireMock.Server NullValueHandling = NullValueHandling.Ignore, }; - /// - /// Reads the static mappings from a folder. - /// - /// The optional folder. If not defined, use \__admin\mappings\ - [PublicAPI] - public void ReadStaticMappings([CanBeNull] string folder = null) - { - if (folder == null) - folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); - - if (!Directory.Exists(folder)) - return; - - foreach (string filename in Directory.EnumerateFiles(folder).OrderBy(f => f)) - { - ReadStaticMapping(filename); - } - } - - /// - /// Reads the static mapping. - /// - /// The filename. - [PublicAPI] - public void ReadStaticMapping([NotNull] string filename) - { - Check.NotNull(filename, nameof(filename)); - - string filenameWithoutExtension = Path.GetFileNameWithoutExtension(filename); - - if (Guid.TryParse(filenameWithoutExtension, out var guidFromFilename)) - { - DeserializeAndAddMapping(File.ReadAllText(filename), guidFromFilename); - } - else - { - DeserializeAndAddMapping(File.ReadAllText(filename)); - } - } - + #region InitAdmin private void InitAdmin() { // __admin/settings @@ -129,6 +90,96 @@ namespace WireMock.Server // __admin/scenarios/reset Given(Request.Create().WithPath(AdminScenarios + "/reset").UsingPost()).RespondWith(new DynamicResponseProvider(ScenariosReset)); } + #endregion + + #region StaticMappings + /// + /// Reads the static mappings from a folder. + /// + /// The optional folder. If not defined, use \__admin\mappings\ + [PublicAPI] + public void ReadStaticMappings([CanBeNull] string folder = null) + { + if (folder == null) + { + folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); + } + + if (!Directory.Exists(folder)) + { + return; + } + + foreach (string filename in Directory.EnumerateFiles(folder).OrderBy(f => f)) + { + ReadStaticMappingAndAddOrUpdate(filename); + } + } + + /// + /// Watches the static mappings for changes. + /// + /// The optional folder. If not defined, use \__admin\mappings\ + [PublicAPI] + public void WatchStaticMappings([CanBeNull] string folder = null) + { + if (folder == null) + { + folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); + } + + if (!Directory.Exists(folder)) + { + return; + } + + var watcher = new EnhancedFileSystemWatcher(folder, "*.json", 500); + watcher.Created += (sender, args) => + { + ReadStaticMappingAndAddOrUpdate(args.FullPath); + }; + watcher.Changed += (sender, args) => + { + ReadStaticMappingAndAddOrUpdate(args.FullPath); + }; + watcher.Deleted += (sender, args) => + { + string filenameWithoutExtension = Path.GetFileNameWithoutExtension(args.FullPath); + + if (Guid.TryParse(filenameWithoutExtension, out var guidFromFilename)) + { + DeleteMapping(guidFromFilename); + } + else + { + DeleteMapping(args.FullPath); + } + }; + + watcher.EnableRaisingEvents = true; + } + + /// + /// Reads a static mapping file and adds or updates the mapping. + /// + /// The path. + [PublicAPI] + public void ReadStaticMappingAndAddOrUpdate([NotNull] string path) + { + Check.NotNull(path, nameof(path)); + + string filenameWithoutExtension = Path.GetFileNameWithoutExtension(path); + + if (Guid.TryParse(filenameWithoutExtension, out Guid guidFromFilename)) + { + DeserializeAndAddOrUpdateMapping(FileHelper.ReadAllText(path), guidFromFilename, path); + } + else + { + DeserializeAndAddOrUpdateMapping(FileHelper.ReadAllText(path), null, path); + } + } + #endregion #region Proxy and Record private HttpClient _httpClientForProxy; @@ -185,7 +236,7 @@ namespace WireMock.Server var response = Response.Create(responseMessage); - return new Mapping(Guid.NewGuid(), string.Empty, request, response, 0, null, null, null); + return new Mapping(Guid.NewGuid(), string.Empty, null, request, response, 0, null, null, null); } #endregion @@ -228,7 +279,9 @@ namespace WireMock.Server var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid); if (mapping == null) + { return new ResponseMessage { StatusCode = 404, Body = "Mapping not found" }; + } var model = MappingConverter.ToMappingModel(mapping); @@ -238,23 +291,8 @@ namespace WireMock.Server private ResponseMessage MappingPut(RequestMessage requestMessage) { Guid guid = Guid.Parse(requestMessage.Path.TrimStart(AdminMappings.ToCharArray())); - var mappingModel = JsonConvert.DeserializeObject(requestMessage.Body); - if (mappingModel.Request == null) - return new ResponseMessage { StatusCode = 400, Body = "Request missing" }; - - if (mappingModel.Response == null) - return new ResponseMessage { StatusCode = 400, Body = "Response missing" }; - - var requestBuilder = InitRequestBuilder(mappingModel.Request); - var responseBuilder = InitResponseBuilder(mappingModel.Response); - - IRespondWithAProvider respondProvider = Given(requestBuilder).WithGuid(guid); - - if (!string.IsNullOrEmpty(mappingModel.Title)) - respondProvider = respondProvider.WithTitle(mappingModel.Title); - - respondProvider.RespondWith(responseBuilder); + DeserializeAndAddOrUpdateMapping(requestMessage.Body, guid); return new ResponseMessage { Body = "Mapping added or updated" }; } @@ -264,7 +302,9 @@ namespace WireMock.Server Guid guid = Guid.Parse(requestMessage.Path.Substring(AdminMappings.Length + 1)); if (DeleteMapping(guid)) + { return new ResponseMessage { Body = "Mapping removed" }; + } return new ResponseMessage { Body = "Mapping not found" }; } @@ -285,7 +325,9 @@ namespace WireMock.Server { string folder = Path.Combine(Directory.GetCurrentDirectory(), AdminMappingsFolder); if (!Directory.Exists(folder)) + { Directory.CreateDirectory(folder); + } var model = MappingConverter.ToMappingModel(mapping); string json = JsonConvert.SerializeObject(model, _settings); @@ -315,7 +357,7 @@ namespace WireMock.Server { try { - DeserializeAndAddMapping(requestMessage.Body); + DeserializeAndAddOrUpdateMapping(requestMessage.Body); } catch (ArgumentException a) { @@ -329,7 +371,7 @@ namespace WireMock.Server return new ResponseMessage { StatusCode = 201, Body = "Mapping added" }; } - private void DeserializeAndAddMapping(string json, Guid? guid = null) + private void DeserializeAndAddOrUpdateMapping(string json, Guid? guid = null, string path = null) { var mappingModel = JsonConvert.DeserializeObject(json); @@ -351,11 +393,20 @@ namespace WireMock.Server respondProvider = respondProvider.WithGuid(mappingModel.Guid.Value); } + if (path != null) + { + respondProvider = respondProvider.WithPath(path); + } + if (!string.IsNullOrEmpty(mappingModel.Title)) + { respondProvider = respondProvider.WithTitle(mappingModel.Title); + } if (mappingModel.Priority != null) + { respondProvider = respondProvider.AtPriority(mappingModel.Priority.Value); + } if (mappingModel.Scenario != null) { @@ -688,7 +739,7 @@ namespace WireMock.Server { Body = JsonConvert.SerializeObject(result, _settings), StatusCode = 200, - Headers = new Dictionary> { { "Content-Type", new WireMockList("application/json") } } + Headers = new Dictionary> { { HttpKnownHeaderNames.ContentType, new WireMockList("application/json") } } }; } diff --git a/src/WireMock.Net/Server/FluentMockServer.cs b/src/WireMock.Net/Server/FluentMockServer.cs index b2e55cc3..3ab9838c 100644 --- a/src/WireMock.Net/Server/FluentMockServer.cs +++ b/src/WireMock.Net/Server/FluentMockServer.cs @@ -13,6 +13,7 @@ using WireMock.RequestBuilders; using WireMock.Settings; using WireMock.Validation; using WireMock.Owin; +using WireMock.Serialization; namespace WireMock.Server { @@ -202,6 +203,11 @@ namespace WireMock.Server ReadStaticMappings(); } + if (settings.WatchStaticMappings == true) + { + WatchStaticMappings(); + } + if (settings.ProxyAndRecordSettings != null) { InitProxyAndRecord(settings.ProxyAndRecordSettings); @@ -274,7 +280,18 @@ namespace WireMock.Server public bool DeleteMapping(Guid guid) { // Check a mapping exists with the same GUID, if so, remove it. - var existingMapping = _options.Mappings.FirstOrDefault(m => m.Guid == guid); + return DeleteMapping(m => m.Guid == guid); + } + + private bool DeleteMapping(string path) + { + // Check a mapping exists with the same path, if so, remove it. + return DeleteMapping(m => string.Equals(m.Path, path, StringComparison.OrdinalIgnoreCase)); + } + + private bool DeleteMapping(Func predicate) + { + var existingMapping = _options.Mappings.FirstOrDefault(predicate); if (existingMapping != null) { _options.Mappings.Remove(existingMapping); diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs index 72486ac9..fd10623e 100644 --- a/src/WireMock.Net/Server/IRespondWithAProvider.cs +++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs @@ -21,6 +21,13 @@ namespace WireMock.Server /// The . IRespondWithAProvider WithTitle(string title); + /// + /// Define the full filepath for this mapping. + /// + /// The full filepath. + /// The . + IRespondWithAProvider WithPath(string path); + /// /// Define a unique identifier for this mapping. /// diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index 77546c68..bc7f50f2 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -11,18 +11,11 @@ namespace WireMock.Server private int _priority; private Guid? _guid; private string _title; + private string _path; private object _executionConditionState; private object _nextState; private string _scenario; - - /// - /// The _registration callback. - /// private readonly RegistrationCallback _registrationCallback; - - /// - /// The _request matcher. - /// private readonly IRequestMatcher _requestMatcher; /// @@ -39,30 +32,20 @@ namespace WireMock.Server /// /// The respond with. /// - /// - /// The provider. - /// + /// The provider. public void RespondWith(IResponseProvider provider) { var mappingGuid = _guid ?? Guid.NewGuid(); - _registrationCallback(new Mapping(mappingGuid, _title, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState)); + _registrationCallback(new Mapping(mappingGuid, _title, _path, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState)); } - /// - /// Define a unique identifier for this mapping. - /// - /// The unique identifier. - /// The . + /// public IRespondWithAProvider WithGuid(string guid) { return WithGuid(Guid.Parse(guid)); } - /// - /// Define a unique identifier for this mapping. - /// - /// The unique identifier. - /// The . + /// public IRespondWithAProvider WithGuid(Guid guid) { _guid = guid; @@ -70,11 +53,7 @@ namespace WireMock.Server return this; } - /// - /// Define a unique identifier for this mapping. - /// - /// The unique identifier. - /// The . + /// public IRespondWithAProvider WithTitle(string title) { _title = title; @@ -82,11 +61,15 @@ namespace WireMock.Server return this; } - /// - /// Define the priority for this mapping. - /// - /// The priority. - /// The . + /// + public IRespondWithAProvider WithPath(string path) + { + _path = path; + + return this; + } + + /// public IRespondWithAProvider AtPriority(int priority) { _priority = priority; @@ -94,6 +77,7 @@ namespace WireMock.Server return this; } + /// public IRespondWithAProvider InScenario(string scenario) { _scenario = scenario; @@ -101,6 +85,7 @@ namespace WireMock.Server return this; } + /// public IRespondWithAProvider WhenStateIs(object state) { if (string.IsNullOrEmpty(_scenario)) @@ -118,6 +103,7 @@ namespace WireMock.Server return this; } + /// public IRespondWithAProvider WillSetStateTo(object state) { if (string.IsNullOrEmpty(_scenario)) diff --git a/src/WireMock.Net/Settings/FluentMockServerSettings.cs b/src/WireMock.Net/Settings/FluentMockServerSettings.cs index d526eafb..2784e620 100644 --- a/src/WireMock.Net/Settings/FluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/FluentMockServerSettings.cs @@ -25,6 +25,10 @@ namespace WireMock.Settings [PublicAPI] public bool? ReadStaticMappings { get; set; } + /// + [PublicAPI] + public bool? WatchStaticMappings { get; set; } + /// [PublicAPI] public IProxyAndRecordSettings ProxyAndRecordSettings { get; set; } diff --git a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs index 92cebb7a..3ad5ba59 100644 --- a/src/WireMock.Net/Settings/IFluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/IFluentMockServerSettings.cs @@ -28,6 +28,11 @@ namespace WireMock.Settings /// bool? ReadStaticMappings { get; set; } + /// + /// Watch the static mapping files + folder for changes when running. + /// + bool? WatchStaticMappings { get; set; } + /// /// Gets or sets if the proxy and record settings. /// diff --git a/src/WireMock.Net/Util/FileHelper.cs b/src/WireMock.Net/Util/FileHelper.cs new file mode 100644 index 00000000..18c0aad2 --- /dev/null +++ b/src/WireMock.Net/Util/FileHelper.cs @@ -0,0 +1,29 @@ +using System.IO; +using System.Threading; + +namespace WireMock.Util +{ + internal static class FileHelper + { + private const int NumberOfRetries = 3; + private const int DelayOnRetry = 500; + + public static string ReadAllText(string path) + { + for (int i = 1; i <= NumberOfRetries; ++i) + { + try + { + return File.ReadAllText(path); + } + catch + { + // You may check error code to filter some exceptions, not every error can be recovered. + Thread.Sleep(DelayOnRetry); + } + } + + throw new IOException(); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 7884a767..e85b9cb2 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -33,7 +33,7 @@ - + All @@ -41,6 +41,7 @@ + diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index db45d7af..91431a5c 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -47,7 +47,7 @@ namespace WireMock.Net.Tests _server = FluentMockServer.Start(); string folder = Path.Combine(GetCurrentFolder(), "__admin", "mappings", "documentdb_root.json"); - _server.ReadStaticMapping(folder); + _server.ReadStaticMappingAndAddOrUpdate(folder); var mappings = _server.Mappings.ToArray(); Check.That(mappings).HasSize(1); @@ -65,7 +65,7 @@ namespace WireMock.Net.Tests _server = FluentMockServer.Start(); string folder = Path.Combine(GetCurrentFolder(), "__admin", "mappings", guid + ".json"); - _server.ReadStaticMapping(folder); + _server.ReadStaticMappingAndAddOrUpdate(folder); var mappings = _server.Mappings.ToArray(); Check.That(mappings).HasSize(1);