diff --git a/tests/FMData.Rest.Tests/EditDeleteGetByIdTests.cs b/tests/FMData.Rest.Tests/EditDeleteGetByIdTests.cs new file mode 100644 index 0000000..6d77578 --- /dev/null +++ b/tests/FMData.Rest.Tests/EditDeleteGetByIdTests.cs @@ -0,0 +1,222 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FMData.Rest.Tests.TestModels; +using RichardSzalay.MockHttp; +using Xunit; + +namespace FMData.Rest.Tests +{ + public class EditDeleteGetByIdTests + { + private static readonly string Server = "http://localhost"; + private static readonly string File = "test-file"; + private static readonly string Layout = "Users"; + + private static ConnectionInfo TestConnection => new ConnectionInfo + { + FmsUri = Server, + Database = File, + Username = "unit", + Password = "test" + }; + + private static MockHttpMessageHandler CreateMock() + { + var mockHttp = new MockHttpMessageHandler(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/sessions*") + .Respond(HttpStatusCode.OK, "application/json", ""); + + return mockHttp; + } + + #region EditAsync Overloads + + [Fact(DisplayName = "EditAsync(int, T) passthrough should succeed")] + public async Task EditAsync_RecordIdAndModel_ShouldSucceed() + { + var mockHttp = CreateMock(); + + mockHttp.When(new HttpMethod("PATCH"), $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/42") + .Respond("application/json", DataApiResponses.SuccessfulEdit()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var response = await client.EditAsync(42, new User { Name = "updated" }); + + Assert.NotNull(response); + Assert.Contains(response.Messages, r => r.Message == "OK"); + } + + [Fact(DisplayName = "EditAsync(int, T, bool, bool) includes null values when flag set")] + public async Task EditAsync_WithNullValueFlag_ShouldSucceed() + { + var mockHttp = CreateMock(); + + mockHttp.When(new HttpMethod("PATCH"), $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/42") + .Respond("application/json", DataApiResponses.SuccessfulEdit()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var response = await client.EditAsync(42, new User { Name = "updated", ForeignKeyId = null }, + includeNullValues: true, includeDefaultValues: true); + + Assert.NotNull(response); + Assert.Contains(response.Messages, r => r.Message == "OK"); + } + + #endregion + + #region GetByFileMakerIdAsync Overloads + + [Fact(DisplayName = "GetByFileMakerIdAsync(int) infers layout from type")] + public async Task GetByFileMakerIdAsync_InfersLayout() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Get, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/4") + .Respond("application/json", DataApiResponses.SuccessfulGetById(4)); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var result = await client.GetByFileMakerIdAsync(4, fmId: null); + + Assert.NotNull(result); + Assert.Equal("fuzzzerd", result.Name); + } + + [Fact(DisplayName = "GetByFileMakerIdAsync with fmIdFunc maps RecordId")] + public async Task GetByFileMakerIdAsync_WithFmIdFunc_MapsRecordId() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Get, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/4") + .Respond("application/json", DataApiResponses.SuccessfulGetById(4)); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var result = await client.GetByFileMakerIdAsync(4, + fmId: (o, id) => o.FileMakerRecordId = id); + + Assert.NotNull(result); + Assert.Equal(4, result.FileMakerRecordId); + } + + [Fact(DisplayName = "GetByFileMakerIdAsync with both mappers maps RecordId and ModId")] + public async Task GetByFileMakerIdAsync_WithBothMappers_MapsBothIds() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Get, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/4") + .Respond("application/json", DataApiResponses.SuccessfulGetById(4)); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var result = await client.GetByFileMakerIdAsync(4, + fmId: (o, id) => o.FileMakerRecordId = id, + fmMod: (o, modId) => o.FileMakerModId = modId); + + Assert.NotNull(result); + Assert.Equal(4, result.FileMakerRecordId); + Assert.Equal(0, result.FileMakerModId); + } + + [Fact(DisplayName = "GetByFileMakerIdAsync with explicit layout works")] + public async Task GetByFileMakerIdAsync_WithExplicitLayout() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Get, $"{Server}/fmi/data/v1/databases/{File}/layouts/CustomLayout/records/7") + .Respond("application/json", DataApiResponses.SuccessfulGetById(7)); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var result = await client.GetByFileMakerIdAsync("CustomLayout", 7, + fmId: (o, id) => o.FileMakerRecordId = id); + + Assert.NotNull(result); + Assert.Equal(7, result.FileMakerRecordId); + } + + #endregion + + #region ProcessContainers (batch) + + [Fact(DisplayName = "ProcessContainers processes multiple models")] + public async Task ProcessContainers_ProcessesMultipleModels() + { + var mockHttp = CreateMock(); + + // container URLs need to be valid URIs + var url1 = $"{Server}/container1.jpg"; + var url2 = $"{Server}/container2.jpg"; + + mockHttp.When(HttpMethod.Get, url1) + .Respond("application/octet-stream", new System.IO.MemoryStream(new byte[] { 1, 2, 3 })); + + mockHttp.When(HttpMethod.Get, url2) + .Respond("application/octet-stream", new System.IO.MemoryStream(new byte[] { 4, 5, 6 })); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var models = new[] + { + new ContainerFieldTestModel { SomeContainerField = url1 }, + new ContainerFieldTestModel { SomeContainerField = url2 } + }; + + await client.ProcessContainers(models); + + Assert.NotNull(models[0].SomeContainerFieldData); + Assert.NotNull(models[1].SomeContainerFieldData); + } + + #endregion + + #region Obsolete Metadata Methods with Database Validation + + [Fact(DisplayName = "GetLayoutsAsync(database) throws when database doesn't match")] + public async Task GetLayoutsAsync_WithWrongDatabase_Throws() + { + var mockHttp = CreateMock(); + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + +#pragma warning disable CS0618 // intentionally testing the obsolete overload + await Assert.ThrowsAsync( + () => client.GetLayoutsAsync("wrong-database")); +#pragma warning restore CS0618 + } + + [Fact(DisplayName = "GetScriptsAsync(database) throws when database doesn't match")] + public async Task GetScriptsAsync_WithWrongDatabase_Throws() + { + var mockHttp = CreateMock(); + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + +#pragma warning disable CS0618 // intentionally testing the obsolete overload + await Assert.ThrowsAsync( + () => client.GetScriptsAsync("wrong-database")); +#pragma warning restore CS0618 + } + + [Fact(DisplayName = "GetLayoutAsync(database, layout) throws when database doesn't match")] + public async Task GetLayoutAsync_WithWrongDatabase_Throws() + { + var mockHttp = CreateMock(); + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + +#pragma warning disable CS0618 // intentionally testing the obsolete overload + await Assert.ThrowsAsync( + () => client.GetLayoutAsync("wrong-database", "layout")); +#pragma warning restore CS0618 + } + + #endregion + } +} diff --git a/tests/FMData.Rest.Tests/ExecuteRequestAsyncTests.cs b/tests/FMData.Rest.Tests/ExecuteRequestAsyncTests.cs new file mode 100644 index 0000000..3e4e6ff --- /dev/null +++ b/tests/FMData.Rest.Tests/ExecuteRequestAsyncTests.cs @@ -0,0 +1,170 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FMData.Rest.Requests; +using RichardSzalay.MockHttp; +using Xunit; + +namespace FMData.Rest.Tests +{ + public class ExecuteRequestAsyncTests + { + private static readonly string Server = "http://localhost"; + private static readonly string File = "test-file"; + private static readonly string Layout = "layout"; + + private static ConnectionInfo TestConnection => new ConnectionInfo + { + FmsUri = Server, + Database = File, + Username = "unit", + Password = "test" + }; + + private static MockHttpMessageHandler CreateMock() + { + var mockHttp = new MockHttpMessageHandler(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/sessions*") + .Respond(HttpStatusCode.OK, "application/json", ""); + + return mockHttp; + } + + #region GetRecordsEndpoint + + [Fact(DisplayName = "GetRecordsEndpoint produces correct URL")] + public void GetRecordsEndpoint_ProducesCorrectUrl() + { + var mockHttp = new MockHttpMessageHandler(); + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var endpoint = client.GetRecordsEndpoint(Layout, 10, 5); + + Assert.Equal($"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records?_limit=10&_offset=5", endpoint); + } + + [Fact(DisplayName = "GetRecordsEndpoint escapes layout name")] + public void GetRecordsEndpoint_EscapesLayoutName() + { + var mockHttp = new MockHttpMessageHandler(); + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var endpoint = client.GetRecordsEndpoint("My Layout", 10, 0); + + Assert.Contains("My%20Layout", endpoint); + } + + #endregion + + #region ExecuteRequestAsync Typed Overloads + + [Fact(DisplayName = "ExecuteRequestAsync with CreateRequest sends POST")] + public async Task ExecuteRequestAsync_CreateRequest_SendsPost() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records") + .Respond("application/json", DataApiResponses.SuccessfulCreate()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var req = new CreateRequest + { + Layout = Layout, + Data = new TestModels.User { Name = "test" } + }; + + var response = await client.ExecuteRequestAsync(req); + + Assert.True(response.IsSuccessStatusCode); + } + + [Fact(DisplayName = "ExecuteRequestAsync with FindRequest sends POST to _find")] + public async Task ExecuteRequestAsync_FindRequest_SendsPostToFind() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/_find") + .Respond("application/json", DataApiResponses.SuccessfulFind()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var req = new FindRequest { Layout = Layout }; + req.AddQuery(new TestModels.User { Name = "test" }, false); + + var response = await client.ExecuteRequestAsync(req); + + Assert.True(response.IsSuccessStatusCode); + } + + [Fact(DisplayName = "ExecuteRequestAsync with EditRequest sends PATCH")] + public async Task ExecuteRequestAsync_EditRequest_SendsPatch() + { + var mockHttp = CreateMock(); + + mockHttp.When(new HttpMethod("PATCH"), $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/42") + .Respond("application/json", DataApiResponses.SuccessfulEdit()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var req = new EditRequest + { + Layout = Layout, + RecordId = 42, + Data = new TestModels.User { Name = "updated" } + }; + + var response = await client.ExecuteRequestAsync(req); + + Assert.True(response.IsSuccessStatusCode); + } + + [Fact(DisplayName = "ExecuteRequestAsync with DeleteRequest sends DELETE")] + public async Task ExecuteRequestAsync_DeleteRequest_SendsDelete() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/records/99") + .Respond("application/json", DataApiResponses.SuccessfulDelete()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + var req = new DeleteRequest { Layout = Layout, RecordId = 99 }; + + var response = await client.ExecuteRequestAsync(req); + + Assert.True(response.IsSuccessStatusCode); + } + + #endregion + + #region ExecuteRequestAsync Core Method + + [Fact(DisplayName = "ExecuteRequestAsync refreshes token before sending")] + public async Task ExecuteRequestAsync_RefreshesTokenBeforeSending() + { + var mockHttp = CreateMock(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/_find") + .Respond("application/json", DataApiResponses.SuccessfulFind()); + + using var client = new FileMakerRestClient(mockHttp.ToHttpClient(), TestConnection); + + Assert.False(client.IsAuthenticated); + + var req = new FindRequest { Layout = Layout }; + req.AddQuery(new TestModels.User { Name = "test" }, false); + + var response = await client.ExecuteRequestAsync(req); + + Assert.True(client.IsAuthenticated); + Assert.True(response.IsSuccessStatusCode); + } + + #endregion + } +} diff --git a/tests/FMData.Rest.Tests/FindAsyncMapperTests.cs b/tests/FMData.Rest.Tests/FindAsyncMapperTests.cs new file mode 100644 index 0000000..c038f6d --- /dev/null +++ b/tests/FMData.Rest.Tests/FindAsyncMapperTests.cs @@ -0,0 +1,182 @@ +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FMData.Rest.Tests.TestModels; +using RichardSzalay.MockHttp; +using Xunit; + +namespace FMData.Rest.Tests +{ + public class FindAsyncMapperTests + { + private static readonly string Server = "http://localhost"; + private static readonly string File = "test-file"; + private static readonly string User = "unit"; + private static readonly string Pass = "test"; + private static readonly string Layout = "Users"; + + private static FileMakerRestClient GetClient(MockHttpMessageHandler mockHttp) + { + return new FileMakerRestClient( + mockHttp.ToHttpClient(), + new ConnectionInfo + { + FmsUri = Server, + Database = File, + Username = User, + Password = Pass + }); + } + + private static MockHttpMessageHandler CreateMockWithFindAndAuth() + { + var mockHttp = new MockHttpMessageHandler(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/sessions*") + .Respond(HttpStatusCode.OK, "application/json", ""); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/_find") + .Respond("application/json", DataApiResponses.SuccessfulFind()); + + return mockHttp; + } + + [Fact(DisplayName = "FindAsync with fmIdFunc maps FileMaker RecordId")] + public async Task FindAsync_WithFmIdFunc_MapsRecordId() + { + var mockHttp = CreateMockWithFindAndAuth(); + using var client = GetClient(mockHttp); + + var results = await client.FindAsync( + new User { Name = "fuzzzerd" }, + (o, id) => o.FileMakerRecordId = id); + + Assert.NotEmpty(results); + Assert.Contains(results, r => r.FileMakerRecordId == 4); + Assert.Contains(results, r => r.FileMakerRecordId == 1); + } + + [Fact(DisplayName = "FindAsync with skip/take and fmIdFunc maps RecordId")] + public async Task FindAsync_WithSkipTakeAndFmIdFunc_MapsRecordId() + { + var mockHttp = CreateMockWithFindAndAuth(); + using var client = GetClient(mockHttp); + + var results = await client.FindAsync( + new User { Name = "fuzzzerd" }, + skip: 0, + take: 10, + fmIdFunc: (o, id) => o.FileMakerRecordId = id); + + Assert.NotEmpty(results); + Assert.Contains(results, r => r.FileMakerRecordId == 4); + } + + [Fact(DisplayName = "FindAsync with script and fmIdFunc maps RecordId")] + public async Task FindAsync_WithScriptAndFmIdFunc_MapsRecordId() + { + var mockHttp = new MockHttpMessageHandler(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/sessions*") + .Respond(HttpStatusCode.OK, "application/json", ""); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/_find") + .WithPartialContent("script") + .Respond("application/json", DataApiResponses.SuccessfulFind()); + + using var client = GetClient(mockHttp); + + var results = await client.FindAsync( + new User { Name = "fuzzzerd" }, + script: "TestScript", + scriptParameter: "param", + fmIdFunc: (o, id) => o.FileMakerRecordId = id); + + Assert.NotEmpty(results); + Assert.Contains(results, r => r.FileMakerRecordId == 4); + } + + [Fact(DisplayName = "FindAsync with skip/take, script, and fmIdFunc maps RecordId")] + public async Task FindAsync_WithSkipTakeScriptAndFmIdFunc_MapsRecordId() + { + var mockHttp = new MockHttpMessageHandler(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/sessions*") + .Respond(HttpStatusCode.OK, "application/json", ""); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/{Layout}/_find") + .WithPartialContent("limit") + .WithPartialContent("script") + .Respond("application/json", DataApiResponses.SuccessfulFind()); + + using var client = GetClient(mockHttp); + + var results = await client.FindAsync( + new User { Name = "fuzzzerd" }, + skip: 0, + take: 5, + script: "TestScript", + scriptParameter: "param", + fmIdFunc: (o, id) => o.FileMakerRecordId = id); + + Assert.NotEmpty(results); + Assert.Contains(results, r => r.FileMakerRecordId == 4); + } + + [Fact(DisplayName = "FindAsync with both fmIdFunc and fmModIdFunc maps both IDs")] + public async Task FindAsync_WithBothMappers_MapsBothIds() + { + var mockHttp = CreateMockWithFindAndAuth(); + using var client = GetClient(mockHttp); + + var results = await client.FindAsync( + new User { Name = "fuzzzerd" }, + skip: 0, + take: 10, + script: null, + scriptParameter: null, + fmIdFunc: (o, id) => o.FileMakerRecordId = id, + fmModIdFunc: (o, modId) => o.FileMakerModId = modId); + + Assert.NotEmpty(results); + var first = results.First(r => r.FileMakerRecordId == 4); + Assert.Equal(4, first.FileMakerRecordId); + Assert.Equal(0, first.FileMakerModId); + + var second = results.First(r => r.FileMakerRecordId == 1); + Assert.Equal(1, second.FileMakerRecordId); + Assert.Equal(12, second.FileMakerModId); + } + + [Fact(DisplayName = "FindAsync with layout override uses specified layout")] + public async Task FindAsync_WithLayoutOverride_UsesSpecifiedLayout() + { + var mockHttp = new MockHttpMessageHandler(); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/sessions") + .Respond("application/json", DataApiResponses.SuccessfulAuthentication()); + + mockHttp.When(HttpMethod.Delete, $"{Server}/fmi/data/v1/databases/{File}/sessions*") + .Respond(HttpStatusCode.OK, "application/json", ""); + + mockHttp.When(HttpMethod.Post, $"{Server}/fmi/data/v1/databases/{File}/layouts/CustomLayout/_find") + .Respond("application/json", DataApiResponses.SuccessfulFind()); + + using var client = GetClient(mockHttp); + + var results = await client.FindAsync("CustomLayout", new User { Name = "fuzzzerd" }); + + Assert.NotEmpty(results); + } + } +} diff --git a/tests/FMData.Rest.Tests/RequestExtensionTests.cs b/tests/FMData.Rest.Tests/RequestExtensionTests.cs new file mode 100644 index 0000000..9bfa04d --- /dev/null +++ b/tests/FMData.Rest.Tests/RequestExtensionTests.cs @@ -0,0 +1,463 @@ +using System.Linq; +using FMData.Rest.Requests; +using FMData.Rest.Tests.TestModels; +using Xunit; + +namespace FMData.Rest.Tests +{ + public class RequestExtensionTests + { + #region FindRequest Extensions + + [Fact(DisplayName = "AddCriteria adds query and returns same request")] + public void AddCriteria_AddsQueryAndReturnsSameRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.AddCriteria(new User { Name = "test" }, false); + + Assert.Same(req, result); + Assert.Single(req.Query); + } + + [Fact(DisplayName = "AddCriteria with omit sets omit flag")] + public void AddCriteria_WithOmit_SetsOmitFlag() + { + var req = new FindRequest { Layout = "Users" }; + req.AddCriteria(new User { Name = "exclude" }, true); + + Assert.Single(req.Query); + Assert.True(req.Query.First().Omit); + } + + [Fact(DisplayName = "AddSortFieldDirection adds sort and returns same request")] + public void AddSortFieldDirection_AddsSortAndReturnsSameRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.AddSortFieldDirection("Name", "ascend"); + + Assert.Same(req, result); + Assert.Single(req.Sort); + Assert.Equal("Name", req.Sort.First().FieldName); + Assert.Equal("ascend", req.Sort.First().SortOrder); + } + + [Fact(DisplayName = "SetLimit sets limit and returns same request")] + public void SetLimit_SetsLimitAndReturnsSameRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.SetLimit(50); + + Assert.Same(req, result); + Assert.Equal(50, req.Limit); + } + + [Fact(DisplayName = "SetOffset sets offset and returns same request")] + public void SetOffset_SetsOffsetAndReturnsSameRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.SetOffset(25); + + Assert.Same(req, result); + Assert.Equal(25, req.Offset); + } + + [Fact(DisplayName = "UseLayout with string sets layout and returns same request")] + public void UseLayout_String_SetsLayoutAndReturnsSameRequest() + { + var req = new FindRequest(); + var result = req.UseLayout("CustomLayout"); + + Assert.Same(req, result); + Assert.Equal("CustomLayout", req.Layout); + } + + [Fact(DisplayName = "UseLayout with instance infers layout from DataContract")] + public void UseLayout_Instance_InfersLayoutFromDataContract() + { + var req = new FindRequest(); + var result = req.UseLayout(new User()); + + Assert.Same(req, result); + Assert.Equal("Users", req.Layout); + } + + [Fact(DisplayName = "LoadContainers sets flag and returns same request")] + public void LoadContainers_SetsFlagAndReturnsSameRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.LoadContainers(); + + Assert.Same(req, result); + Assert.True(req.LoadContainerData); + } + + [Fact(DisplayName = "SetScript sets script name and returns same request")] + public void SetScript_SetsScriptNameAndReturnsSameRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.SetScript("MyScript"); + + Assert.Same(req, result); + Assert.Equal("MyScript", req.Script); + } + + [Fact(DisplayName = "SetScript with parameter sets both values")] + public void SetScript_WithParameter_SetsBothValues() + { + var req = new FindRequest { Layout = "Users" }; + req.SetScript("MyScript", "param1"); + + Assert.Equal("MyScript", req.Script); + Assert.Equal("param1", req.ScriptParameter); + } + + [Fact(DisplayName = "SetScript without parameter does not set parameter")] + public void SetScript_WithoutParameter_DoesNotSetParameter() + { + var req = new FindRequest { Layout = "Users" }; + req.SetScript("MyScript"); + + Assert.Equal("MyScript", req.Script); + Assert.Null(req.ScriptParameter); + } + + [Fact(DisplayName = "SetPreRequestScript sets pre-request script and parameter")] + public void SetPreRequestScript_SetsPreRequestScriptAndParameter() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.SetPreRequestScript("PreScript", "preParam"); + + Assert.Same(req, result); + Assert.Equal("PreScript", req.PreRequestScript); + Assert.Equal("preParam", req.PreRequestScriptParameter); + } + + [Fact(DisplayName = "SetPreSortScript sets pre-sort script and parameter")] + public void SetPreSortScript_SetsPreSortScriptAndParameter() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.SetPreSortScript("SortScript", "sortParam"); + + Assert.Same(req, result); + Assert.Equal("SortScript", req.PreSortScript); + Assert.Equal("sortParam", req.PreSortScriptParameter); + } + + [Fact(DisplayName = "IncludePortals adds portal names to request")] + public void IncludePortals_AddsPortalNamesToRequest() + { + var req = new FindRequest { Layout = "Users" }; + var result = req.IncludePortals("Portal1", "Portal2"); + + Assert.Same(req, result); + Assert.Equal(2, req.Portals.Count); + Assert.Contains(req.Portals, p => p.PortalName == "Portal1"); + Assert.Contains(req.Portals, p => p.PortalName == "Portal2"); + } + + [Fact(DisplayName = "Method chaining works across multiple extensions")] + public void MethodChaining_WorksAcrossMultipleExtensions() + { + var req = new FindRequest(); + + req.UseLayout("Users") + .AddCriteria(new User { Name = "test" }, false) + .SetLimit(10) + .SetOffset(5) + .AddSortFieldDirection("Name", "ascend") + .SetScript("PostScript", "postParam") + .SetPreRequestScript("PreScript") + .SetPreSortScript("SortScript") + .LoadContainers(); + + Assert.Equal("Users", req.Layout); + Assert.Single(req.Query); + Assert.Equal(10, req.Limit); + Assert.Equal(5, req.Offset); + Assert.Single(req.Sort); + Assert.Equal("PostScript", req.Script); + Assert.Equal("PreScript", req.PreRequestScript); + Assert.Equal("SortScript", req.PreSortScript); + Assert.True(req.LoadContainerData); + } + + #endregion + + #region CreateRequest Extensions + + [Fact(DisplayName = "CreateRequest SetData sets data and returns same request")] + public void CreateRequest_SetData_SetsDataAndReturnsSameRequest() + { + var req = new CreateRequest(); + var user = new User { Name = "test" }; + var result = req.SetData(user); + + Assert.Same(req, result); + Assert.Same(user, req.Data); + } + + [Fact(DisplayName = "CreateRequest UseLayout with string sets layout")] + public void CreateRequest_UseLayout_String_SetsLayout() + { + var req = new CreateRequest(); + var result = req.UseLayout("CustomLayout"); + + Assert.Same(req, result); + Assert.Equal("CustomLayout", req.Layout); + } + + [Fact(DisplayName = "CreateRequest UseLayout with instance infers layout")] + public void CreateRequest_UseLayout_Instance_InfersLayout() + { + var req = new CreateRequest(); + var result = req.UseLayout(new User()); + + Assert.Same(req, result); + Assert.Equal("Users", req.Layout); + } + + [Fact(DisplayName = "CreateRequest SetScript sets script name and parameter")] + public void CreateRequest_SetScript_SetsScriptNameAndParameter() + { + var req = new CreateRequest { Layout = "Users" }; + var result = req.SetScript("MyScript", "param1"); + + Assert.Same(req, result); + Assert.Equal("MyScript", req.Script); + Assert.Equal("param1", req.ScriptParameter); + } + + [Fact(DisplayName = "CreateRequest SetPreRequestScript sets values")] + public void CreateRequest_SetPreRequestScript_SetsValues() + { + var req = new CreateRequest { Layout = "Users" }; + var result = req.SetPreRequestScript("PreScript", "preParam"); + + Assert.Same(req, result); + Assert.Equal("PreScript", req.PreRequestScript); + Assert.Equal("preParam", req.PreRequestScriptParameter); + } + + [Fact(DisplayName = "CreateRequest SetPreSortScript sets values")] + public void CreateRequest_SetPreSortScript_SetsValues() + { + var req = new CreateRequest { Layout = "Users" }; + var result = req.SetPreSortScript("SortScript", "sortParam"); + + Assert.Same(req, result); + Assert.Equal("SortScript", req.PreSortScript); + Assert.Equal("sortParam", req.PreSortScriptParameter); + } + + #endregion + + #region EditRequest Extensions + + [Fact(DisplayName = "EditRequest SetData sets data and returns same request")] + public void EditRequest_SetData_SetsDataAndReturnsSameRequest() + { + var req = new EditRequest { Layout = "Users", RecordId = 1 }; + var user = new User { Name = "updated" }; + var result = req.SetData(user); + + Assert.Same(req, result); + Assert.Same(user, req.Data); + } + + [Fact(DisplayName = "EditRequest UseLayout with string sets layout")] + public void EditRequest_UseLayout_String_SetsLayout() + { + var req = new EditRequest(); + var result = req.UseLayout("CustomLayout"); + + Assert.Same(req, result); + Assert.Equal("CustomLayout", req.Layout); + } + + [Fact(DisplayName = "EditRequest UseLayout with instance infers layout")] + public void EditRequest_UseLayout_Instance_InfersLayout() + { + var req = new EditRequest(); + var result = req.UseLayout(new User()); + + Assert.Same(req, result); + Assert.Equal("Users", req.Layout); + } + + [Fact(DisplayName = "EditRequest SetScript sets script name and parameter")] + public void EditRequest_SetScript_SetsScriptNameAndParameter() + { + var req = new EditRequest { Layout = "Users", RecordId = 1 }; + var result = req.SetScript("MyScript", "param1"); + + Assert.Same(req, result); + Assert.Equal("MyScript", req.Script); + Assert.Equal("param1", req.ScriptParameter); + } + + [Fact(DisplayName = "EditRequest SetPreRequestScript sets values")] + public void EditRequest_SetPreRequestScript_SetsValues() + { + var req = new EditRequest { Layout = "Users", RecordId = 1 }; + var result = req.SetPreRequestScript("PreScript", "preParam"); + + Assert.Same(req, result); + Assert.Equal("PreScript", req.PreRequestScript); + Assert.Equal("preParam", req.PreRequestScriptParameter); + } + + [Fact(DisplayName = "EditRequest SetPreSortScript sets values")] + public void EditRequest_SetPreSortScript_SetsValues() + { + var req = new EditRequest { Layout = "Users", RecordId = 1 }; + var result = req.SetPreSortScript("SortScript", "sortParam"); + + Assert.Same(req, result); + Assert.Equal("SortScript", req.PreSortScript); + Assert.Equal("sortParam", req.PreSortScriptParameter); + } + + #endregion + + #region IFileMakerRequest (Obsolete) Extensions + + [Fact(DisplayName = "RequestExtensions SetScript on IFileMakerRequest sets values")] + public void RequestExtensions_SetScript_OnIFileMakerRequest() + { + IFileMakerRequest req = new FindRequest { Layout = "Users" }; + +#pragma warning disable CS0612 // intentionally testing the obsolete extension + var result = req.SetScript("MyScript", "param1"); +#pragma warning restore CS0612 + + Assert.Same(req, result); + Assert.Equal("MyScript", req.Script); + Assert.Equal("param1", req.ScriptParameter); + } + + [Fact(DisplayName = "RequestExtensions SetPreRequestScript on IFileMakerRequest sets values")] + public void RequestExtensions_SetPreRequestScript_OnIFileMakerRequest() + { + IFileMakerRequest req = new CreateRequest { Layout = "Users" }; + +#pragma warning disable CS0612 // intentionally testing the obsolete extension + var result = req.SetPreRequestScript("PreScript", "preParam"); +#pragma warning restore CS0612 + + Assert.Same(req, result); + Assert.Equal("PreScript", req.PreRequestScript); + Assert.Equal("preParam", req.PreRequestScriptParameter); + } + + [Fact(DisplayName = "RequestExtensions SetPreSortScript on IFileMakerRequest sets values")] + public void RequestExtensions_SetPreSortScript_OnIFileMakerRequest() + { + IFileMakerRequest req = new EditRequest { Layout = "Users", RecordId = 1 }; + +#pragma warning disable CS0612 // intentionally testing the obsolete extension + var result = req.SetPreSortScript("SortScript", "sortParam"); +#pragma warning restore CS0612 + + Assert.Same(req, result); + Assert.Equal("SortScript", req.PreSortScript); + Assert.Equal("sortParam", req.PreSortScriptParameter); + } + + #endregion + + #region PortalBuilder + + [Fact(DisplayName = "WithPortal creates PortalBuilder and adds portal to request")] + public void WithPortal_CreatesPortalBuilderAndAddsPortal() + { + var req = new FindRequest { Layout = "Users" }; + var builder = req.WithPortal("RelatedTable"); + + Assert.NotNull(builder); + Assert.Single(req.Portals); + Assert.Equal("RelatedTable", req.Portals.First().PortalName); + } + + [Fact(DisplayName = "PortalBuilder Limit sets portal limit")] + public void PortalBuilder_Limit_SetsPortalLimit() + { + var req = new FindRequest { Layout = "Users" }; + req.WithPortal("RelatedTable").Limit(10); + + var portal = req.Portals.First(p => p.PortalName == "RelatedTable"); + Assert.Equal(10, portal.Limit); + } + + [Fact(DisplayName = "PortalBuilder Offset sets portal offset")] + public void PortalBuilder_Offset_SetsPortalOffset() + { + var req = new FindRequest { Layout = "Users" }; + req.WithPortal("RelatedTable").Offset(5); + + var portal = req.Portals.First(p => p.PortalName == "RelatedTable"); + Assert.Equal(5, portal.Offset); + } + + [Fact(DisplayName = "PortalBuilder chaining sets both limit and offset")] + public void PortalBuilder_Chaining_SetsBothLimitAndOffset() + { + var req = new FindRequest { Layout = "Users" }; + req.WithPortal("RelatedTable").Limit(10).Offset(5); + + var portal = req.Portals.First(p => p.PortalName == "RelatedTable"); + Assert.Equal(10, portal.Limit); + Assert.Equal(5, portal.Offset); + } + + [Fact(DisplayName = "PortalBuilder WithPortal chains to another portal")] + public void PortalBuilder_WithPortal_ChainsToAnotherPortal() + { + var req = new FindRequest { Layout = "Users" }; + + req.WithPortal("Portal1").Limit(10) + .WithPortal("Portal2").Offset(3); + + Assert.Equal(2, req.Portals.Count); + + var portal1 = req.Portals.First(p => p.PortalName == "Portal1"); + Assert.Equal(10, portal1.Limit); + + var portal2 = req.Portals.First(p => p.PortalName == "Portal2"); + Assert.Equal(3, portal2.Offset); + } + + [Fact(DisplayName = "Portal configuration serializes correctly")] + public void Portal_Configuration_SerializesCorrectly() + { + var req = new FindRequest { Layout = "Users" }; + req.AddQuery(new User { Name = "test" }, false); + req.WithPortal("RelatedRecords").Limit(5).Offset(2); + + var json = req.SerializeRequest(); + + Assert.Contains("\"portal\":[\"RelatedRecords\"]", json); + Assert.Contains("\"limit.RelatedRecords\":5", json); + Assert.Contains("\"offset.RelatedRecords\":2", json); + } + + [Fact(DisplayName = "Multiple portals serialize correctly")] + public void MultiplePortals_SerializeCorrectly() + { + var req = new FindRequest { Layout = "Users" }; + req.AddQuery(new User { Name = "test" }, false); + + req.WithPortal("Portal1").Limit(10) + .WithPortal("Portal2").Limit(20).Offset(5); + + var json = req.SerializeRequest(); + + Assert.Contains("Portal1", json); + Assert.Contains("Portal2", json); + Assert.Contains("\"limit.Portal1\":10", json); + Assert.Contains("\"limit.Portal2\":20", json); + Assert.Contains("\"offset.Portal2\":5", json); + } + + #endregion + } +} diff --git a/tests/FMData.Rest.Tests/TokenRetryTests.cs b/tests/FMData.Rest.Tests/TokenRetryTests.cs index a681349..156f57d 100644 --- a/tests/FMData.Rest.Tests/TokenRetryTests.cs +++ b/tests/FMData.Rest.Tests/TokenRetryTests.cs @@ -43,7 +43,9 @@ public async Task FindAsync_ShouldRetry_OnUnauthorized() using var fdc = new FileMakerRestClient(mockHttp.ToHttpClient(), new ConnectionInfo { FmsUri = Server, Database = File, Username = User, Password = Pass }); +#pragma warning disable CS0618 // intentionally testing the obsolete overload var response = await fdc.FindAsync(Layout, new Dictionary { { "Name", "test" } }); +#pragma warning restore CS0618 Assert.NotNull(response); mockHttp.VerifyNoOutstandingExpectation();