Skip to content

Commit

Permalink
feat: Add Google Cloud Storage API (fake-gcs-server) module (#1023)
Browse files Browse the repository at this point in the history
Co-authored-by: Andre Hofmeister <[email protected]>
  • Loading branch information
KSemenenko and HofmeisterAn authored Oct 18, 2023
1 parent dfbaef6 commit c7cedb8
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb", "src\Testcontainers.EventStoreDb\Testcontainers.EventStoreDb.csproj", "{84D707E0-C9FA-4327-85DC-0AFEBEA73572}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer", "src\Testcontainers.FakeGcsServer\Testcontainers.FakeGcsServer.csproj", "{FF86B509-2F9E-4269-ABC2-912B3339DE29}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore", "src\Testcontainers.Firestore\Testcontainers.Firestore.csproj", "{B3CC460D-0DFD-48A8-9502-54E9828B7B05}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb", "src\Testcontainers.InfluxDb\Testcontainers.InfluxDb.csproj", "{8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}"
Expand Down Expand Up @@ -97,6 +99,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb.Tests", "tests\Testcontainers.EventStoreDb.Tests\Testcontainers.EventStoreDb.Tests.csproj", "{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer.Tests", "tests\Testcontainers.FakeGcsServer.Tests\Testcontainers.FakeGcsServer.Tests.csproj", "{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore.Tests", "tests\Testcontainers.Firestore.Tests\Testcontainers.Firestore.Tests.csproj", "{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb.Tests", "tests\Testcontainers.InfluxDb.Tests\Testcontainers.InfluxDb.Tests.csproj", "{B45B0EF2-5852-4ED3-904A-8FC62A3253D7}"
Expand Down Expand Up @@ -192,6 +196,10 @@ Global
{84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Release|Any CPU.Build.0 = Release|Any CPU
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Release|Any CPU.Build.0 = Release|Any CPU
{B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -328,6 +336,10 @@ Global
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Release|Any CPU.Build.0 = Release|Any CPU
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Release|Any CPU.Build.0 = Release|Any CPU
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -446,6 +458,7 @@ Global
{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{641DDEA5-B6E0-41E6-BA11-7A28C0913127} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{FF86B509-2F9E-4269-ABC2-912B3339DE29} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{B3CC460D-0DFD-48A8-9502-54E9828B7B05} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{8F483B83-7BD4-4BD5-9F03-DFC26E1CE678} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -480,6 +493,7 @@ Global
{101515E6-74C1-40F9-85C8-871F742A378D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{DD5B3678-468F-4D73-AECE-705E3D66CD43} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{B45B0EF2-5852-4ED3-904A-8FC62A3253D7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{F0F40AE2-70FF-4191-ADDA-26A19E0D1A0F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.FakeGcsServer/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
85 changes: 85 additions & 0 deletions src/Testcontainers.FakeGcsServer/FakeGcsServerBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace Testcontainers.FakeGcsServer;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class FakeGcsServerBuilder : ContainerBuilder<FakeGcsServerBuilder, FakeGcsServerContainer, FakeGcsServerConfiguration>
{
public const string FakeGcsServerImage = "fsouza/fake-gcs-server:1.47.5";

public const ushort FakeGcsServerPort = 4443;

public const string StartupScriptFilePath = "/testcontainers.sh";

/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerBuilder" /> class.
/// </summary>
public FakeGcsServerBuilder()
: this(new FakeGcsServerConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerBuilder" /> class.
/// </summary>
/// <param name="dockerResourceConfiguration">The Docker resource configuration.</param>
private FakeGcsServerBuilder(FakeGcsServerConfiguration dockerResourceConfiguration)
: base(dockerResourceConfiguration)
{
DockerResourceConfiguration = dockerResourceConfiguration;
}

/// <inheritdoc />
protected override FakeGcsServerConfiguration DockerResourceConfiguration { get; }

/// <inheritdoc />
public override FakeGcsServerContainer Build()
{
Validate();
return new FakeGcsServerContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
}

/// <inheritdoc />
protected override FakeGcsServerBuilder Init()
{
return base.Init()
.WithImage(FakeGcsServerImage)
.WithPortBinding(FakeGcsServerPort, true)
.WithEntrypoint("/bin/sh", "-c")
.WithCommand("while [ ! -f " + StartupScriptFilePath + " ]; do sleep 0.1; done; " + StartupScriptFilePath)
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("server started"))
.WithStartupCallback((container, ct) =>
{
const char lf = '\n';
var startupScript = new StringBuilder();
startupScript.Append("#!/bin/sh");
startupScript.Append(lf);
startupScript.Append("fake-gcs-server ");
startupScript.Append("-backend memory ");
startupScript.Append("-scheme http ");
// If we do not remove the trailing slash, uploading an object will result in an
// error: HttpStatusCode.NotFound. The HTTP request appears incorrect. The
// container logs indicate the presence of an extra slash: `PUT //upload/storage/v1`.
startupScript.Append("-external-url " + new UriBuilder(Uri.UriSchemeHttp, container.Hostname, container.GetMappedPublicPort(FakeGcsServerPort)).ToString().Trim('/'));
return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), StartupScriptFilePath, Unix.FileMode755, ct);
});
}

/// <inheritdoc />
protected override FakeGcsServerBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new FakeGcsServerConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override FakeGcsServerBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new FakeGcsServerConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override FakeGcsServerBuilder Merge(FakeGcsServerConfiguration oldValue, FakeGcsServerConfiguration newValue)
{
return new FakeGcsServerBuilder(new FakeGcsServerConfiguration(oldValue, newValue));
}
}
53 changes: 53 additions & 0 deletions src/Testcontainers.FakeGcsServer/FakeGcsServerConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace Testcontainers.FakeGcsServer;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class FakeGcsServerConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
/// </summary>
public FakeGcsServerConfiguration()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FakeGcsServerConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FakeGcsServerConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FakeGcsServerConfiguration(FakeGcsServerConfiguration resourceConfiguration)
: this(new FakeGcsServerConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public FakeGcsServerConfiguration(FakeGcsServerConfiguration oldValue, FakeGcsServerConfiguration newValue)
: base(oldValue, newValue)
{
}
}
25 changes: 25 additions & 0 deletions src/Testcontainers.FakeGcsServer/FakeGcsServerContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Testcontainers.FakeGcsServer;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class FakeGcsServerContainer : DockerContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="FakeGcsServerContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public FakeGcsServerContainer(FakeGcsServerConfiguration configuration, ILogger logger)
: base(configuration, logger)
{
}

/// <summary>
/// Gets the FakeGcsServer connection string.
/// </summary>
/// <returns>The FakeGcsServer connection string.</returns>
public string GetConnectionString()
{
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(FakeGcsServerBuilder.FakeGcsServerPort), "/storage/v1/").ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
8 changes: 8 additions & 0 deletions src/Testcontainers.FakeGcsServer/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global using System;
global using System.Text;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers.Builders;
global using DotNet.Testcontainers.Configurations;
global using DotNet.Testcontainers.Containers;
global using JetBrains.Annotations;
global using Microsoft.Extensions.Logging;
1 change: 1 addition & 0 deletions tests/Testcontainers.FakeGcsServer.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace Testcontainers.FakeGcsServer;

public sealed class FakeGcsServerContainerTest : IAsyncLifetime
{
private readonly FakeGcsServerContainer _fakeGcsServerContainer = new FakeGcsServerBuilder().Build();

public Task InitializeAsync()
{
return _fakeGcsServerContainer.StartAsync();
}

public Task DisposeAsync()
{
return _fakeGcsServerContainer.DisposeAsync().AsTask();
}

[Fact]
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
public async Task DownloadObjectReturnsUploadObject()
{
// Given
const string helloWorld = "Hello, World!";

using var writeStream = new MemoryStream(Encoding.Default.GetBytes(helloWorld));

using var readStream = new MemoryStream();

var project = Guid.NewGuid().ToString("D");

var bucket = Guid.NewGuid().ToString("D");

var fileName = Path.GetRandomFileName();

var storageClientBuilder = new StorageClientBuilder();
storageClientBuilder.UnauthenticatedAccess = true;
storageClientBuilder.BaseUri = _fakeGcsServerContainer.GetConnectionString();

// When
var client = await storageClientBuilder.BuildAsync()
.ConfigureAwait(false);

_ = await client.CreateBucketAsync(project, bucket)
.ConfigureAwait(false);

_ = await client.UploadObjectAsync(bucket, fileName, "text/plain", writeStream)
.ConfigureAwait(false);

_ = await client.DownloadObjectAsync(bucket, fileName, readStream)
.ConfigureAwait(false);

// Then
Assert.Equal(helloWorld, Encoding.Default.GetString(readStream.ToArray()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0"/>
<PackageReference Include="xunit" Version="2.5.0"/>
<PackageReference Include="Google.Cloud.Storage.V1" Version="4.6.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers.FakeGcsServer/Testcontainers.FakeGcsServer.csproj"/>
<ProjectReference Include="$(SolutionDir)tests/Testcontainers.Commons/Testcontainers.Commons.csproj"/>
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions tests/Testcontainers.FakeGcsServer.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
global using System;
global using System.IO;
global using System.Text;
global using System.Threading.Tasks;
global using DotNet.Testcontainers.Commons;
global using Google.Cloud.Storage.V1;
global using Xunit;

0 comments on commit c7cedb8

Please sign in to comment.