Skip to content

Commit 4e04c3a

Browse files
feat: Add ODP GraphQL Manager and Tests (#310)
* Initial GraphQLManager tests * Correct GraphQLManagerTest.cs * Add GraphQL manager and supporting entities * Fully cover json response * Adding GQLManager interface for testing * Tests for common scenarios Swift ref * Fix compiler error * Refactors * WIP Handle versions of .NET * WebRequest and HttpClient ODP clients * Only support NET Standard; Inject & unit test * Filled unit tests * Copyright notices and ending line * Corrections and simplifications * Corrections logged messages & supporting tests * Remove excess validations * Add unexpected node; Fix test * Update OptimizelySDK.Tests/OdpTests/GraphQLManagerTest.cs Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Update OptimizelySDK/Odp/Client/OdpClient.cs Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Update OptimizelySDK/Odp/Client/OdpClient.cs Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Enhanced error handling and matching tests * Add end of file lines * Add class internal documentation * Add line at EOF * Refactoring GraphQLManager * Refactor OdpClient * WIP Code review edits * Update doc * QuerySegmentsParameters Builder * Documentation * Document constructors * Code review changes * Adjust sync-to-async call methodology Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
1 parent 08b97d2 commit 4e04c3a

17 files changed

+1222
-3
lines changed
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Copyright 2022 Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Moq;
18+
using Moq.Protected;
19+
using NUnit.Framework;
20+
using OptimizelySDK.AudienceConditions;
21+
using OptimizelySDK.ErrorHandler;
22+
using OptimizelySDK.Logger;
23+
using OptimizelySDK.Odp;
24+
using OptimizelySDK.Odp.Client;
25+
using OptimizelySDK.Odp.Entity;
26+
using System.Collections.Generic;
27+
using System.Net;
28+
using System.Net.Http;
29+
using System.Threading;
30+
using System.Threading.Tasks;
31+
32+
namespace OptimizelySDK.Tests.OdpTests
33+
{
34+
[TestFixture]
35+
public class GraphQLManagerTest
36+
{
37+
private const string VALID_ODP_PUBLIC_KEY = "not-real-odp-public-key";
38+
private const string ODP_GRAPHQL_URL = "https://example.com/endpoint";
39+
private const string FS_USER_ID = "fs_user_id";
40+
41+
private readonly List<string> _segmentsToCheck = new List<string>
42+
{
43+
"has_email",
44+
"has_email_opted_in",
45+
"push_on_sale",
46+
};
47+
48+
private Mock<IErrorHandler> _mockErrorHandler;
49+
private Mock<ILogger> _mockLogger;
50+
private Mock<IOdpClient> _mockOdpClient;
51+
52+
[SetUp]
53+
public void Setup()
54+
{
55+
_mockErrorHandler = new Mock<IErrorHandler>();
56+
_mockLogger = new Mock<ILogger>();
57+
_mockLogger.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
58+
59+
_mockOdpClient = new Mock<IOdpClient>();
60+
}
61+
62+
[Test]
63+
public void ShouldParseSuccessfulResponse()
64+
{
65+
const string RESPONSE_JSON = @"
66+
{
67+
""data"": {
68+
""customer"": {
69+
""audiences"": {
70+
""edges"": [
71+
{
72+
""node"": {
73+
""name"": ""has_email"",
74+
""state"": ""qualified"",
75+
}
76+
},
77+
{
78+
""node"": {
79+
""name"": ""has_email_opted_in"",
80+
""state"": ""not-qualified""
81+
}
82+
},
83+
]
84+
},
85+
}
86+
}
87+
}";
88+
89+
var response = GraphQLManager.ParseSegmentsResponseJson(RESPONSE_JSON);
90+
91+
Assert.IsNull(response.Errors);
92+
Assert.IsNotNull(response.Data);
93+
Assert.IsNotNull(response.Data.Customer);
94+
Assert.IsNotNull(response.Data.Customer.Audiences);
95+
Assert.IsNotNull(response.Data.Customer.Audiences.Edges);
96+
Assert.IsTrue(response.Data.Customer.Audiences.Edges.Length == 2);
97+
var node = response.Data.Customer.Audiences.Edges[0].Node;
98+
Assert.AreEqual(node.Name, "has_email");
99+
Assert.AreEqual(node.State, BaseCondition.QUALIFIED);
100+
node = response.Data.Customer.Audiences.Edges[1].Node;
101+
Assert.AreEqual(node.Name, "has_email_opted_in");
102+
Assert.AreNotEqual(node.State, BaseCondition.QUALIFIED);
103+
}
104+
105+
[Test]
106+
public void ShouldParseErrorResponse()
107+
{
108+
const string RESPONSE_JSON = @"
109+
{
110+
""errors"": [
111+
{
112+
""message"": ""Exception while fetching data (/customer) : Exception: could not resolve _fs_user_id = asdsdaddddd"",
113+
""locations"": [
114+
{
115+
""line"": 2,
116+
""column"": 3
117+
}
118+
],
119+
""path"": [
120+
""customer""
121+
],
122+
""extensions"": {
123+
""classification"": ""InvalidIdentifierException""
124+
}
125+
}
126+
],
127+
""data"": {
128+
""customer"": null
129+
}
130+
}";
131+
132+
var response = GraphQLManager.ParseSegmentsResponseJson(RESPONSE_JSON);
133+
134+
Assert.IsNull(response.Data.Customer);
135+
Assert.IsNotNull(response.Errors);
136+
Assert.AreEqual(response.Errors[0].Extensions.Classification,
137+
"InvalidIdentifierException");
138+
}
139+
140+
[Test]
141+
public void ShouldFetchValidQualifiedSegments()
142+
{
143+
const string RESPONSE_DATA = "{\"data\":{\"customer\":{\"audiences\":" +
144+
"{\"edges\":[{\"node\":{\"name\":\"has_email\"," +
145+
"\"state\":\"qualified\"}},{\"node\":{\"name\":" +
146+
"\"has_email_opted_in\",\"state\":\"qualified\"}}]}}}}";
147+
_mockOdpClient.Setup(
148+
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())).
149+
Returns(RESPONSE_DATA);
150+
var manager = new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object,
151+
_mockOdpClient.Object);
152+
153+
var segments = manager.FetchSegments(
154+
VALID_ODP_PUBLIC_KEY,
155+
ODP_GRAPHQL_URL,
156+
FS_USER_ID,
157+
"tester-101",
158+
_segmentsToCheck);
159+
160+
Assert.IsTrue(segments.Length == 2);
161+
Assert.Contains("has_email", segments);
162+
Assert.Contains("has_email_opted_in", segments);
163+
_mockLogger.Verify(l => l.Log(LogLevel.WARN, It.IsAny<string>()), Times.Never);
164+
}
165+
166+
[Test]
167+
public void ShouldHandleEmptyQualifiedSegments()
168+
{
169+
const string RESPONSE_DATA = "{\"data\":{\"customer\":{\"audiences\":" +
170+
"{\"edges\":[ ]}}}}";
171+
_mockOdpClient.Setup(
172+
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())).
173+
Returns(RESPONSE_DATA);
174+
var manager = new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object,
175+
_mockOdpClient.Object);
176+
177+
var segments = manager.FetchSegments(
178+
VALID_ODP_PUBLIC_KEY,
179+
ODP_GRAPHQL_URL,
180+
FS_USER_ID,
181+
"tester-101",
182+
_segmentsToCheck);
183+
184+
Assert.IsTrue(segments.Length == 0);
185+
_mockLogger.Verify(l => l.Log(LogLevel.WARN, It.IsAny<string>()), Times.Never);
186+
}
187+
188+
[Test]
189+
public void ShouldHandleErrorWithInvalidIdentifier()
190+
{
191+
const string RESPONSE_DATA = "{\"errors\":[{\"message\":" +
192+
"\"Exception while fetching data (/customer) : " +
193+
"Exception: could not resolve _fs_user_id = invalid-user\"," +
194+
"\"locations\":[{\"line\":1,\"column\":8}],\"path\":[\"customer\"]," +
195+
"\"extensions\":{\"classification\":\"DataFetchingException\"}}]," +
196+
"\"data\":{\"customer\":null}}";
197+
_mockOdpClient.Setup(
198+
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())).
199+
Returns(RESPONSE_DATA);
200+
var manager = new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object,
201+
_mockOdpClient.Object);
202+
203+
var segments = manager.FetchSegments(
204+
VALID_ODP_PUBLIC_KEY,
205+
ODP_GRAPHQL_URL,
206+
FS_USER_ID,
207+
"invalid-user",
208+
_segmentsToCheck);
209+
210+
Assert.IsTrue(segments.Length == 0);
211+
_mockLogger.Verify(l => l.Log(LogLevel.WARN, It.IsAny<string>()),
212+
Times.Once);
213+
}
214+
215+
[Test]
216+
public void ShouldHandleOtherException()
217+
{
218+
const string RESPONSE_DATA = "{\"errors\":[{\"message\":\"Validation error of type " +
219+
"UnknownArgument: Unknown field argument not_real_userKey @ " +
220+
"'customer'\",\"locations\":[{\"line\":1,\"column\":17}]," +
221+
"\"extensions\":{\"classification\":\"ValidationError\"}}]}";
222+
223+
_mockOdpClient.Setup(
224+
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())).
225+
Returns(RESPONSE_DATA);
226+
var manager = new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object,
227+
_mockOdpClient.Object);
228+
229+
var segments = manager.FetchSegments(
230+
VALID_ODP_PUBLIC_KEY,
231+
ODP_GRAPHQL_URL,
232+
"not_real_userKey",
233+
"tester-101",
234+
_segmentsToCheck);
235+
236+
Assert.IsTrue(segments.Length == 0);
237+
_mockLogger.Verify(l => l.Log(LogLevel.WARN, It.IsAny<string>()), Times.Once);
238+
}
239+
240+
[Test]
241+
public void ShouldHandleBadResponse()
242+
{
243+
const string RESPONSE_DATA = "{\"data\":{ }}";
244+
_mockOdpClient.Setup(
245+
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())).
246+
Returns(RESPONSE_DATA);
247+
var manager = new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object,
248+
_mockOdpClient.Object);
249+
250+
var segments = manager.FetchSegments(
251+
VALID_ODP_PUBLIC_KEY,
252+
ODP_GRAPHQL_URL,
253+
"not_real_userKey",
254+
"tester-101",
255+
_segmentsToCheck);
256+
257+
Assert.IsTrue(segments.Length == 0);
258+
_mockLogger.Verify(
259+
l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (decode error)"),
260+
Times.Once);
261+
}
262+
263+
[Test]
264+
public void ShouldHandleUnrecognizedJsonResponse()
265+
{
266+
const string RESPONSE_DATA =
267+
"{\"unExpectedObject\":{ \"withSome\": \"value\", \"thatIsNotParseable\": \"true\" }}";
268+
_mockOdpClient.Setup(
269+
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())).
270+
Returns(RESPONSE_DATA);
271+
var manager = new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object,
272+
_mockOdpClient.Object);
273+
274+
var segments = manager.FetchSegments(
275+
VALID_ODP_PUBLIC_KEY,
276+
ODP_GRAPHQL_URL,
277+
"not_real_userKey",
278+
"tester-101",
279+
_segmentsToCheck);
280+
281+
Assert.IsTrue(segments.Length == 0);
282+
_mockLogger.Verify(
283+
l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (decode error)"),
284+
Times.Once);
285+
}
286+
287+
[Test]
288+
public void ShouldHandle400HttpCode()
289+
{
290+
var odpClient = new OdpClient(_mockErrorHandler.Object, _mockLogger.Object,
291+
GetHttpClientThatReturnsStatus(HttpStatusCode.BadRequest));
292+
var manager =
293+
new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object, odpClient);
294+
295+
var segments = manager.FetchSegments(
296+
VALID_ODP_PUBLIC_KEY,
297+
ODP_GRAPHQL_URL,
298+
FS_USER_ID,
299+
"tester-101",
300+
_segmentsToCheck);
301+
302+
Assert.IsTrue(segments.Length == 0);
303+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (400)"),
304+
Times.Once);
305+
}
306+
307+
[Test]
308+
public void ShouldHandle500HttpCode()
309+
{
310+
var odpClient = new OdpClient(_mockErrorHandler.Object, _mockLogger.Object,
311+
GetHttpClientThatReturnsStatus(HttpStatusCode.InternalServerError));
312+
var manager =
313+
new GraphQLManager(_mockErrorHandler.Object, _mockLogger.Object, odpClient);
314+
315+
var segments = manager.FetchSegments(
316+
VALID_ODP_PUBLIC_KEY,
317+
ODP_GRAPHQL_URL,
318+
FS_USER_ID,
319+
"tester-101",
320+
_segmentsToCheck);
321+
322+
Assert.IsTrue(segments.Length == 0);
323+
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (500)"),
324+
Times.Once);
325+
}
326+
327+
private static HttpClient GetHttpClientThatReturnsStatus(HttpStatusCode statusCode)
328+
{
329+
var mockedHandler = new Mock<HttpMessageHandler>();
330+
mockedHandler.Protected().Setup<Task<HttpResponseMessage>>(
331+
"SendAsync",
332+
ItExpr.IsAny<HttpRequestMessage>(),
333+
ItExpr.IsAny<CancellationToken>()).
334+
ReturnsAsync(() => new HttpResponseMessage(statusCode));
335+
return new HttpClient(mockedHandler.Object);
336+
}
337+
}
338+
}

