-
Notifications
You must be signed in to change notification settings - Fork 20
feat: Add ODP GraphQL Manager and Tests #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
f7e0138
Initial GraphQLManager tests
mikechu-optimizely 4536719
Correct GraphQLManagerTest.cs
mikechu-optimizely f2b4e79
Add GraphQL manager and supporting entities
mikechu-optimizely 1e579ee
Fully cover json response
mikechu-optimizely d2df4a0
Adding GQLManager interface for testing
mikechu-optimizely 368817f
Tests for common scenarios Swift ref
mikechu-optimizely 728e1a8
Fix compiler error
mikechu-optimizely 5e08827
Refactors
mikechu-optimizely 5909f8e
WIP Handle versions of .NET
mikechu-optimizely 7d35390
Merge branch 'master' into mike/ats-graphql
mikechu-optimizely 5664b75
WebRequest and HttpClient ODP clients
mikechu-optimizely 0e96ee1
Only support NET Standard; Inject & unit test
mikechu-optimizely 3b267f3
Filled unit tests
mikechu-optimizely ed3f049
Copyright notices and ending line
mikechu-optimizely 06a61a5
Corrections and simplifications
mikechu-optimizely 705ce61
Corrections logged messages & supporting tests
mikechu-optimizely 9a7ef8f
Remove excess validations
mikechu-optimizely dd91fce
Add unexpected node; Fix test
mikechu-optimizely 96b84fa
Update OptimizelySDK.Tests/OdpTests/GraphQLManagerTest.cs
mikechu-optimizely 0f444df
Update OptimizelySDK/Odp/Client/OdpClient.cs
mikechu-optimizely 8c74e43
Update OptimizelySDK/Odp/Client/OdpClient.cs
mikechu-optimizely 6866c82
Enhanced error handling and matching tests
mikechu-optimizely 2b2b65d
Add end of file lines
mikechu-optimizely cd4e8f1
Add class internal documentation
mikechu-optimizely d58e683
Add line at EOF
mikechu-optimizely 0ef9849
Merge branch 'master' into mike/ats-graphql
mikechu-optimizely 85b9fd3
Refactoring GraphQLManager
mikechu-optimizely f87d7f7
Refactor OdpClient
mikechu-optimizely c443b52
WIP Code review edits
mikechu-optimizely f57b438
Update doc
mikechu-optimizely 98ad25d
QuerySegmentsParameters Builder
mikechu-optimizely 2d76e3b
Documentation
mikechu-optimizely 20016f9
Document constructors
mikechu-optimizely 76e3e9e
Code review changes
mikechu-optimizely f5fc257
Adjust sync-to-async call methodology
mikechu-optimizely File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,322 @@ | ||
/* | ||
* Copyright 2022 Optimizely | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
using Moq; | ||
using Moq.Protected; | ||
using NUnit.Framework; | ||
using OptimizelySDK.AudienceConditions; | ||
using OptimizelySDK.Logger; | ||
using OptimizelySDK.Odp; | ||
using OptimizelySDK.Odp.Client; | ||
using OptimizelySDK.Odp.Entity; | ||
using System.Collections.Generic; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace OptimizelySDK.Tests.OdpTests | ||
{ | ||
[TestFixture] | ||
public class GraphQLManagerTest | ||
{ | ||
private const string VALID_ODP_PUBLIC_KEY = "W4WzcEs-ABgXorzY7h1LCQ"; | ||
private const string ODP_GRAPHQL_URL = "https://api.zaius.com/v3/graphql"; | ||
private const string FS_USER_ID = "fs_user_id"; | ||
|
||
private readonly List<string> _segmentsToCheck = new List<string>() | ||
mikechu-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
"has_email", | ||
"has_email_opted_in", | ||
"push_on_sale", | ||
}; | ||
|
||
private Mock<ILogger> _mockLogger; | ||
private Mock<IOdpClient> _mockOdpClient; | ||
|
||
[SetUp] | ||
public void Setup() | ||
{ | ||
_mockLogger = new Mock<ILogger>(); | ||
_mockLogger.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); | ||
|
||
_mockOdpClient = new Mock<IOdpClient>(); | ||
} | ||
|
||
[Test] | ||
public void ShouldParseSuccessfulResponse() | ||
{ | ||
var responseJson = @" | ||
{ | ||
""data"": { | ||
""customer"": { | ||
""audiences"": { | ||
""edges"": [ | ||
{ | ||
""node"": { | ||
""name"": ""has_email"", | ||
""state"": ""qualified"", | ||
} | ||
}, | ||
{ | ||
""node"": { | ||
""name"": ""has_email_opted_in"", | ||
""state"": ""qualified"", | ||
""unexpected"": ""node value for testing"", | ||
} | ||
mikechu-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
] | ||
}, | ||
} | ||
} | ||
}"; | ||
var manager = new GraphQLManager(_mockLogger.Object); | ||
|
||
var response = manager.ParseSegmentsResponseJson(responseJson); | ||
|
||
Assert.IsNull(response.Errors); | ||
Assert.IsNotNull(response.Data); | ||
Assert.IsNotNull(response.Data.Customer); | ||
Assert.IsNotNull(response.Data.Customer.Audiences); | ||
Assert.IsNotNull(response.Data.Customer.Audiences.Edges); | ||
Assert.IsTrue(response.Data.Customer.Audiences.Edges.Length == 2); | ||
var node = response.Data.Customer.Audiences.Edges[0].Node; | ||
Assert.IsTrue(node.Name == "has_email"); | ||
Assert.IsTrue(node.State == BaseCondition.QUALIFIED); | ||
node = response.Data.Customer.Audiences.Edges[1].Node; | ||
Assert.IsTrue(node.Name == "has_email_opted_in"); | ||
Assert.IsTrue(node.State == BaseCondition.QUALIFIED); | ||
} | ||
|
||
[Test] | ||
public void ShouldParseErrorResponse() | ||
{ | ||
const string responseJson = @" | ||
{ | ||
""errors"": [ | ||
{ | ||
""message"": ""Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd"", | ||
mikechu-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""locations"": [ | ||
{ | ||
""line"": 2, | ||
""column"": 3 | ||
} | ||
], | ||
""path"": [ | ||
""customer"" | ||
], | ||
""extensions"": { | ||
""classification"": ""InvalidIdentifierException"" | ||
} | ||
} | ||
], | ||
""data"": { | ||
""customer"": null | ||
} | ||
}"; | ||
var manager = new GraphQLManager(_mockLogger.Object); | ||
|
||
var response = manager.ParseSegmentsResponseJson(responseJson); | ||
|
||
Assert.IsNull(response.Data.Customer); | ||
Assert.IsNotNull(response.Errors); | ||
Assert.IsTrue(response.Errors[0].Extensions.Classification == "InvalidIdentifierException"); | ||
} | ||
|
||
[Test] | ||
public void ShouldFetchValidQualifiedSegments() | ||
{ | ||
var responseData = "{\"data\":{\"customer\":{\"audiences\":" + | ||
"{\"edges\":[{\"node\":{\"name\":\"has_email\"," + | ||
"\"state\":\"qualified\"}},{\"node\":{\"name\":" + | ||
"\"has_email_opted_in\",\"state\":\"qualified\"}}]}}}}"; | ||
_mockOdpClient.Setup( | ||
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())). | ||
Returns(responseData); | ||
var manager = new GraphQLManager(_mockLogger.Object, _mockOdpClient.Object); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
FS_USER_ID, | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 2); | ||
Assert.Contains("has_email", segments); | ||
Assert.Contains("has_email_opted_in", segments); | ||
_mockLogger.Verify(l => l.Log(LogLevel.WARN, It.IsAny<string>()), Times.Never); | ||
} | ||
|
||
[Test] | ||
public void ShouldHandleEmptyQualifiedSegments() | ||
{ | ||
var responseData = "{\"data\":{\"customer\":{\"audiences\":" + | ||
"{\"edges\":[ ]}}}}"; | ||
_mockOdpClient.Setup( | ||
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())). | ||
Returns(responseData); | ||
var manager = new GraphQLManager(_mockLogger.Object, _mockOdpClient.Object); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
FS_USER_ID, | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.WARN, It.IsAny<string>()), Times.Never); | ||
} | ||
|
||
[Test] | ||
public void ShouldHandleErrorWithInvalidIdentifier() | ||
{ | ||
var responseData = "{\"errors\":[{\"message\":" + | ||
"\"Exception while fetching data (/customer) : " + | ||
"java.lang.RuntimeException: could not resolve _fs_user_id = invalid-user\"," + | ||
"\"locations\":[{\"line\":1,\"column\":8}],\"path\":[\"customer\"]," + | ||
"\"extensions\":{\"classification\":\"DataFetchingException\"}}]," + | ||
"\"data\":{\"customer\":null}}"; | ||
_mockOdpClient.Setup( | ||
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())). | ||
Returns(responseData); | ||
var manager = new GraphQLManager(_mockLogger.Object); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
FS_USER_ID, | ||
"invalid-user", // invalid user | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once); | ||
mikechu-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
[Test] | ||
public void ShouldHandleOtherException() | ||
{ | ||
var responseData = "{\"errors\":[{\"message\":\"Validation error of type " + | ||
"UnknownArgument: Unknown field argument not_real_userKey @ " + | ||
"'customer'\",\"locations\":[{\"line\":1,\"column\":17}]," + | ||
"\"extensions\":{\"classification\":\"ValidationError\"}}]}"; | ||
|
||
_mockOdpClient.Setup( | ||
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())). | ||
Returns(responseData); | ||
var manager = new GraphQLManager(_mockLogger.Object, _mockOdpClient.Object); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
"not_real_userKey", | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()), Times.Once); | ||
} | ||
|
||
[Test] | ||
public void ShouldHandleBadResponse() | ||
{ | ||
var responseData = "{\"data\":{ }}"; | ||
_mockOdpClient.Setup( | ||
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())). | ||
Returns(responseData); | ||
var manager = new GraphQLManager(_mockLogger.Object, _mockOdpClient.Object); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
"not_real_userKey", | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (decode error)"), Times.Once); | ||
} | ||
|
||
[Test] | ||
public void ShouldHandleUnrecognizedJsonResponse() | ||
{ | ||
var responseData = "{\"unExpectedObject\":{ \"withSome\": \"value\", \"thatIsNotParseable\": \"true\" }}"; | ||
_mockOdpClient.Setup( | ||
c => c.QuerySegments(It.IsAny<QuerySegmentsParameters>())). | ||
Returns(responseData); | ||
var manager = new GraphQLManager(_mockLogger.Object, _mockOdpClient.Object); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
"not_real_userKey", | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (decode error)"), Times.Once); | ||
|
||
} | ||
|
||
[Test] | ||
public void ShouldHandle400HttpCode() | ||
{ | ||
var odpClient = new OdpClient(_mockLogger.Object, | ||
GetHttpClientThatReturnsStatus(HttpStatusCode.BadRequest)); | ||
var manager = new GraphQLManager(_mockLogger.Object, odpClient); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
FS_USER_ID, | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (network error)"), Times.Once); | ||
} | ||
|
||
[Test] | ||
public void ShouldHandle500HttpCode() | ||
{ | ||
var odpClient = new OdpClient(_mockLogger.Object, | ||
GetHttpClientThatReturnsStatus(HttpStatusCode.InternalServerError)); | ||
var manager = new GraphQLManager(_mockLogger.Object, odpClient); | ||
|
||
var segments = manager.FetchSegments( | ||
VALID_ODP_PUBLIC_KEY, | ||
ODP_GRAPHQL_URL, | ||
FS_USER_ID, | ||
"tester-101", | ||
_segmentsToCheck); | ||
|
||
Assert.IsTrue(segments.Length == 0); | ||
_mockLogger.Verify(l => l.Log(LogLevel.ERROR, "Audience segments fetch failed (network error)"), Times.Once); | ||
} | ||
|
||
private HttpClient GetHttpClientThatReturnsStatus(HttpStatusCode statusCode) | ||
{ | ||
var mockedHandler = new Mock<HttpMessageHandler>(); | ||
mockedHandler.Protected().Setup<Task<HttpResponseMessage>>( | ||
"SendAsync", | ||
ItExpr.IsAny<HttpRequestMessage>(), | ||
ItExpr.IsAny<CancellationToken>()). | ||
ReturnsAsync(() => new HttpResponseMessage(statusCode)); | ||
return new HttpClient(mockedHandler.Object); | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright 2022 Optimizely | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
using OptimizelySDK.Odp.Entity; | ||
|
||
namespace OptimizelySDK.Odp.Client | ||
{ | ||
public interface IOdpClient | ||
mikechu-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
string QuerySegments(QuerySegmentsParameters parameters); | ||
mikechu-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.