Initial commit
Some checks failed
Continuous Integration - Pull Request / code-tests (pull_request) Has been cancelled
Continuous Integration - Pull Request / deployment-tests (local-code) (pull_request) Has been cancelled
helm-chart-ci / helm-chart-ci (pull_request) Has been cancelled
kubevious-manifests-ci / kubevious-manifests-ci (pull_request) Has been cancelled
kustomize-build-ci / kustomize-build-ci (pull_request) Has been cancelled
terraform-validate-ci / terraform-validate-ci (pull_request) Has been cancelled
Clean up deployment / cleanup-namespace (pull_request) Has been cancelled
Continuous Integration - Main/Release / code-tests (push) Has been cancelled
Continuous Integration - Main/Release / deployment-tests (local-code) (push) Has been cancelled
helm-chart-ci / helm-chart-ci (push) Has been cancelled
kubevious-manifests-ci / kubevious-manifests-ci (push) Has been cancelled
kustomize-build-ci / kustomize-build-ci (push) Has been cancelled
terraform-validate-ci / terraform-validate-ci (push) Has been cancelled
Some checks failed
Continuous Integration - Pull Request / code-tests (pull_request) Has been cancelled
Continuous Integration - Pull Request / deployment-tests (local-code) (pull_request) Has been cancelled
helm-chart-ci / helm-chart-ci (pull_request) Has been cancelled
kubevious-manifests-ci / kubevious-manifests-ci (pull_request) Has been cancelled
kustomize-build-ci / kustomize-build-ci (pull_request) Has been cancelled
terraform-validate-ci / terraform-validate-ci (pull_request) Has been cancelled
Clean up deployment / cleanup-namespace (pull_request) Has been cancelled
Continuous Integration - Main/Release / code-tests (push) Has been cancelled
Continuous Integration - Main/Release / deployment-tests (local-code) (push) Has been cancelled
helm-chart-ci / helm-chart-ci (push) Has been cancelled
kubevious-manifests-ci / kubevious-manifests-ci (push) Has been cancelled
kustomize-build-ci / kustomize-build-ci (push) Has been cancelled
terraform-validate-ci / terraform-validate-ci (push) Has been cancelled
This commit is contained in:
48
src/cartservice/cartservice.sln
Normal file
48
src/cartservice/cartservice.sln
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26124.0
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cartservice", "src\cartservice.csproj", "{2348C29F-E8D3-4955-916D-D609CBC97FCB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cartservice.tests", "tests\cartservice.tests.csproj", "{59825342-CE64-4AFA-8744-781692C0811B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
6
src/cartservice/src/.dockerignore
Normal file
6
src/cartservice/src/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
**/*.sh
|
||||
**/*.bat
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/out/
|
||||
Dockerfile*
|
||||
41
src/cartservice/src/Dockerfile
Normal file
41
src/cartservice/src/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# 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.
|
||||
|
||||
# https://mcr.microsoft.com/product/dotnet/sdk
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.100-noble@sha256:c7445f141c04f1a6b454181bd098dcfa606c61ba0bd213d0a702489e5bd4cd71 AS builder
|
||||
ARG TARGETARCH
|
||||
WORKDIR /app
|
||||
COPY cartservice.csproj .
|
||||
RUN dotnet restore cartservice.csproj \
|
||||
-a $TARGETARCH
|
||||
COPY . .
|
||||
RUN dotnet publish cartservice.csproj \
|
||||
-p:PublishSingleFile=true \
|
||||
-a $TARGETARCH \
|
||||
--self-contained true \
|
||||
-p:PublishTrimmed=true \
|
||||
-p:TrimMode=full \
|
||||
-c release \
|
||||
-o /cartservice
|
||||
|
||||
# https://mcr.microsoft.com/product/dotnet/runtime-deps
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0.0-noble-chiseled@sha256:b857c8cb8d929183cfe4c6dd9994abba92a2639dd2dbaf06005379f815991604
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /cartservice .
|
||||
EXPOSE 7070
|
||||
ENV DOTNET_EnableDiagnostics=0 \
|
||||
ASPNETCORE_HTTP_PORTS=7070
|
||||
USER 1000
|
||||
ENTRYPOINT ["/app/cartservice"]
|
||||
33
src/cartservice/src/Dockerfile.debug
Normal file
33
src/cartservice/src/Dockerfile.debug
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright 2021 Google LLC
|
||||
#
|
||||
# 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.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:25d14b400b75fa4e89d5bd4487a92a604a4e409ab65becb91821e7dc4ac7f81f AS build
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN dotnet restore cartservice.csproj
|
||||
RUN dotnet build "./cartservice.csproj" -c Debug -o /out
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish cartservice.csproj -c Debug -o /out
|
||||
|
||||
# Building final image used in running container
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:1aacc8154bc3071349907dae26849df301188be1a2e1f4560b903fb6275e481a AS final
|
||||
# Installing procps on the container to enable debugging of .NET Core
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y unzip procps wget
|
||||
WORKDIR /app
|
||||
COPY --from=publish /out .
|
||||
ENV ASPNETCORE_HTTP_PORTS=7070
|
||||
|
||||
ENTRYPOINT ["dotnet", "cartservice.dll"]
|
||||
26
src/cartservice/src/Program.cs
Normal file
26
src/cartservice/src/Program.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// 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 Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using cartservice;
|
||||
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
|
||||
static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
84
src/cartservice/src/Startup.cs
Normal file
84
src/cartservice/src/Startup.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using cartservice.cartstore;
|
||||
using cartservice.services;
|
||||
using Microsoft.Extensions.Caching.StackExchangeRedis;
|
||||
|
||||
namespace cartservice
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
string redisAddress = Configuration["REDIS_ADDR"];
|
||||
string spannerProjectId = Configuration["SPANNER_PROJECT"];
|
||||
string spannerConnectionString = Configuration["SPANNER_CONNECTION_STRING"];
|
||||
string alloyDBConnectionString = Configuration["ALLOYDB_PRIMARY_IP"];
|
||||
|
||||
if (!string.IsNullOrEmpty(redisAddress))
|
||||
{
|
||||
services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = redisAddress;
|
||||
});
|
||||
services.AddSingleton<ICartStore, RedisCartStore>();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(spannerProjectId) || !string.IsNullOrEmpty(spannerConnectionString))
|
||||
{
|
||||
services.AddSingleton<ICartStore, SpannerCartStore>();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(alloyDBConnectionString))
|
||||
{
|
||||
Console.WriteLine("Creating AlloyDB cart store");
|
||||
services.AddSingleton<ICartStore, AlloyDBCartStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Redis cache host(hostname+port) was not specified. Starting a cart service using in memory store");
|
||||
services.AddDistributedMemoryCache();
|
||||
services.AddSingleton<ICartStore, RedisCartStore>();
|
||||
}
|
||||
|
||||
|
||||
services.AddGrpc();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapGrpcService<CartService>();
|
||||
endpoints.MapGrpcService<cartservice.services.HealthCheckService>();
|
||||
|
||||
endpoints.MapGet("/", async context =>
|
||||
{
|
||||
await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/cartservice/src/appsettings.json
Normal file
15
src/cartservice/src/appsettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/cartservice/src/cartservice.csproj
Normal file
19
src/cartservice/src/cartservice.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
||||
<PackageReference Include="Grpc.HealthCheck" Version="2.76.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
|
||||
<PackageReference Include="Google.Cloud.Spanner.Data" Version="5.12.0" />
|
||||
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||
<PackageReference Include="Google.Cloud.SecretManager.V1" Version="2.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="protos\Cart.proto" GrpcServices="Both" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
177
src/cartservice/src/cartstore/AlloyDBCartStore.cs
Normal file
177
src/cartservice/src/cartstore/AlloyDBCartStore.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// 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 Grpc.Core;
|
||||
using Npgsql;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Api.Gax.ResourceNames;
|
||||
using Google.Cloud.SecretManager.V1;
|
||||
|
||||
namespace cartservice.cartstore
|
||||
{
|
||||
public class AlloyDBCartStore : ICartStore
|
||||
{
|
||||
private readonly string tableName;
|
||||
private readonly string connectionString;
|
||||
|
||||
public AlloyDBCartStore(IConfiguration configuration)
|
||||
{
|
||||
// Create a Cloud Secrets client.
|
||||
SecretManagerServiceClient client = SecretManagerServiceClient.Create();
|
||||
var projectId = configuration["PROJECT_ID"];
|
||||
var secretId = configuration["ALLOYDB_SECRET_NAME"];
|
||||
SecretVersionName secretVersionName = new SecretVersionName(projectId, secretId, "latest");
|
||||
|
||||
AccessSecretVersionResponse result = client.AccessSecretVersion(secretVersionName);
|
||||
// Convert the payload to a string. Payloads are bytes by default.
|
||||
string alloyDBPassword = result.Payload.Data.ToStringUtf8().TrimEnd('\r', '\n');
|
||||
|
||||
// TODO: Create a separate user for connecting within the application
|
||||
// rather than using our superuser
|
||||
string alloyDBUser = "postgres";
|
||||
string databaseName = configuration["ALLOYDB_DATABASE_NAME"];
|
||||
// TODO: Consider splitting workloads into read vs. write and take
|
||||
// advantage of the AlloyDB read pools
|
||||
string primaryIPAddress = configuration["ALLOYDB_PRIMARY_IP"];
|
||||
connectionString = "Host=" +
|
||||
primaryIPAddress +
|
||||
";Username=" +
|
||||
alloyDBUser +
|
||||
";Password=" +
|
||||
alloyDBPassword +
|
||||
";Database=" +
|
||||
databaseName;
|
||||
|
||||
tableName = configuration["ALLOYDB_TABLE_NAME"];
|
||||
}
|
||||
|
||||
|
||||
public async Task AddItemAsync(string userId, string productId, int quantity)
|
||||
{
|
||||
Console.WriteLine($"AddItemAsync for {userId} called");
|
||||
try
|
||||
{
|
||||
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||
|
||||
var fetchCmd = $"SELECT quantity FROM {tableName} WHERE userID='{userId}' AND productID='{productId}'";
|
||||
var currentQuantity = 0;
|
||||
var cmdRead = dataSource.CreateCommand(fetchCmd);
|
||||
await using (var reader = await cmdRead.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
currentQuantity += reader.GetInt32(0);
|
||||
}
|
||||
|
||||
var totalQuantity = quantity + currentQuantity;
|
||||
|
||||
// Use INSERT ... ON CONFLICT to prevent duplicate key error
|
||||
var insertCmd = $@"
|
||||
INSERT INTO {tableName} (userId, productId, quantity)
|
||||
VALUES ('{userId}', '{productId}', {totalQuantity})
|
||||
ON CONFLICT (userId, productId)
|
||||
DO UPDATE SET quantity = {totalQuantity};
|
||||
";
|
||||
|
||||
await using (var cmdInsert = dataSource.CreateCommand(insertCmd))
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return cmdInsert.ExecuteNonQueryAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(
|
||||
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
|
||||
{
|
||||
Console.WriteLine($"GetCartAsync called for userId={userId}");
|
||||
Hipstershop.Cart cart = new();
|
||||
cart.UserId = userId;
|
||||
try
|
||||
{
|
||||
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||
|
||||
var cartFetchCmd = $"SELECT productId, quantity FROM {tableName} WHERE userId = '{userId}'";
|
||||
var cmd = dataSource.CreateCommand(cartFetchCmd);
|
||||
await using (var reader = await cmd.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
Hipstershop.CartItem item = new()
|
||||
{
|
||||
ProductId = reader.GetString(0),
|
||||
Quantity = reader.GetInt32(1)
|
||||
};
|
||||
cart.Items.Add(item);
|
||||
}
|
||||
}
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return cart;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(
|
||||
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
|
||||
public async Task EmptyCartAsync(string userId)
|
||||
{
|
||||
Console.WriteLine($"EmptyCartAsync called for userId={userId}");
|
||||
|
||||
try
|
||||
{
|
||||
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||
var deleteCmd = $"DELETE FROM {tableName} WHERE userID = '{userId}'";
|
||||
await using (var cmd = dataSource.CreateCommand(deleteCmd))
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return cmd.ExecuteNonQueryAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(
|
||||
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
public bool Ping()
|
||||
{
|
||||
try
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
src/cartservice/src/cartstore/ICartStore.cs
Normal file
26
src/cartservice/src/cartstore/ICartStore.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
namespace cartservice.cartstore
|
||||
{
|
||||
public interface ICartStore
|
||||
{
|
||||
Task AddItemAsync(string userId, string productId, int quantity);
|
||||
Task EmptyCartAsync(string userId);
|
||||
Task<Hipstershop.Cart> GetCartAsync(string userId);
|
||||
bool Ping();
|
||||
}
|
||||
}
|
||||
118
src/cartservice/src/cartstore/RedisCartStore.cs
Normal file
118
src/cartservice/src/cartstore/RedisCartStore.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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 System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Google.Protobuf;
|
||||
|
||||
namespace cartservice.cartstore
|
||||
{
|
||||
public class RedisCartStore : ICartStore
|
||||
{
|
||||
private readonly IDistributedCache _cache;
|
||||
|
||||
public RedisCartStore(IDistributedCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async Task AddItemAsync(string userId, string productId, int quantity)
|
||||
{
|
||||
Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}");
|
||||
|
||||
try
|
||||
{
|
||||
Hipstershop.Cart cart;
|
||||
var value = await _cache.GetAsync(userId);
|
||||
if (value == null)
|
||||
{
|
||||
cart = new Hipstershop.Cart();
|
||||
cart.UserId = userId;
|
||||
cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity });
|
||||
}
|
||||
else
|
||||
{
|
||||
cart = Hipstershop.Cart.Parser.ParseFrom(value);
|
||||
var existingItem = cart.Items.SingleOrDefault(i => i.ProductId == productId);
|
||||
if (existingItem == null)
|
||||
{
|
||||
cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity });
|
||||
}
|
||||
else
|
||||
{
|
||||
existingItem.Quantity += quantity;
|
||||
}
|
||||
}
|
||||
await _cache.SetAsync(userId, cart.ToByteArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EmptyCartAsync(string userId)
|
||||
{
|
||||
Console.WriteLine($"EmptyCartAsync called with userId={userId}");
|
||||
|
||||
try
|
||||
{
|
||||
var cart = new Hipstershop.Cart();
|
||||
await _cache.SetAsync(userId, cart.ToByteArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
|
||||
{
|
||||
Console.WriteLine($"GetCartAsync called with userId={userId}");
|
||||
|
||||
try
|
||||
{
|
||||
// Access the cart from the cache
|
||||
var value = await _cache.GetAsync(userId);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
return Hipstershop.Cart.Parser.ParseFrom(value);
|
||||
}
|
||||
|
||||
// We decided to return empty cart in cases when user wasn't in the cache before
|
||||
return new Hipstershop.Cart();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
public bool Ping()
|
||||
{
|
||||
try
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/cartservice/src/cartstore/SpannerCartStore.cs
Normal file
185
src/cartservice/src/cartstore/SpannerCartStore.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// 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 Google.Cloud.Spanner.Data;
|
||||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace cartservice.cartstore
|
||||
{
|
||||
public class SpannerCartStore : ICartStore
|
||||
{
|
||||
private static readonly string TableName = "CartItems";
|
||||
private static readonly string DefaultInstanceName = "onlineboutique";
|
||||
private static readonly string DefaultDatabaseName = "carts";
|
||||
private readonly string databaseString;
|
||||
|
||||
public SpannerCartStore(IConfiguration configuration)
|
||||
{
|
||||
string spannerProjectId = configuration["SPANNER_PROJECT"];
|
||||
string spannerInstanceId = configuration["SPANNER_INSTANCE"];
|
||||
string spannerDatabaseId = configuration["SPANNER_DATABASE"];
|
||||
string spannerConnectionString = configuration["SPANNER_CONNECTION_STRING"];
|
||||
SpannerConnectionStringBuilder builder = new();
|
||||
if (!string.IsNullOrEmpty(spannerConnectionString)) {
|
||||
builder.DataSource = spannerConnectionString;
|
||||
databaseString = builder.ToString();
|
||||
Console.WriteLine($"Spanner connection string: ${databaseString}");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(spannerInstanceId))
|
||||
spannerInstanceId = DefaultInstanceName;
|
||||
if (string.IsNullOrEmpty(spannerDatabaseId))
|
||||
spannerDatabaseId = DefaultDatabaseName;
|
||||
builder.DataSource =
|
||||
$"projects/{spannerProjectId}/instances/{spannerInstanceId}/databases/{spannerDatabaseId}";
|
||||
databaseString = builder.ToString();
|
||||
Console.WriteLine($"Built Spanner connection string: '{databaseString}'");
|
||||
}
|
||||
|
||||
|
||||
public async Task AddItemAsync(string userId, string productId, int quantity)
|
||||
{
|
||||
Console.WriteLine($"AddItemAsync for {userId} called");
|
||||
try
|
||||
{
|
||||
using SpannerConnection spannerConnection = new(databaseString);
|
||||
await spannerConnection.RunWithRetriableTransactionAsync(async transaction =>
|
||||
{
|
||||
int currentQuantity = 0;
|
||||
var quantityLookup = spannerConnection.CreateSelectCommand(
|
||||
$"SELECT * FROM {TableName} WHERE userId = @userId AND productId = @productId",
|
||||
new SpannerParameterCollection
|
||||
{
|
||||
{ "userId", SpannerDbType.String },
|
||||
{ "productId", SpannerDbType.String }
|
||||
});
|
||||
quantityLookup.Parameters["userId"].Value = userId;
|
||||
quantityLookup.Parameters["productId"].Value = productId;
|
||||
quantityLookup.Transaction = transaction;
|
||||
using (var reader = await quantityLookup.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync()) {
|
||||
currentQuantity += reader.GetFieldValue<int>("quantity");
|
||||
}
|
||||
}
|
||||
|
||||
var cmd = spannerConnection.CreateInsertOrUpdateCommand(TableName,
|
||||
new SpannerParameterCollection
|
||||
{
|
||||
{ "userId", SpannerDbType.String },
|
||||
{ "productId", SpannerDbType.String },
|
||||
{ "quantity", SpannerDbType.Int64 }
|
||||
});
|
||||
cmd.Parameters["userId"].Value = userId;
|
||||
cmd.Parameters["productId"].Value = productId;
|
||||
cmd.Parameters["quantity"].Value = currentQuantity + quantity;
|
||||
cmd.Transaction = transaction;
|
||||
await Task.Run(() =>
|
||||
{
|
||||
return cmd.ExecuteNonQueryAsync();
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(
|
||||
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
|
||||
{
|
||||
Console.WriteLine($"GetCartAsync called for userId={userId}");
|
||||
Hipstershop.Cart cart = new();
|
||||
try
|
||||
{
|
||||
using SpannerConnection spannerConnection = new(databaseString);
|
||||
var cmd = spannerConnection.CreateSelectCommand(
|
||||
$"SELECT * FROM {TableName} WHERE userId = @userId",
|
||||
new SpannerParameterCollection {
|
||||
{ "userId", SpannerDbType.String }
|
||||
}
|
||||
);
|
||||
cmd.Parameters["userId"].Value = userId;
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
// Only add the userId if something is in the cart.
|
||||
// This is based on how the cartservice example behaves.
|
||||
// An empty cart has no userId attached.
|
||||
cart.UserId = userId;
|
||||
|
||||
Hipstershop.CartItem item = new()
|
||||
{
|
||||
ProductId = reader.GetFieldValue<string>("productId"),
|
||||
Quantity = reader.GetFieldValue<int>("quantity")
|
||||
};
|
||||
cart.Items.Add(item);
|
||||
}
|
||||
|
||||
return cart;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(
|
||||
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task EmptyCartAsync(string userId)
|
||||
{
|
||||
Console.WriteLine($"EmptyCartAsync called for userId={userId}");
|
||||
|
||||
try
|
||||
{
|
||||
using SpannerConnection spannerConnection = new(databaseString);
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var cmd = spannerConnection.CreateDmlCommand(
|
||||
$"DELETE FROM {TableName} WHERE userId = @userId",
|
||||
new SpannerParameterCollection
|
||||
{
|
||||
{ "userId", SpannerDbType.String }
|
||||
});
|
||||
cmd.Parameters["userId"].Value = userId;
|
||||
return cmd.ExecuteNonQueryAsync();
|
||||
});
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new RpcException(
|
||||
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
|
||||
}
|
||||
}
|
||||
|
||||
public bool Ping()
|
||||
{
|
||||
try
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
src/cartservice/src/protos/Cart.proto
Normal file
50
src/cartservice/src/protos/Cart.proto
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package hipstershop;
|
||||
|
||||
// -----------------Cart service-----------------
|
||||
|
||||
service CartService {
|
||||
rpc AddItem(AddItemRequest) returns (Empty) {}
|
||||
rpc GetCart(GetCartRequest) returns (Cart) {}
|
||||
rpc EmptyCart(EmptyCartRequest) returns (Empty) {}
|
||||
}
|
||||
|
||||
message CartItem {
|
||||
string product_id = 1;
|
||||
int32 quantity = 2;
|
||||
}
|
||||
|
||||
message AddItemRequest {
|
||||
string user_id = 1;
|
||||
CartItem item = 2;
|
||||
}
|
||||
|
||||
message EmptyCartRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
|
||||
message GetCartRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
|
||||
message Cart {
|
||||
string user_id = 1;
|
||||
repeated CartItem items = 2;
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
51
src/cartservice/src/services/CartService.cs
Normal file
51
src/cartservice/src/services/CartService.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// 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 System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using cartservice.cartstore;
|
||||
using Hipstershop;
|
||||
|
||||
namespace cartservice.services
|
||||
{
|
||||
public class CartService : Hipstershop.CartService.CartServiceBase
|
||||
{
|
||||
private readonly static Empty Empty = new Empty();
|
||||
private readonly ICartStore _cartStore;
|
||||
|
||||
public CartService(ICartStore cartStore)
|
||||
{
|
||||
_cartStore = cartStore;
|
||||
}
|
||||
|
||||
public async override Task<Empty> AddItem(AddItemRequest request, ServerCallContext context)
|
||||
{
|
||||
await _cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity);
|
||||
return Empty;
|
||||
}
|
||||
|
||||
public override Task<Cart> GetCart(GetCartRequest request, ServerCallContext context)
|
||||
{
|
||||
return _cartStore.GetCartAsync(request.UserId);
|
||||
}
|
||||
|
||||
public async override Task<Empty> EmptyCart(EmptyCartRequest request, ServerCallContext context)
|
||||
{
|
||||
await _cartStore.EmptyCartAsync(request.UserId);
|
||||
return Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/cartservice/src/services/HealthCheckService.cs
Normal file
41
src/cartservice/src/services/HealthCheckService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// 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 System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
using Grpc.Health.V1;
|
||||
using static Grpc.Health.V1.Health;
|
||||
using cartservice.cartstore;
|
||||
|
||||
namespace cartservice.services
|
||||
{
|
||||
internal class HealthCheckService : HealthBase
|
||||
{
|
||||
private ICartStore _cartStore { get; }
|
||||
|
||||
public HealthCheckService (ICartStore cartStore)
|
||||
{
|
||||
_cartStore = cartStore;
|
||||
}
|
||||
|
||||
public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
|
||||
{
|
||||
Console.WriteLine ("Checking CartService Health");
|
||||
return Task.FromResult(new HealthCheckResponse {
|
||||
Status = _cartStore.Ping() ? HealthCheckResponse.Types.ServingStatus.Serving : HealthCheckResponse.Types.ServingStatus.NotServing
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/cartservice/tests/CartServiceTests.cs
Normal file
160
src/cartservice/tests/CartServiceTests.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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 System.Threading.Tasks;
|
||||
using Grpc.Net.Client;
|
||||
using Hipstershop;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
using static Hipstershop.CartService;
|
||||
|
||||
namespace cartservice.tests
|
||||
{
|
||||
public class CartServiceTests
|
||||
{
|
||||
private readonly IHostBuilder _host;
|
||||
|
||||
public CartServiceTests()
|
||||
{
|
||||
_host = new HostBuilder().ConfigureWebHost(webBuilder =>
|
||||
{
|
||||
webBuilder
|
||||
.UseStartup<Startup>()
|
||||
.UseTestServer();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetItem_NoAddItemBefore_EmptyCartReturned()
|
||||
{
|
||||
// Setup test server and client
|
||||
using var server = await _host.StartAsync();
|
||||
var httpClient = server.GetTestClient();
|
||||
|
||||
string userId = Guid.NewGuid().ToString();
|
||||
|
||||
// Create a GRPC communication channel between the client and the server
|
||||
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
|
||||
{
|
||||
HttpClient = httpClient
|
||||
});
|
||||
|
||||
var cartClient = new CartServiceClient(channel);
|
||||
|
||||
var request = new GetCartRequest
|
||||
{
|
||||
UserId = userId,
|
||||
};
|
||||
|
||||
var cart = await cartClient.GetCartAsync(request);
|
||||
Assert.NotNull(cart);
|
||||
|
||||
// All grpc objects implement IEquitable, so we can compare equality with by-value semantics
|
||||
Assert.Equal(new Cart(), cart);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddItem_ItemExists_Updated()
|
||||
{
|
||||
// Setup test server and client
|
||||
using var server = await _host.StartAsync();
|
||||
var httpClient = server.GetTestClient();
|
||||
|
||||
string userId = Guid.NewGuid().ToString();
|
||||
|
||||
// Create a GRPC communication channel between the client and the server
|
||||
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
|
||||
{
|
||||
HttpClient = httpClient
|
||||
});
|
||||
|
||||
var client = new CartServiceClient(channel);
|
||||
var request = new AddItemRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Item = new CartItem
|
||||
{
|
||||
ProductId = "1",
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
|
||||
// First add - nothing should fail
|
||||
await client.AddItemAsync(request);
|
||||
|
||||
// Second add of existing product - quantity should be updated
|
||||
await client.AddItemAsync(request);
|
||||
|
||||
var getCartRequest = new GetCartRequest
|
||||
{
|
||||
UserId = userId
|
||||
};
|
||||
var cart = await client.GetCartAsync(getCartRequest);
|
||||
Assert.NotNull(cart);
|
||||
Assert.Equal(userId, cart.UserId);
|
||||
Assert.Single(cart.Items);
|
||||
Assert.Equal(2, cart.Items[0].Quantity);
|
||||
|
||||
// Cleanup
|
||||
await client.EmptyCartAsync(new EmptyCartRequest { UserId = userId });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddItem_New_Inserted()
|
||||
{
|
||||
// Setup test server and client
|
||||
using var server = await _host.StartAsync();
|
||||
var httpClient = server.GetTestClient();
|
||||
|
||||
string userId = Guid.NewGuid().ToString();
|
||||
|
||||
// Create a GRPC communication channel between the client and the server
|
||||
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
|
||||
{
|
||||
HttpClient = httpClient
|
||||
});
|
||||
|
||||
// Create a proxy object to work with the server
|
||||
var client = new CartServiceClient(channel);
|
||||
|
||||
var request = new AddItemRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Item = new CartItem
|
||||
{
|
||||
ProductId = "1",
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
|
||||
await client.AddItemAsync(request);
|
||||
|
||||
var getCartRequest = new GetCartRequest
|
||||
{
|
||||
UserId = userId
|
||||
};
|
||||
var cart = await client.GetCartAsync(getCartRequest);
|
||||
Assert.NotNull(cart);
|
||||
Assert.Equal(userId, cart.UserId);
|
||||
Assert.Single(cart.Items);
|
||||
|
||||
await client.EmptyCartAsync(new EmptyCartRequest { UserId = userId });
|
||||
cart = await client.GetCartAsync(getCartRequest);
|
||||
Assert.Empty(cart.Items);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/cartservice/tests/cartservice.tests.csproj
Normal file
20
src/cartservice/tests/cartservice.tests.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\cartservice.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user