OptimizelySDK.Tests/OptimizelySDK.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
<Compile Include="DefaultErrorHandlerTest.cs" />
8181
<Compile Include="EntityTests\IntegrationTest.cs" />
8282
<Compile Include="EventTests\EventProcessorProps.cs" />
83+
<Compile Include="OdpTests\GraphQLManagerTest.cs" />
8384
<Compile Include="OdpTests\LruCacheTest.cs" />
8485
<Compile Include="OptimizelyConfigTests\OptimizelyConfigTest.cs" />
8586
<Compile Include="OptimizelyDecisions\OptimizelyDecisionTest.cs" />
@@ -159,4 +160,4 @@
159160
<Target Name="AfterBuild">
160161
</Target>
161162
-->
162-
</Project>
163+
</Project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2022 Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using OptimizelySDK.Odp.Entity;
18+
19+
namespace OptimizelySDK.Odp.Client
20+
{
21+
/// <summary>
22+
/// An implementation for sending requests and handling responses to Optimizely Data Platform
23+
/// </summary>
24+
public interface IOdpClient
25+
{
26+
/// <summary>
27+
/// Synchronous handler for querying the ODP GraphQL endpoint
28+
/// </summary>
29+
/// <param name="parameters">Parameters inputs to send to ODP</param>
30+
/// <returns>JSON response from ODP</returns>
31+
string QuerySegments(QuerySegmentsParameters parameters);
32+
}
33+
}

0 commit comments

Comments
 (0)