Skip to content

Commit c7cedb8

Browse files
feat: Add Google Cloud Storage API (fake-gcs-server) module (#1023)
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
1 parent dfbaef6 commit c7cedb8

11 files changed

+279
-0
lines changed

Testcontainers.sln

+14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearc
2929
EndProject
3030
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb", "src\Testcontainers.EventStoreDb\Testcontainers.EventStoreDb.csproj", "{84D707E0-C9FA-4327-85DC-0AFEBEA73572}"
3131
EndProject
32+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer", "src\Testcontainers.FakeGcsServer\Testcontainers.FakeGcsServer.csproj", "{FF86B509-2F9E-4269-ABC2-912B3339DE29}"
33+
EndProject
3234
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore", "src\Testcontainers.Firestore\Testcontainers.Firestore.csproj", "{B3CC460D-0DFD-48A8-9502-54E9828B7B05}"
3335
EndProject
3436
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb", "src\Testcontainers.InfluxDb\Testcontainers.InfluxDb.csproj", "{8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}"
@@ -97,6 +99,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearc
9799
EndProject
98100
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb.Tests", "tests\Testcontainers.EventStoreDb.Tests\Testcontainers.EventStoreDb.Tests.csproj", "{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}"
99101
EndProject
102+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer.Tests", "tests\Testcontainers.FakeGcsServer.Tests\Testcontainers.FakeGcsServer.Tests.csproj", "{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}"
103+
EndProject
100104
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore.Tests", "tests\Testcontainers.Firestore.Tests\Testcontainers.Firestore.Tests.csproj", "{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}"
101105
EndProject
102106
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb.Tests", "tests\Testcontainers.InfluxDb.Tests\Testcontainers.InfluxDb.Tests.csproj", "{B45B0EF2-5852-4ED3-904A-8FC62A3253D7}"
@@ -192,6 +196,10 @@ Global
192196
{84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Debug|Any CPU.Build.0 = Debug|Any CPU
193197
{84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Release|Any CPU.ActiveCfg = Release|Any CPU
194198
{84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Release|Any CPU.Build.0 = Release|Any CPU
199+
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
200+
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Debug|Any CPU.Build.0 = Debug|Any CPU
201+
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Release|Any CPU.ActiveCfg = Release|Any CPU
202+
{FF86B509-2F9E-4269-ABC2-912B3339DE29}.Release|Any CPU.Build.0 = Release|Any CPU
195203
{B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
196204
{B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.Build.0 = Debug|Any CPU
197205
{B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -328,6 +336,10 @@ Global
328336
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
329337
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
330338
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Release|Any CPU.Build.0 = Release|Any CPU
339+
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
340+
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
341+
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
342+
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Release|Any CPU.Build.0 = Release|Any CPU
331343
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
332344
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Debug|Any CPU.Build.0 = Debug|Any CPU
333345
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -446,6 +458,7 @@ Global
446458
{2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
447459
{641DDEA5-B6E0-41E6-BA11-7A28C0913127} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
448460
{84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
461+
{FF86B509-2F9E-4269-ABC2-912B3339DE29} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
449462
{B3CC460D-0DFD-48A8-9502-54E9828B7B05} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
450463
{8F483B83-7BD4-4BD5-9F03-DFC26E1CE678} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
451464
{111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -480,6 +493,7 @@ Global
480493
{101515E6-74C1-40F9-85C8-871F742A378D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
481494
{DD5B3678-468F-4D73-AECE-705E3D66CD43} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
482495
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
496+
{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
483497
{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
484498
{B45B0EF2-5852-4ED3-904A-8FC62A3253D7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
485499
{F0F40AE2-70FF-4191-ADDA-26A19E0D1A0F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
namespace Testcontainers.FakeGcsServer;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class FakeGcsServerBuilder : ContainerBuilder<FakeGcsServerBuilder, FakeGcsServerContainer, FakeGcsServerConfiguration>
6+
{
7+
public const string FakeGcsServerImage = "fsouza/fake-gcs-server:1.47.5";
8+
9+
public const ushort FakeGcsServerPort = 4443;
10+
11+
public const string StartupScriptFilePath = "/testcontainers.sh";
12+
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="FakeGcsServerBuilder" /> class.
15+
/// </summary>
16+
public FakeGcsServerBuilder()
17+
: this(new FakeGcsServerConfiguration())
18+
{
19+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
20+
}
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="FakeGcsServerBuilder" /> class.
24+
/// </summary>
25+
/// <param name="dockerResourceConfiguration">The Docker resource configuration.</param>
26+
private FakeGcsServerBuilder(FakeGcsServerConfiguration dockerResourceConfiguration)
27+
: base(dockerResourceConfiguration)
28+
{
29+
DockerResourceConfiguration = dockerResourceConfiguration;
30+
}
31+
32+
/// <inheritdoc />
33+
protected override FakeGcsServerConfiguration DockerResourceConfiguration { get; }
34+
35+
/// <inheritdoc />
36+
public override FakeGcsServerContainer Build()
37+
{
38+
Validate();
39+
return new FakeGcsServerContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
40+
}
41+
42+
/// <inheritdoc />
43+
protected override FakeGcsServerBuilder Init()
44+
{
45+
return base.Init()
46+
.WithImage(FakeGcsServerImage)
47+
.WithPortBinding(FakeGcsServerPort, true)
48+
.WithEntrypoint("/bin/sh", "-c")
49+
.WithCommand("while [ ! -f " + StartupScriptFilePath + " ]; do sleep 0.1; done; " + StartupScriptFilePath)
50+
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("server started"))
51+
.WithStartupCallback((container, ct) =>
52+
{
53+
const char lf = '\n';
54+
var startupScript = new StringBuilder();
55+
startupScript.Append("#!/bin/sh");
56+
startupScript.Append(lf);
57+
startupScript.Append("fake-gcs-server ");
58+
startupScript.Append("-backend memory ");
59+
startupScript.Append("-scheme http ");
60+
// If we do not remove the trailing slash, uploading an object will result in an
61+
// error: HttpStatusCode.NotFound. The HTTP request appears incorrect. The
62+
// container logs indicate the presence of an extra slash: `PUT //upload/storage/v1`.
63+
startupScript.Append("-external-url " + new UriBuilder(Uri.UriSchemeHttp, container.Hostname, container.GetMappedPublicPort(FakeGcsServerPort)).ToString().Trim('/'));
64+
return container.CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), StartupScriptFilePath, Unix.FileMode755, ct);
65+
});
66+
}
67+
68+
/// <inheritdoc />
69+
protected override FakeGcsServerBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
70+
{
71+
return Merge(DockerResourceConfiguration, new FakeGcsServerConfiguration(resourceConfiguration));
72+
}
73+
74+
/// <inheritdoc />
75+
protected override FakeGcsServerBuilder Clone(IContainerConfiguration resourceConfiguration)
76+
{
77+
return Merge(DockerResourceConfiguration, new FakeGcsServerConfiguration(resourceConfiguration));
78+
}
79+
80+
/// <inheritdoc />
81+
protected override FakeGcsServerBuilder Merge(FakeGcsServerConfiguration oldValue, FakeGcsServerConfiguration newValue)
82+
{
83+
return new FakeGcsServerBuilder(new FakeGcsServerConfiguration(oldValue, newValue));
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace Testcontainers.FakeGcsServer;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class FakeGcsServerConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
9+
/// </summary>
10+
public FakeGcsServerConfiguration()
11+
{
12+
}
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
16+
/// </summary>
17+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
18+
public FakeGcsServerConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
19+
: base(resourceConfiguration)
20+
{
21+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
26+
/// </summary>
27+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
28+
public FakeGcsServerConfiguration(IContainerConfiguration resourceConfiguration)
29+
: base(resourceConfiguration)
30+
{
31+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
32+
}
33+
34+
/// <summary>
35+
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
36+
/// </summary>
37+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
38+
public FakeGcsServerConfiguration(FakeGcsServerConfiguration resourceConfiguration)
39+
: this(new FakeGcsServerConfiguration(), resourceConfiguration)
40+
{
41+
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
42+
}
43+
44+
/// <summary>
45+
/// Initializes a new instance of the <see cref="FakeGcsServerConfiguration" /> class.
46+
/// </summary>
47+
/// <param name="oldValue">The old Docker resource configuration.</param>
48+
/// <param name="newValue">The new Docker resource configuration.</param>
49+
public FakeGcsServerConfiguration(FakeGcsServerConfiguration oldValue, FakeGcsServerConfiguration newValue)
50+
: base(oldValue, newValue)
51+
{
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Testcontainers.FakeGcsServer;
2+
3+
/// <inheritdoc cref="DockerContainer" />
4+
[PublicAPI]
5+
public sealed class FakeGcsServerContainer : DockerContainer
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="FakeGcsServerContainer" /> class.
9+
/// </summary>
10+
/// <param name="configuration">The container configuration.</param>
11+
/// <param name="logger">The logger.</param>
12+
public FakeGcsServerContainer(FakeGcsServerConfiguration configuration, ILogger logger)
13+
: base(configuration, logger)
14+
{
15+
}
16+
17+
/// <summary>
18+
/// Gets the FakeGcsServer connection string.
19+
/// </summary>
20+
/// <returns>The FakeGcsServer connection string.</returns>
21+
public string GetConnectionString()
22+
{
23+
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(FakeGcsServerBuilder.FakeGcsServerPort), "/storage/v1/").ToString();
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
4+
<LangVersion>latest</LangVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
8+
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
9+
</ItemGroup>
10+
<ItemGroup>
11+
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
12+
</ItemGroup>
13+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
global using System;
2+
global using System.Text;
3+
global using Docker.DotNet.Models;
4+
global using DotNet.Testcontainers.Builders;
5+
global using DotNet.Testcontainers.Configurations;
6+
global using DotNet.Testcontainers.Containers;
7+
global using JetBrains.Annotations;
8+
global using Microsoft.Extensions.Logging;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
namespace Testcontainers.FakeGcsServer;
2+
3+
public sealed class FakeGcsServerContainerTest : IAsyncLifetime
4+
{
5+
private readonly FakeGcsServerContainer _fakeGcsServerContainer = new FakeGcsServerBuilder().Build();
6+
7+
public Task InitializeAsync()
8+
{
9+
return _fakeGcsServerContainer.StartAsync();
10+
}
11+
12+
public Task DisposeAsync()
13+
{
14+
return _fakeGcsServerContainer.DisposeAsync().AsTask();
15+
}
16+
17+
[Fact]
18+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
19+
public async Task DownloadObjectReturnsUploadObject()
20+
{
21+
// Given
22+
const string helloWorld = "Hello, World!";
23+
24+
using var writeStream = new MemoryStream(Encoding.Default.GetBytes(helloWorld));
25+
26+
using var readStream = new MemoryStream();
27+
28+
var project = Guid.NewGuid().ToString("D");
29+
30+
var bucket = Guid.NewGuid().ToString("D");
31+
32+
var fileName = Path.GetRandomFileName();
33+
34+
var storageClientBuilder = new StorageClientBuilder();
35+
storageClientBuilder.UnauthenticatedAccess = true;
36+
storageClientBuilder.BaseUri = _fakeGcsServerContainer.GetConnectionString();
37+
38+
// When
39+
var client = await storageClientBuilder.BuildAsync()
40+
.ConfigureAwait(false);
41+
42+
_ = await client.CreateBucketAsync(project, bucket)
43+
.ConfigureAwait(false);
44+
45+
_ = await client.UploadObjectAsync(bucket, fileName, "text/plain", writeStream)
46+
.ConfigureAwait(false);
47+
48+
_ = await client.DownloadObjectAsync(bucket, fileName, readStream)
49+
.ConfigureAwait(false);
50+
51+
// Then
52+
Assert.Equal(helloWorld, Encoding.Default.GetString(readStream.ToArray()));
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net6.0</TargetFrameworks>
4+
<IsPackable>false</IsPackable>
5+
<IsPublishable>false</IsPublishable>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2"/>
9+
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
10+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0"/>
11+
<PackageReference Include="xunit" Version="2.5.0"/>
12+
<PackageReference Include="Google.Cloud.Storage.V1" Version="4.6.0"/>
13+
</ItemGroup>
14+
<ItemGroup>
15+
<ProjectReference Include="$(SolutionDir)src/Testcontainers.FakeGcsServer/Testcontainers.FakeGcsServer.csproj"/>
16+
<ProjectReference Include="$(SolutionDir)tests/Testcontainers.Commons/Testcontainers.Commons.csproj"/>
17+
</ItemGroup>
18+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
global using System;
2+
global using System.IO;
3+
global using System.Text;
4+
global using System.Threading.Tasks;
5+
global using DotNet.Testcontainers.Commons;
6+
global using Google.Cloud.Storage.V1;
7+
global using Xunit;

0 commit comments

Comments
 (0)