Skip to content

Commit b10d3d0

Browse files
authored
Merge pull request #38 from joadan/copilot/add-tests-for-singleton
Add tests for SingletonBuilder
2 parents 9903990 + 754b0c9 commit b10d3d0

1 file changed

Lines changed: 290 additions & 0 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
using Linq2OData.Core;
2+
using Linq2OData.Core.Builders;
3+
using System.Net;
4+
5+
namespace Linq2OData.Tests;
6+
7+
/// <summary>
8+
/// Tests for the SingletonBuilder class, which builds requests for OData singleton entities.
9+
/// </summary>
10+
public class SingletonBuilderTests
11+
{
12+
[ODataEntitySet("Persons")]
13+
public class TestPerson : IODataEntitySet
14+
{
15+
[ODataMember("ID")]
16+
public int ID { get; set; }
17+
18+
[ODataMember("Name")]
19+
public string? Name { get; set; }
20+
21+
[ODataMember("Email")]
22+
public string? Email { get; set; }
23+
24+
[ODataMember("Address", isComplex: true)]
25+
public TestAddress? Address { get; set; }
26+
27+
public string __Key => $"ID={ID}";
28+
}
29+
30+
[ODataEntitySet("Addresses")]
31+
public class TestAddress : IODataEntitySet
32+
{
33+
[ODataMember("City")]
34+
public string? City { get; set; }
35+
36+
public string __Key => "";
37+
}
38+
39+
private (SingletonBuilder<TestPerson> builder, MockHttpHandler handler) CreateBuilder(
40+
ODataVersion version = ODataVersion.V4,
41+
string responseBody = "{}",
42+
HttpStatusCode statusCode = HttpStatusCode.OK)
43+
{
44+
var handler = new MockHttpHandler(responseBody, statusCode);
45+
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://example.com/odata/") };
46+
var odataClient = new ODataClient(httpClient, version);
47+
return (new SingletonBuilder<TestPerson>(odataClient, "Me"), handler);
48+
}
49+
50+
#region Builder State Tests
51+
52+
[Fact]
53+
public void SingletonBuilder_Select_SetsSelectField()
54+
{
55+
// Arrange
56+
var (builder, _) = CreateBuilder();
57+
58+
// Act
59+
var result = builder.Select("Name,Email");
60+
61+
// Assert
62+
Assert.Equal("Name,Email", builder.select);
63+
Assert.Same(builder, result);
64+
}
65+
66+
[Fact]
67+
public void SingletonBuilder_Expand_SetsExpandField()
68+
{
69+
// Arrange
70+
var (builder, _) = CreateBuilder();
71+
72+
// Act
73+
var result = builder.Expand("Address");
74+
75+
// Assert
76+
Assert.Equal("Address", builder.expand);
77+
Assert.Same(builder, result);
78+
}
79+
80+
[Fact]
81+
public void SingletonBuilder_SelectNull_SetsSelectFieldToNull()
82+
{
83+
// Arrange
84+
var (builder, _) = CreateBuilder();
85+
builder.Select("Name");
86+
87+
// Act
88+
builder.Select(null);
89+
90+
// Assert
91+
Assert.Null(builder.select);
92+
}
93+
94+
[Fact]
95+
public void SingletonBuilder_ExpandNull_SetsExpandFieldToNull()
96+
{
97+
// Arrange
98+
var (builder, _) = CreateBuilder();
99+
builder.Expand("Address");
100+
101+
// Act
102+
builder.Expand(null);
103+
104+
// Assert
105+
Assert.Null(builder.expand);
106+
}
107+
108+
[Fact]
109+
public void SingletonBuilder_ChainSelectAndExpand_SetsBothFields()
110+
{
111+
// Arrange
112+
var (builder, _) = CreateBuilder();
113+
114+
// Act
115+
builder.Select("Name").Expand("Address");
116+
117+
// Assert
118+
Assert.Equal("Name", builder.select);
119+
Assert.Equal("Address", builder.expand);
120+
}
121+
122+
[Fact]
123+
public void SingletonBuilder_DefaultState_HasNullSelectAndExpand()
124+
{
125+
// Arrange / Act
126+
var (builder, _) = CreateBuilder();
127+
128+
// Assert
129+
Assert.Null(builder.select);
130+
Assert.Null(builder.expand);
131+
}
132+
133+
#endregion
134+
135+
#region URL Generation Tests
136+
137+
[Fact]
138+
public async Task SingletonBuilder_ExecuteAsync_NoSelectOrExpand_RequestsCorrectUrl()
139+
{
140+
// Arrange
141+
var json = """{"@odata.context":"https://example.com/odata/$metadata#Me","ID":1,"Name":"John"}""";
142+
var (builder, handler) = CreateBuilder(ODataVersion.V4, json);
143+
144+
// Act
145+
await builder.ExecuteAsync();
146+
147+
// Assert - singleton path with no query string (no key expression)
148+
Assert.Equal("/odata/Me", handler.LastRequestPath);
149+
Assert.Empty(handler.LastRequestQuery);
150+
}
151+
152+
[Fact]
153+
public async Task SingletonBuilder_ExecuteAsync_WithSelect_IncludesSelectInUrl()
154+
{
155+
// Arrange
156+
var json = """{"@odata.context":"https://example.com/odata/$metadata#Me","Name":"John"}""";
157+
var (builder, handler) = CreateBuilder(ODataVersion.V4, json);
158+
builder.Select("Name,Email");
159+
160+
// Act
161+
await builder.ExecuteAsync();
162+
163+
// Assert
164+
Assert.Equal("/odata/Me", handler.LastRequestPath);
165+
Assert.Contains("$select=Name,Email", handler.LastRequestQuery);
166+
}
167+
168+
[Fact]
169+
public async Task SingletonBuilder_ExecuteAsync_WithExpand_IncludesExpandInUrl()
170+
{
171+
// Arrange
172+
var json = """{"@odata.context":"...","Name":"John","Address":{"City":"NYC"}}""";
173+
var (builder, handler) = CreateBuilder(ODataVersion.V4, json);
174+
builder.Expand("Address");
175+
176+
// Act
177+
await builder.ExecuteAsync();
178+
179+
// Assert
180+
Assert.Equal("/odata/Me", handler.LastRequestPath);
181+
Assert.Contains("$expand=", handler.LastRequestQuery);
182+
Assert.Contains("Address", handler.LastRequestQuery);
183+
}
184+
185+
[Fact]
186+
public async Task SingletonBuilder_ExecuteAsync_WithSelectAndExpand_IncludesBothInUrl()
187+
{
188+
// Arrange
189+
var json = """{"@odata.context":"...","Name":"John","Address":{"City":"NYC"}}""";
190+
var (builder, handler) = CreateBuilder(ODataVersion.V4, json);
191+
builder.Select("Name").Expand("Address");
192+
193+
// Act
194+
await builder.ExecuteAsync();
195+
196+
// Assert
197+
Assert.Equal("/odata/Me", handler.LastRequestPath);
198+
Assert.Contains("$select=Name", handler.LastRequestQuery);
199+
Assert.Contains("$expand=", handler.LastRequestQuery);
200+
}
201+
202+
[Fact]
203+
public async Task SingletonBuilder_ExecuteAsync_DoesNotAppendKeyExpression()
204+
{
205+
// Arrange - singletons have no key, so the URL must NOT contain "(..."
206+
var json = """{"@odata.context":"...","ID":1,"Name":"John"}""";
207+
var (builder, handler) = CreateBuilder(ODataVersion.V4, json);
208+
209+
// Act
210+
await builder.ExecuteAsync();
211+
212+
// Assert - no parentheses for key
213+
Assert.DoesNotContain("(", handler.LastRequestPath);
214+
}
215+
216+
#endregion
217+
218+
#region Deserialization Tests
219+
220+
[Fact]
221+
public async Task SingletonBuilder_ExecuteAsync_V4Response_DeserializesCorrectly()
222+
{
223+
// Arrange - OData V4 singleton response (entity at root, no "value" wrapper)
224+
var json = """{"@odata.context":"https://example.com/odata/$metadata#Me","ID":42,"Name":"Jane Doe","Email":"jane@example.com"}""";
225+
var (builder, _) = CreateBuilder(ODataVersion.V4, json);
226+
227+
// Act
228+
var result = await builder.ExecuteAsync();
229+
230+
// Assert
231+
Assert.NotNull(result);
232+
Assert.Equal(42, result.ID);
233+
Assert.Equal("Jane Doe", result.Name);
234+
Assert.Equal("jane@example.com", result.Email);
235+
}
236+
237+
[Fact]
238+
public async Task SingletonBuilder_ExecuteAsync_V4ResponseWithExpand_DeserializesNestedEntity()
239+
{
240+
// Arrange
241+
var json = """{"@odata.context":"...","ID":1,"Name":"John","Address":{"City":"Seattle"}}""";
242+
var (builder, _) = CreateBuilder(ODataVersion.V4, json);
243+
builder.Expand("Address");
244+
245+
// Act
246+
var result = await builder.ExecuteAsync();
247+
248+
// Assert
249+
Assert.NotNull(result);
250+
Assert.Equal(1, result.ID);
251+
Assert.Equal("John", result.Name);
252+
Assert.NotNull(result.Address);
253+
Assert.Equal("Seattle", result.Address.City);
254+
}
255+
256+
[Fact]
257+
public async Task SingletonBuilder_ExecuteAsync_NotFound_ReturnsNull()
258+
{
259+
// Arrange
260+
var (builder, _) = CreateBuilder(ODataVersion.V4, "", HttpStatusCode.NotFound);
261+
262+
// Act
263+
var result = await builder.ExecuteAsync();
264+
265+
// Assert
266+
Assert.Null(result);
267+
}
268+
269+
#endregion
270+
271+
/// <summary>
272+
/// A simple HTTP handler that captures request details and returns a fixed response.
273+
/// </summary>
274+
private class MockHttpHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK) : HttpMessageHandler
275+
{
276+
public string LastRequestPath { get; private set; } = "";
277+
public string LastRequestQuery { get; private set; } = "";
278+
279+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
280+
{
281+
LastRequestPath = request.RequestUri?.AbsolutePath ?? "";
282+
LastRequestQuery = request.RequestUri?.Query ?? "";
283+
284+
return Task.FromResult(new HttpResponseMessage(statusCode)
285+
{
286+
Content = new StringContent(responseBody, System.Text.Encoding.UTF8, "application/json")
287+
});
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)