diff --git a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/FirestoreClientImplTest.cs b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/FirestoreClientImplTest.cs new file mode 100644 index 000000000000..cbf9f4b2c750 --- /dev/null +++ b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/FirestoreClientImplTest.cs @@ -0,0 +1,48 @@ +// Copyright 2019, Google Inc. All rights reserved. +// +// 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 +// +// http://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 System; +using Xunit; + +namespace Google.Cloud.Firestore.V1.Tests +{ + public class FirestoreClientImplTest + { + [Theory] + [InlineData("projects/proj/databases/database", "projects/proj/databases/database")] + [InlineData("projects/proj/databases/database/", "projects/proj/databases/database")] + [InlineData("projects/proj/databases/database/documents", "projects/proj/databases/database")] + [InlineData("projects/proj/databases/database/documents/col1/doc", "projects/proj/databases/database")] + [InlineData("projects/proj_id/databases/(default)/foo", "projects/proj_id/databases/(default)")] + public void GetDatabaseResourceName_Valid(string resourceName, string expectedDatabaseName) + { + Assert.Equal(expectedDatabaseName, FirestoreClientImpl.GetDatabaseResourceName(resourceName)); + } + + [Theory] + [InlineData("")] + [InlineData("projects//databases/(default)/foo")] + [InlineData("not_projects/project/databases/(default)/foo")] + [InlineData("projects/proj/not_databases/database")] + [InlineData("projects/")] + [InlineData("projects/proj/")] + [InlineData("projects/proj/databases")] + [InlineData("projects/proj/databases/")] + [InlineData("projects/proj/databases//other")] + public void GetDatabaseResourceName_Invalid(string resourceName) + { + Assert.Throws(() => FirestoreClientImpl.GetDatabaseResourceName(resourceName)); + } + } +} diff --git a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/Google.Cloud.Firestore.V1.Tests.csproj b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/Google.Cloud.Firestore.V1.Tests.csproj new file mode 100644 index 000000000000..f6516034c168 --- /dev/null +++ b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/Google.Cloud.Firestore.V1.Tests.csproj @@ -0,0 +1,20 @@ + + + + net6.0;net462 + net6.0 + false + 1701;1702;1705;xUnit2004;xUnit2013 + + + + + + + + + + + + + \ No newline at end of file diff --git a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/coverage.xml b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/coverage.xml new file mode 100644 index 000000000000..a8ebb3857262 --- /dev/null +++ b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.Tests/coverage.xml @@ -0,0 +1,18 @@ + + + C:/Program Files/dotnet/dotnet.exe + test --no-build -c Release + + + + Google.Cloud.Firestore.V1 + + + + + System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute + System.Diagnostics.DebuggerNonUserCodeAttribute + + . + ../../../coverage/Google.Cloud.Firestore.V1.Tests.dvcr + \ No newline at end of file diff --git a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.sln b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.sln index 336cb37bab57..89fbcf78e138 100644 --- a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.sln +++ b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1.sln @@ -3,19 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.V1", "Google.Cloud.Firestore.V1\Google.Cloud.Firestore.V1.csproj", "{F9CE8ABD-FB0E-437E-B03A-27F52D7A9F9A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.V1", "Google.Cloud.Firestore.V1\Google.Cloud.Firestore.V1.csproj", "{F9CE8ABD-FB0E-437E-B03A-27F52D7A9F9A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.V1.GeneratedSnippets", "Google.Cloud.Firestore.V1.GeneratedSnippets\Google.Cloud.Firestore.V1.GeneratedSnippets.csproj", "{F841636E-5304-4C09-B151-7B1DDB0618C8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.V1.GeneratedSnippets", "Google.Cloud.Firestore.V1.GeneratedSnippets\Google.Cloud.Firestore.V1.GeneratedSnippets.csproj", "{F841636E-5304-4C09-B151-7B1DDB0618C8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.ClientTesting", "..\..\tools\Google.Cloud.ClientTesting\Google.Cloud.ClientTesting.csproj", "{99487E3D-9DFC-419B-95AC-F8D5BEE8A3A4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.ClientTesting", "..\..\tools\Google.Cloud.ClientTesting\Google.Cloud.ClientTesting.csproj", "{99487E3D-9DFC-419B-95AC-F8D5BEE8A3A4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.V1.Snippets", "Google.Cloud.Firestore.V1.Snippets\Google.Cloud.Firestore.V1.Snippets.csproj", "{4357F6C5-8599-41D7-84EF-ED59E32B54DF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.V1.Snippets", "Google.Cloud.Firestore.V1.Snippets\Google.Cloud.Firestore.V1.Snippets.csproj", "{4357F6C5-8599-41D7-84EF-ED59E32B54DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.V1.Tests", "Google.Cloud.Firestore.V1.Tests\Google.Cloud.Firestore.V1.Tests.csproj", "{8C638D13-3585-4344-B597-DA0EA0266688}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F9CE8ABD-FB0E-437E-B03A-27F52D7A9F9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F9CE8ABD-FB0E-437E-B03A-27F52D7A9F9A}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -33,11 +38,9 @@ Global {4357F6C5-8599-41D7-84EF-ED59E32B54DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {4357F6C5-8599-41D7-84EF-ED59E32B54DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {4357F6C5-8599-41D7-84EF-ED59E32B54DF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A454C46B-D941-47B5-A3BD-218EC33797BE} + {8C638D13-3585-4344-B597-DA0EA0266688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C638D13-3585-4344-B597-DA0EA0266688}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C638D13-3585-4344-B597-DA0EA0266688}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C638D13-3585-4344-B597-DA0EA0266688}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1/FirestoreClientPartial.cs b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1/FirestoreClientPartial.cs index 58bd803464bf..35d7ca38cdc0 100644 --- a/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1/FirestoreClientPartial.cs +++ b/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1/FirestoreClientPartial.cs @@ -54,6 +54,100 @@ public partial class FirestoreClientImpl [Obsolete("This header is obsolete; x-goog-request-params should now be used instead. " + "This constant will be removed in a future version")] public const string ResourcePrefixHeader = "google-cloud-resource-prefix"; + + partial void Modify_BatchGetDocumentsRequest(ref BatchGetDocumentsRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Database); + + partial void Modify_BeginTransactionRequest(ref BeginTransactionRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Database); + + partial void Modify_CommitRequest(ref CommitRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Database); + + partial void Modify_CreateDocumentRequest(ref CreateDocumentRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Parent); + + partial void Modify_DeleteDocumentRequest(ref DeleteDocumentRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Name); + + partial void Modify_GetDocumentRequest(ref GetDocumentRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Name); + + partial void Modify_ListCollectionIdsRequest(ref ListCollectionIdsRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Parent); + + partial void Modify_ListDocumentsRequest(ref ListDocumentsRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Parent); + + partial void Modify_RollbackRequest(ref RollbackRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Database); + + partial void Modify_RunQueryRequest(ref RunQueryRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Parent); + + partial void Modify_RunAggregationQueryRequest(ref RunAggregationQueryRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Parent); + + partial void Modify_UpdateDocumentRequest(ref UpdateDocumentRequest request, ref CallSettings settings) => + ApplyResourcePrefixHeader(ref settings, request.Document?.Name); + + private static void ApplyResourcePrefixHeader(ref CallSettings settings, string resource) + { + // If we haven't been given a resource name, just let the request as it is. + if (string.IsNullOrEmpty(resource)) + { + return; + } + string database = GetDatabaseResourceName(resource); +#pragma warning disable CS0618 // Type or member is obsolete + settings = settings.WithHeader(ResourcePrefixHeader, database); +#pragma warning restore CS0618 // Type or member is obsolete + } + + // Visible for testing + + /// + /// Retrieves the database resource name from a full resource name. + /// Validation is performed as far as the database ID but no further; the database ID is deemed to end at the first slash. + /// + /// The resource name, which must start with projects/{project_id}/databases/{database_id} + /// The database resource name + internal static string GetDatabaseResourceName(string resource) + { + const string projectsPrefix = "projects/"; + const string databasesPrefix = "databases/"; + if (string.CompareOrdinal(resource, 0, projectsPrefix, 0, projectsPrefix.Length) != 0) + { + // "projects/" doesn't match + ThrowInvalidResource(); + } + int endOfProjectId = resource.IndexOf('/', projectsPrefix.Length); + if (endOfProjectId == -1 || endOfProjectId == projectsPrefix.Length) + { + // Empty project ID or no slash at the end of it + ThrowInvalidResource(); + } + if (string.CompareOrdinal(resource, endOfProjectId + 1, databasesPrefix, 0, databasesPrefix.Length) != 0) + { + // "databases/" doesn't match + ThrowInvalidResource(); + } + int startOfDatabaseId = endOfProjectId + 1 + databasesPrefix.Length; + if (startOfDatabaseId == resource.Length) + { + // No database ID + ThrowInvalidResource(); + } + int endOfDatabaseId = resource.IndexOf('/', startOfDatabaseId); + if (endOfDatabaseId == startOfDatabaseId) + { + ThrowInvalidResource(); + } + // It's valid for the whole resource name to be the database name + return endOfDatabaseId == -1 ? resource : resource.Substring(0, endOfDatabaseId); + + void ThrowInvalidResource() => throw new ArgumentException($"{resource} is not a valid Firestore resource name", nameof(resource)); + } } // Support for FirestoreDbBuilder.