Skip to content

Commit 07d7f5e

Browse files
Snowflake V2 Connector - fix handling nulls and add integration tests (#3920)
* FLOW-1429 Add integration test for PowerApps connector (#2) * FLOW-1429 Add integration test for PowerApps connector #2 * FLOW-3900: Handling nulls and setup tests based on client id and secret --------- Co-authored-by: Rafal Zukowski <[email protected]> * FLOW-3900: Update summary with a fix description * FLOW-1429 Update SnowflakeTestApp mock parameters to use TestData (#5) FLOW-1429 Update SnowflakeTestApp mock parameters to use TestData --------- Co-authored-by: Rafal Zukowski <[email protected]>
1 parent 7f11c42 commit 07d7f5e

24 files changed

+2816
-98
lines changed

certified-connectors/Snowflake v2/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
!/.gitignore
66
!/SnowflakeV2CoreLogic/
77
!/SnowflakeTestApp/
8+
!/SnowflakeTestApp.Tests/
89
!/ConnectorArtifacts/
910

1011
# .config folder is not ignored
@@ -64,6 +65,9 @@ QLogs
6465
## Secret Disclosure Risks
6566
###
6667

68+
# Test configuration file with secrets
69+
SnowflakeTestApp/Mocks/ConnectionParametersProviderMock.cs
70+
6771
# *.pfx
6872
*.[Pp][Ff][Xx]
6973

certified-connectors/Snowflake v2/ConnectorArtifacts/apidefinition.swagger.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"swagger": "2.0",
3-
"info": {
4-
"version": "1.1",
5-
"title": "Snowflake",
6-
"description": "Snowflake Connector allows you to build canvas apps and surface Snowflake data in Virtual Tables, while also enabling faster data processing and analytics compared to traditional solutions.",
7-
"x-ms-api-annotation": {
8-
"status": "Preview"
3+
"info": {
4+
"version": "1.1",
5+
"title": "Snowflake",
6+
"description": "Snowflake Connector allows you to build canvas apps and surface Snowflake data in Virtual Tables, while also enabling faster data processing and analytics compared to traditional solutions. This version fixes support for null values in columns of date and time types.",
7+
"x-ms-api-annotation": {
8+
"status": "Preview"
9+
},
10+
"x-ms-keywords": [
11+
"snowflake"
12+
]
913
},
10-
"x-ms-keywords": [
11-
"snowflake"
12-
]
13-
},
1414
"host": "localhost:56800",
1515
"basePath": "/",
1616
"schemes": [
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Runtime.CompilerServices;
6+
using System.Threading.Tasks;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using Newtonsoft.Json;
9+
using SnowflakeTestApp.Tests.Infrastructure;
10+
11+
namespace SnowflakeTestApp.Tests
12+
{
13+
/// <summary>
14+
/// Base class for integration tests providing common functionality and test data management.
15+
/// </summary>
16+
[TestClass]
17+
public abstract class BaseIntegrationTest
18+
{
19+
private const int APPLICATION_HEALTH_CHECK_TIMEOUT_SECONDS = 5;
20+
private const string BEARER_TOKEN_CONFIGURATION_ERROR =
21+
"Bearer token not configured. Please update ConnectionParametersProviderMock.TestBearerToken with a valid OAuth bearer token. See README.md for instructions.";
22+
private const string APPLICATION_NOT_RUNNING_ERROR =
23+
"SnowflakeTestApp is not running at {0}. Please start the application before running tests. Error: {1}";
24+
25+
protected static AccessTokenService AccessTokenService;
26+
protected string BaseUrl => TestData.BaseUrl;
27+
protected HttpClient HttpClient;
28+
protected static TestDataSeeder DataSeeder;
29+
protected static List<TestDataRecord> SeededTestData => DataSeeder?.SeededRecords ?? new List<TestDataRecord>();
30+
public TestContext TestContext { get; set; }
31+
32+
[AssemblyInitialize]
33+
public static void AssemblyInitialize(TestContext context)
34+
{
35+
InitializeAccessTokenService();
36+
InitializeTestDataSeeder();
37+
SeedTestData();
38+
}
39+
40+
[AssemblyCleanup]
41+
public static void AssemblyCleanup()
42+
{
43+
CleanupTestResources();
44+
}
45+
46+
[TestInitialize]
47+
public virtual void TestInitialize()
48+
{
49+
HttpClient = CreateHttpClient();
50+
}
51+
52+
[TestCleanup]
53+
public virtual void TestCleanup()
54+
{
55+
HttpClient?.Dispose();
56+
}
57+
58+
protected string GetTestToken()
59+
{
60+
var service = new AccessTokenService(TestData.TenantId, TestData.ClientId, TestData.ClientSecret, TestData.Scope);
61+
var token = service.GetAccessTokenAsync().GetAwaiter().GetResult();
62+
return token;
63+
}
64+
65+
protected void EnsureApplicationIsRunning()
66+
{
67+
try
68+
{
69+
ValidateApplicationHealth();
70+
}
71+
catch (Exception ex)
72+
{
73+
Assert.Inconclusive(string.Format(APPLICATION_NOT_RUNNING_ERROR, BaseUrl, ex.Message));
74+
}
75+
}
76+
77+
protected T DeserializeResponse<T>(HttpResponseMessage response)
78+
{
79+
var content = response.Content.ReadAsStringAsync().Result;
80+
81+
try
82+
{
83+
return JsonConvert.DeserializeObject<T>(content);
84+
}
85+
catch (JsonException ex)
86+
{
87+
var errorMessage = $"Failed to deserialize response content as {typeof(T).Name}. Content: {content}. Error: {ex.Message}";
88+
Assert.Fail(errorMessage);
89+
return default(T);
90+
}
91+
}
92+
93+
protected StringContent CreateJsonContent(object data)
94+
{
95+
var json = JsonConvert.SerializeObject(data);
96+
return new StringContent(json, System.Text.Encoding.UTF8, "application/json");
97+
}
98+
99+
protected async Task<List<TestDataRecord>> FetchActualDataFromDatabase(string tableName = null)
100+
{
101+
using (var httpClient = CreateHttpClient())
102+
{
103+
var dataSeeder = new TestDataSeeder(httpClient, TestData.BaseUrl, AccessTokenService);
104+
return await dataSeeder.FetchTestDataFromDatabase(tableName);
105+
}
106+
}
107+
108+
protected async Task<TestDataRecord> FetchActualRecordById(int id, string tableName = null)
109+
{
110+
using (var httpClient = CreateHttpClient())
111+
{
112+
var dataSeeder = new TestDataSeeder(httpClient, TestData.BaseUrl, AccessTokenService);
113+
return await dataSeeder.FetchTestRecordById(id, tableName);
114+
}
115+
}
116+
117+
protected void ValidateDataMatches(List<TestDataRecord> expectedRecords, List<TestDataRecord> actualRecords, string message = null)
118+
{
119+
var assertionMessage = message ?? "Database records should match seeded test data";
120+
121+
Assert.AreEqual(expectedRecords.Count, actualRecords.Count, $"{assertionMessage}: Record count mismatch");
122+
123+
ValidateIndividualRecords(expectedRecords, actualRecords, assertionMessage);
124+
}
125+
126+
protected void ValidateRecordMatches(TestDataRecord expected, TestDataRecord actual, string message = null)
127+
{
128+
var assertionMessage = message ?? $"Record with ID {expected?.Id} should match expected values";
129+
130+
Assert.IsNotNull(actual, $"{assertionMessage}: Record not found in database");
131+
Assert.AreEqual(expected, actual, assertionMessage);
132+
}
133+
private static void InitializeAccessTokenService()
134+
{
135+
AccessTokenService = new AccessTokenService(TestData.TenantId, TestData.ClientId, TestData.ClientSecret, TestData.Scope);
136+
}
137+
private static void InitializeTestDataSeeder()
138+
{
139+
var httpClient = new HttpClient
140+
{
141+
Timeout = TimeSpan.FromSeconds(TestData.DefaultTimeoutSeconds)
142+
};
143+
144+
DataSeeder = new TestDataSeeder(httpClient, TestData.BaseUrl, AccessTokenService);
145+
}
146+
147+
private static void SeedTestData()
148+
{
149+
try
150+
{
151+
var success = DataSeeder.EnsureTestTableExistsAndSeed(TestData.DefaultTable, TestData.DefaultDataset)
152+
.GetAwaiter().GetResult();
153+
154+
if (!success)
155+
{
156+
throw new InvalidOperationException($"Failed to setup test table '{TestData.DefaultTable}'");
157+
}
158+
}
159+
catch (Exception ex)
160+
{
161+
throw new InvalidOperationException($"Test data seeding failed: {ex.Message}", ex);
162+
}
163+
}
164+
165+
private static void CleanupTestResources()
166+
{
167+
try
168+
{
169+
DataSeeder?.CleanupTestTable(TestData.DefaultTable).GetAwaiter().GetResult();
170+
DataSeeder?.Dispose();
171+
}
172+
catch (Exception)
173+
{
174+
// Ignore cleanup errors to prevent masking test failures
175+
}
176+
}
177+
178+
private HttpClient CreateHttpClient()
179+
{
180+
return new HttpClient
181+
{
182+
Timeout = TimeSpan.FromSeconds(TestData.DefaultTimeoutSeconds)
183+
};
184+
}
185+
186+
private void ValidateApplicationHealth()
187+
{
188+
using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(APPLICATION_HEALTH_CHECK_TIMEOUT_SECONDS) })
189+
{
190+
var response = client.GetAsync(BaseUrl).Result;
191+
}
192+
}
193+
194+
private void ValidateIndividualRecords(List<TestDataRecord> expectedRecords, List<TestDataRecord> actualRecords, string assertionMessage)
195+
{
196+
foreach (var expected in expectedRecords)
197+
{
198+
var actual = actualRecords.FirstOrDefault(r => r.Id == expected.Id);
199+
200+
Assert.IsNotNull(actual, $"{assertionMessage}: Record with ID {expected.Id} not found in database");
201+
Assert.AreEqual(expected, actual, $"{assertionMessage}: Record with ID {expected.Id} does not match expected values");
202+
}
203+
}
204+
}
205+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
6+
namespace SnowflakeTestApp.Tests.Connection
7+
{
8+
/// <summary>
9+
/// Integration tests for the /testconnection endpoint.
10+
/// </summary>
11+
[TestClass]
12+
public class TestConnectionEndpointIntegrationTest : BaseIntegrationTest
13+
{
14+
private const string TEST_CONNECTION_ENDPOINT = "/testconnection";
15+
private const string INVALID_TOKEN = "invalid-token";
16+
private const string MALFORMED_AUTH_HEADER = "InvalidFormat";
17+
private const string BEARER_TOKEN_MISSING_ERROR = "Bearer token is missing in the HTTP request authorization header.";
18+
private const string INVALID_OAUTH_TOKEN_ERROR = "Invalid OAuth access token.";
19+
private const string POST_METHOD_NOT_ALLOWED_ERROR = "The requested resource does not support http method 'POST'.";
20+
21+
[TestInitialize]
22+
public override void TestInitialize()
23+
{
24+
base.TestInitialize();
25+
EnsureApplicationIsRunning();
26+
}
27+
28+
/// <summary>
29+
/// Test the /testconnection endpoint with authentication
30+
/// </summary>
31+
[TestMethod]
32+
public async Task TestConnectionEndpoint_WithValidAuth_ReturnsOK()
33+
{
34+
var testToken = GetTestToken();
35+
AddAuthorizationHeader(testToken);
36+
37+
var response = await HttpClient.GetAsync($"{BaseUrl}{TEST_CONNECTION_ENDPOINT}");
38+
39+
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
40+
}
41+
42+
/// <summary>
43+
/// Test the endpoint without authentication
44+
/// </summary>
45+
[TestMethod]
46+
public async Task TestConnectionEndpoint_WithoutAuth_ReturnsInternalServerError()
47+
{
48+
var response = await HttpClient.GetAsync($"{BaseUrl}{TEST_CONNECTION_ENDPOINT}");
49+
var content = await response.Content.ReadAsStringAsync();
50+
51+
Assert.AreEqual(HttpStatusCode.InternalServerError, response.StatusCode);
52+
StringAssert.Contains(content, BEARER_TOKEN_MISSING_ERROR);
53+
}
54+
55+
/// <summary>
56+
/// Test the endpoint with invalid authentication
57+
/// </summary>
58+
[TestMethod]
59+
public async Task TestConnectionEndpoint_WithInvalidAuth_ReturnsInternalServerError()
60+
{
61+
AddAuthorizationHeader(INVALID_TOKEN);
62+
63+
var response = await HttpClient.GetAsync($"{BaseUrl}{TEST_CONNECTION_ENDPOINT}");
64+
var content = await response.Content.ReadAsStringAsync();
65+
66+
Assert.AreEqual(HttpStatusCode.InternalServerError, response.StatusCode);
67+
StringAssert.Contains(content, INVALID_OAUTH_TOKEN_ERROR);
68+
}
69+
70+
/// <summary>
71+
/// Test the endpoint with malformed authorization header
72+
/// </summary>
73+
[TestMethod]
74+
public async Task TestConnectionEndpoint_WithMalformedAuth_ReturnsInternalServerError()
75+
{
76+
HttpClient.DefaultRequestHeaders.Add("Authorization", MALFORMED_AUTH_HEADER);
77+
78+
var response = await HttpClient.GetAsync($"{BaseUrl}{TEST_CONNECTION_ENDPOINT}");
79+
var content = await response.Content.ReadAsStringAsync();
80+
81+
Assert.AreEqual(HttpStatusCode.InternalServerError, response.StatusCode);
82+
StringAssert.Contains(content, INVALID_OAUTH_TOKEN_ERROR);
83+
}
84+
85+
/// <summary>
86+
/// Test the endpoint with POST method (should not be allowed)
87+
/// </summary>
88+
[TestMethod]
89+
public async Task TestConnectionEndpoint_WithPOST_ReturnsMethodNotAllowed()
90+
{
91+
var testToken = GetTestToken();
92+
AddAuthorizationHeader(testToken);
93+
94+
var response = await HttpClient.PostAsync($"{BaseUrl}{TEST_CONNECTION_ENDPOINT}", new StringContent(""));
95+
var content = await response.Content.ReadAsStringAsync();
96+
97+
Assert.AreEqual(HttpStatusCode.MethodNotAllowed, response.StatusCode);
98+
StringAssert.Contains(content, POST_METHOD_NOT_ALLOWED_ERROR);
99+
}
100+
101+
private void AddAuthorizationHeader(string token)
102+
{
103+
HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)