From c7cc78a9075ab929306dca393e3505e0a5d390c2 Mon Sep 17 00:00:00 2001 From: Merwin Hill Date: Fri, 9 Feb 2024 15:14:44 +0100 Subject: [PATCH] test: Add unit tests for MetadataClient --- .../Protos/test.proto | 2 +- .../Services/TestService.cs | 2 +- .../GlobalUsings.cs | 1 + .../MetadataClientTests.cs | 357 ++++++++++++++++++ ...Google.Cloud.Compute.Metadata.Tests.csproj | 29 ++ .../MetadataClient.cs | 37 +- .../NotOnGCEException.cs | 3 + Q42.Google.Cloud.Compute.Metadata.sln | 6 + 8 files changed, 422 insertions(+), 15 deletions(-) create mode 100644 Q42.Google.Cloud.Compute.Metadata.Tests/GlobalUsings.cs create mode 100644 Q42.Google.Cloud.Compute.Metadata.Tests/MetadataClientTests.cs create mode 100644 Q42.Google.Cloud.Compute.Metadata.Tests/Q42.Google.Cloud.Compute.Metadata.Tests.csproj create mode 100644 Q42.Google.Cloud.Compute.Metadata.V1/NotOnGCEException.cs diff --git a/Q42.Google.Cloud.Compute.Metadata.TestServer/Protos/test.proto b/Q42.Google.Cloud.Compute.Metadata.TestServer/Protos/test.proto index 898c76d..57e0d02 100644 --- a/Q42.Google.Cloud.Compute.Metadata.TestServer/Protos/test.proto +++ b/Q42.Google.Cloud.Compute.Metadata.TestServer/Protos/test.proto @@ -27,5 +27,5 @@ message TestReply { string zone = 11; repeated string instance_attributes = 12; repeated string project_attributes = 13; - string default_sa_scopes = 14; + repeated string default_sa_scopes = 14; } diff --git a/Q42.Google.Cloud.Compute.Metadata.TestServer/Services/TestService.cs b/Q42.Google.Cloud.Compute.Metadata.TestServer/Services/TestService.cs index e27b67e..f3f833c 100644 --- a/Q42.Google.Cloud.Compute.Metadata.TestServer/Services/TestService.cs +++ b/Q42.Google.Cloud.Compute.Metadata.TestServer/Services/TestService.cs @@ -60,7 +60,7 @@ await Task.WhenAll(projectId, numericProjectId, instanceId, internalIp, defaultS Zone = zone.GetResultOrEmpty(), InstanceAttributes = { instanceAttributes.GetResultOrEmpty() }, ProjectAttributes = { projectAttributes.GetResultOrEmpty() }, - DefaultSaScopes = defaultSaScopes.GetResultOrEmpty() + DefaultSaScopes = { defaultSaScopes.GetResultOrEmpty() } }; } } \ No newline at end of file diff --git a/Q42.Google.Cloud.Compute.Metadata.Tests/GlobalUsings.cs b/Q42.Google.Cloud.Compute.Metadata.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/Q42.Google.Cloud.Compute.Metadata.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/Q42.Google.Cloud.Compute.Metadata.Tests/MetadataClientTests.cs b/Q42.Google.Cloud.Compute.Metadata.Tests/MetadataClientTests.cs new file mode 100644 index 0000000..8be9e28 --- /dev/null +++ b/Q42.Google.Cloud.Compute.Metadata.Tests/MetadataClientTests.cs @@ -0,0 +1,357 @@ +namespace Q42.Google.Cloud.Compute.Metadata.Tests; + +using System.Net; +using System.Text; +using Q42.Google.Cloud.Compute.Metadata.V1; +using RichardSzalay.MockHttp; + +public class Tests +{ + private readonly List> googleHeaders = + [new KeyValuePair("Metadata-Flavor", "Google")]; + + private readonly string baseAddress = "http://169.254.169.254"; + private readonly string metadataBase = "http://169.254.169.254/computeMetadata/v1/"; + + private MockHttpMessageHandler mockHttpOnGce = new(); + + [SetUp] + public void Setup() + { + mockHttpOnGce = new MockHttpMessageHandler(); + mockHttpOnGce.When($"{baseAddress}") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, new StringContent("", Encoding.UTF8, "application/text")); + } + + [Test] + public async Task IsOnGCETrue() + { + var httpContent = new StringContent("", Encoding.UTF8, "application/text"); + var mockHttp = new MockHttpMessageHandler(); + mockHttp.When($"{baseAddress}/*") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, httpContent); + + var client = mockHttp.ToHttpClient(); + using var metadata = new MetadataClient(client); + var onGce = await metadata.IsOnGCEAsync(); + Assert.IsTrue(onGce); + mockHttp.VerifyNoOutstandingExpectation(); + } + + // We timeout the test after 1042ms, because the default timeout is 1000ms. + [Test, Timeout(1042)] + public async Task IsOnGCEFalse() + { + using var metadata = new MetadataClient(); + var onGce = await metadata.IsOnGCEAsync(); + Assert.IsFalse(onGce); + } + + [Test] + public async Task UsesMetadataEnvVar() + { + Environment.SetEnvironmentVariable("GCE_METADATA_HOST", "test-metadata"); + var httpContent = new StringContent("", Encoding.UTF8, "application/text"); + var mockHttp = new MockHttpMessageHandler(); + mockHttp.When("http://test-metadata/*") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, httpContent); + + var client = mockHttp.ToHttpClient(); + using var metadata = new MetadataClient(client); + var onGce = await metadata.IsOnGCEAsync(); + Assert.IsTrue(onGce); + mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task DoesNotThrowWhenNotOnGCEByDefault() + { + using var metadata = new MetadataClient(); + var result = await metadata.GetProjectIdAsync(); + Assert.IsNull(result); + } + + [Test] + public Task ThrowsWhenNotOnGCE() + { + using var metadata = new MetadataClient( true); + Assert.ThrowsAsync(async () => await metadata.GetProjectIdAsync()); + return Task.CompletedTask; + } + + [Test] + public async Task GetsProjectId() + { + const string projectId = "test-project"; + mockHttpOnGce.When($"{metadataBase}project/project-id") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", projectId); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetProjectIdAsync(); + Assert.That(result, Is.EqualTo(projectId)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsNumericProjectId() + { + const string projectId = "424242"; + mockHttpOnGce.When($"{metadataBase}project/numeric-project-id") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", projectId); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetNumericProjectIdAsync(); + Assert.That(result, Is.EqualTo(projectId)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInstanceId() + { + const string instanceId = "424242"; + mockHttpOnGce.When($"{metadataBase}instance/id") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", instanceId); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceIdAsync(); + Assert.That(result, Is.EqualTo(instanceId)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInternalIp() + { + const string internalIp = "10.0.0.42"; + mockHttpOnGce.When($"{metadataBase}instance/network-interfaces/0/ip") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", internalIp); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInternalIpAsync(); + Assert.That(result, Is.EqualTo(internalIp)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInternalIpTrimmed() + { + const string internalIp = "10.0.0.42"; + mockHttpOnGce.When($"{metadataBase}instance/network-interfaces/0/ip") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", " "+ internalIp + " "); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInternalIpAsync(); + Assert.That(result, Is.EqualTo(internalIp)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsDefaultSaEmail() + { + const string saEmail = "default@serviceaccount.test"; + mockHttpOnGce.When($"{metadataBase}instance/service-accounts/default/email") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", saEmail ); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetEmailAsync(); + Assert.That(result, Is.EqualTo(saEmail)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsSaEmail() + { + const string saEmail = "test@serviceaccount.test"; + mockHttpOnGce.When($"{metadataBase}instance/service-accounts/test/email") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", saEmail ); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetEmailAsync("test"); + Assert.That(result, Is.EqualTo(saEmail)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsExternalIp() + { + const string expected = "34.0.0.42"; + mockHttpOnGce.When($"{metadataBase}instance/network-interfaces/0/access-configs/0/external-ip") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", expected); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetExternalIpAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsHostname() + { + const string expected = "internal.test.hostname"; + mockHttpOnGce.When($"{metadataBase}instance/hostname") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", expected); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetHostnameAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsTags() + { + string[] expected = ["tag1", "tag2"]; + mockHttpOnGce.When($"{metadataBase}instance/tags") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/json", "[\"tag1\",\"tag2\"]"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceTagsAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsTagsEmpty() + { + string[] expected = []; + mockHttpOnGce.When($"{metadataBase}instance/tags") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/json", "[]"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceTagsAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInstanceName() + { + const string expected = "test-name"; + mockHttpOnGce.When($"{metadataBase}instance/name") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", expected); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceNameAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsZone() + { + const string expected = "europe-west3-c"; + mockHttpOnGce.When($"{metadataBase}instance/zone") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", $"projects/424242/zones/{expected}"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetZoneAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInstanceAttributes() + { + string[] expected = ["ssh-keys", "test"]; + mockHttpOnGce.When($"{metadataBase}instance/attributes/") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", "ssh-keys\ntest"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceAttributesAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInstanceAttributesEmpty() + { + string[] expected = []; + mockHttpOnGce.When($"{metadataBase}instance/attributes/") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceAttributesAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsProjectAttributes() + { + string[] expected = ["ssh-keys", "test"]; + mockHttpOnGce.When($"{metadataBase}project/attributes/") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", "ssh-keys\ntest"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetProjectAttributesAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsProjectAttributesEmpty() + { + string[] expected = []; + mockHttpOnGce.When($"{metadataBase}project/attributes/") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", ""); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetProjectAttributesAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsInstanceAttribute() + { + string expected = "value"; + mockHttpOnGce.When($"{metadataBase}instance/attributes/test") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", expected); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetInstanceAttributeValueAsync("test"); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsProjectAttribute() + { + string expected = "value"; + mockHttpOnGce.When($"{metadataBase}project/attributes/test") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", expected); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetProjectAttributeValueAsync("test"); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsDefaultScopes() + { + string[] expected = ["scope1", "scope2"]; + mockHttpOnGce.When($"{metadataBase}instance/service-accounts/default/scopes") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", "scope1\nscope2"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetScopesAsync(); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task GetsSaScopes() + { + string[] expected = ["scope1", "scope2"]; + mockHttpOnGce.When($"{metadataBase}instance/service-accounts/account/scopes") + .WithHeaders(googleHeaders) + .Respond(HttpStatusCode.OK, googleHeaders, "application/text", "scope1\nscope2"); + using var metadata = new MetadataClient(mockHttpOnGce.ToHttpClient()); + var result = await metadata.GetScopesAsync("account"); + Assert.That(result, Is.EqualTo(expected)); + mockHttpOnGce.VerifyNoOutstandingExpectation(); + } +} \ No newline at end of file diff --git a/Q42.Google.Cloud.Compute.Metadata.Tests/Q42.Google.Cloud.Compute.Metadata.Tests.csproj b/Q42.Google.Cloud.Compute.Metadata.Tests/Q42.Google.Cloud.Compute.Metadata.Tests.csproj new file mode 100644 index 0000000..4aff041 --- /dev/null +++ b/Q42.Google.Cloud.Compute.Metadata.Tests/Q42.Google.Cloud.Compute.Metadata.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/Q42.Google.Cloud.Compute.Metadata.V1/MetadataClient.cs b/Q42.Google.Cloud.Compute.Metadata.V1/MetadataClient.cs index 836efee..ea532e7 100644 --- a/Q42.Google.Cloud.Compute.Metadata.V1/MetadataClient.cs +++ b/Q42.Google.Cloud.Compute.Metadata.V1/MetadataClient.cs @@ -42,18 +42,19 @@ public class MetadataClient: IDisposable /// /// Initialize a new MetadataClient. /// + /// An injected HttpClient to use /// - /// Defines if the functions should throw when not on GCE. - /// When `false`, the functions will return null when not on GCE. - /// When `true`, the functions will throw when not on GCE. + /// Defines if the functions should throw a NotOnGceException when not on GCE. + /// When `false`, the functions will return null when not on GCE. + /// When `true`, the functions will throw when not on GCE. /// - public MetadataClient(bool throwIfNotOnGce = false) + public MetadataClient(HttpClient httpClient, bool throwIfNotOnGce = false) { this.throwIfNotOnGce = throwIfNotOnGce; metadataHost = Environment.GetEnvironmentVariable(metadataHostEnv) ?? metadataIP; baseUrl = $"http://{metadataHost}/computeMetadata/v1/"; - client = new HttpClient(); + client = httpClient; client.DefaultRequestHeaders.Add("User-Agent", userAgent); // Required header per https://cloud.google.com/compute/docs/metadata/overview#parts-of-a-request client.DefaultRequestHeaders.Add("Metadata-Flavor", "Google"); @@ -62,6 +63,18 @@ public MetadataClient(bool throwIfNotOnGce = false) client.Timeout = TimeSpan.FromSeconds(1); } + /// + /// Initialize a new MetadataClient. + /// + /// + /// Defines if the functions should throw when not on GCE. + /// When `false`, the functions will return null when not on GCE. + /// When `true`, the functions will throw when not on GCE. + /// + public MetadataClient(bool throwIfNotOnGce = false) : this(new HttpClient(), throwIfNotOnGce) + { + } + /// /// reports whether this process is running on Google Compute Engine. /// @@ -74,11 +87,9 @@ public async Task IsOnGCEAsync(CancellationToken cancellationToken = defau { var response = await client.GetAsync($"http://{metadataHost}", cancellationToken); onGCE = response.Headers.TryGetValues("Metadata-Flavor", out var values) && values.Contains("Google"); - Console.WriteLine("On GCE: {0}", onGCE.Value ? "Yes" : "No"); } - catch (Exception e) + catch (Exception) { - Console.WriteLine("Failed to connect to metadata server: {0}", e.Message); // If we get an exception, we're can't connect to the metadata server or receive an error, so we are probably not on GCE. onGCE = false; } @@ -126,7 +137,7 @@ public async Task IsOnGCEAsync(CancellationToken cancellationToken = defau /// The service account to get the email for, or null/empty for default /// /// The email address associated with the service account. - public async Task GetEmailAsync(string? serviceAccount, CancellationToken cancellationToken) + public async Task GetEmailAsync(string? serviceAccount = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(serviceAccount)) { @@ -227,14 +238,14 @@ public async Task IsOnGCEAsync(CancellationToken cancellationToken = defau /// The service account to get the scopes for, or null/empty for default /// /// The scopes associated with the service account. - public async Task GetScopesAsync(string? serviceAccount, CancellationToken cancellationToken) + public async Task GetScopesAsync(string? serviceAccount = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(serviceAccount)) { serviceAccount = "default"; } - return await GetTrimmedCachedString($"instance/service-accounts/{serviceAccount}/scopes", + return await GetCachedLines($"instance/service-accounts/{serviceAccount}/scopes", cancellationToken); } @@ -242,7 +253,7 @@ public async Task IsOnGCEAsync(CancellationToken cancellationToken = defau private async Task GetCachedLines(string suffix, CancellationToken cancellationToken) { var str = await GetCachedString(suffix, cancellationToken); - return str?.Trim().Split('\n').Select(s => s.Trim()).ToArray(); + return str?.Trim().Split('\n').Select(s => s.Trim()).Where(s => !string.IsNullOrWhiteSpace(s)).ToArray(); } private async Task GetTrimmedCachedString(string suffix, CancellationToken cancellationToken) @@ -261,7 +272,7 @@ public async Task IsOnGCEAsync(CancellationToken cancellationToken = defau if (!await IsOnGCEAsync(cancellationToken)) { if (throwIfNotOnGce) - throw new Exception("Not running on GCE"); + throw new NotOnGceException(); return null; } diff --git a/Q42.Google.Cloud.Compute.Metadata.V1/NotOnGCEException.cs b/Q42.Google.Cloud.Compute.Metadata.V1/NotOnGCEException.cs new file mode 100644 index 0000000..c69b148 --- /dev/null +++ b/Q42.Google.Cloud.Compute.Metadata.V1/NotOnGCEException.cs @@ -0,0 +1,3 @@ +namespace Q42.Google.Cloud.Compute.Metadata.V1; + +public class NotOnGceException() : Exception("Not running on GCE."); \ No newline at end of file diff --git a/Q42.Google.Cloud.Compute.Metadata.sln b/Q42.Google.Cloud.Compute.Metadata.sln index 9c24340..ff96fcf 100644 --- a/Q42.Google.Cloud.Compute.Metadata.sln +++ b/Q42.Google.Cloud.Compute.Metadata.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Q42.Google.Cloud.Compute.Me EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Q42.Google.Cloud.Compute.Metadata.TestServer", "Q42.Google.Cloud.Compute.Metadata.TestServer\Q42.Google.Cloud.Compute.Metadata.TestServer.csproj", "{0653D40D-E6F3-4B7C-BF67-F19C70856078}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Q42.Google.Cloud.Compute.Metadata.Tests", "Q42.Google.Cloud.Compute.Metadata.Tests\Q42.Google.Cloud.Compute.Metadata.Tests.csproj", "{7C6FA942-5B98-43B3-9E39-3F91D6761BEB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {0653D40D-E6F3-4B7C-BF67-F19C70856078}.Debug|Any CPU.Build.0 = Debug|Any CPU {0653D40D-E6F3-4B7C-BF67-F19C70856078}.Release|Any CPU.ActiveCfg = Release|Any CPU {0653D40D-E6F3-4B7C-BF67-F19C70856078}.Release|Any CPU.Build.0 = Release|Any CPU + {7C6FA942-5B98-43B3-9E39-3F91D6761BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C6FA942-5B98-43B3-9E39-3F91D6761BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C6FA942-5B98-43B3-9E39-3F91D6761BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C6FA942-5B98-43B3-9E39-3F91D6761BEB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal