Skip to content

Commit 6d41fb0

Browse files
author
krivchenko-kv
committed
added TimeProvider & UrlUtils
1 parent dc89ecd commit 6d41fb0

File tree

13 files changed

+256
-196
lines changed

13 files changed

+256
-196
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"BigFilePath": "",
33
"S3Storage": {
4-
"AccessKey": "ROOTUSER",
4+
"AccessKey": "admin",
55
"Bucket": "benchmark",
6-
"EndPoint": "http://localhost:5300",
7-
"SecretKey": "ChangeMe123"
6+
"EndPoint": "http://localhost:9000",
7+
"SecretKey": "password"
88
}
99
}

src/Storage.Tests/ObjectShould.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
using Microsoft.Extensions.Time.Testing;
12
using System.Net;
23
using static Storage.Tests.StorageFixture;
34

45
namespace Storage.Tests;
56

67
public sealed class ObjectShould : IClassFixture<StorageFixture>
78
{
8-
private readonly S3BucketClient _client;
9+
private readonly S3BucketClient _client;
910
private readonly CancellationToken _ct;
1011
private readonly StorageFixture _fixture;
1112
private readonly S3BucketClient _notExistsBucketClient; // don't dispose it
@@ -591,6 +592,45 @@ private async Task EnsureFileSame(string fileName, MemoryStream expectedBytes)
591592
.Should().BeTrue();
592593
}
593594

595+
596+
[Theory]
597+
[InlineData("file.txt", 3600)] // 1 hour expiration
598+
[InlineData("folder/file.txt", 7200)] // 2 hours expiration
599+
public void BuildFileUrlCorrectly(string fileName, int expirationInSeconds)
600+
{
601+
var settings = new S3BucketSettings
602+
{
603+
Bucket = "my-bucket",
604+
Endpoint = "https://s3.amazonaws.com",
605+
AccessKey = "test-access-key",
606+
SecretKey = "test-secret-key",
607+
Region = "us-east-1",
608+
Service = "s3",
609+
UseHttp2 = false
610+
};
611+
612+
var fake = new FakeTimeProvider();
613+
using var s3BucketClient = new S3BucketClient(new HttpClient(), settings, fake);
614+
615+
// Arrange
616+
fake.SetUtcNow(new DateTimeOffset(2024, 03, 15, 16, 20, 44, TimeSpan.Zero));
617+
var expiration = TimeSpan.FromSeconds(expirationInSeconds);
618+
619+
// Act
620+
var result = s3BucketClient.BuildFileUrl(fileName, expiration);
621+
622+
623+
// Assert
624+
Assert.Contains("my-bucket", result);
625+
Assert.Contains(fileName, result);
626+
Assert.Contains("X-Amz-Algorithm=AWS4-HMAC-SHA256", result);
627+
Assert.Contains("X-Amz-Credential=test-access-key", result);
628+
Assert.Contains("X-Amz-Expires=" + expirationInSeconds, result);
629+
Assert.Contains("X-Amz-Date=20240315T162044Z", result);
630+
Assert.Contains("X-Amz-SignedHeaders=", result);
631+
Assert.Contains("X-Amz-Signature=", result);
632+
}
633+
594634
private Task DeleteTestFile(string fileName)
595635
{
596636
return _client.DeleteFile(fileName, _ct);

src/Storage.Tests/Storage.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<ItemGroup>
1212
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
1313
<PackageReference Include="FluentAssertions" Version="8.2.0" />
14+
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.4.0" />
1415
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
1516
<PackageReference Include="Moq" Version="4.18.4" />
1617
<PackageReference Include="Testcontainers" Version="4.4.0" />

src/Storage.Tests/Utils/QueryParameterTests.cs

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,74 @@ public class QueryParameterTests
66
{
77

88
[Fact]
9-
public void Test_EmptyQuery()
9+
public void EmptyQuery()
1010
{
1111
var builder = new ValueStringBuilder(stackalloc char[512]);
12-
13-
StringUtils.AppendCanonicalQueryParameters(ref builder, null);
12+
UrlUtils.AppendCanonicalQueryParameters(ref builder, null);
1413
Assert.Empty(builder.ToString());
15-
16-
StringUtils.AppendCanonicalQueryParameters(ref builder, "");
14+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "");
1715
Assert.Empty(builder.ToString());
18-
19-
StringUtils.AppendCanonicalQueryParameters(ref builder, "?");
16+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "?");
2017
Assert.Empty(builder.ToString());
2118

2219
builder.Dispose();
2320
}
2421

2522
[Fact]
26-
public void Test_SingleParameter()
23+
public void SingleParameter()
2724
{
2825
var builder = new ValueStringBuilder(stackalloc char[512]);
29-
30-
StringUtils.AppendCanonicalQueryParameters(ref builder, "?key=value");
26+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "?key=value");
3127
Assert.Equal("key=value", builder.ToString());
3228

3329
builder.Dispose();
3430
}
3531

3632
[Fact]
37-
public void Test_MultipleParameters()
33+
public void MultipleParameters()
3834
{
3935
var builder = new ValueStringBuilder(stackalloc char[512]);
40-
41-
StringUtils.AppendCanonicalQueryParameters(ref builder, "?key1=value1&key2=value2");
36+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "?key1=value1&key2=value2");
4237
Assert.Equal("key1=value1&key2=value2", builder.ToString());
4338

4439
builder.Dispose();
4540
}
4641

4742
[Fact]
48-
public void Test_ParameterWithWhitespace()
43+
public void ParameterWithWhitespace()
4944
{
5045
var builder = new ValueStringBuilder(stackalloc char[512]);
51-
52-
StringUtils.AppendCanonicalQueryParameters(ref builder, "? key1 = value1 & key2 = value2 ");
46+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "? key1 = value1 & key2 = value2 ");
5347
Assert.Equal("key1%20=%20value1%20&key2%20=%20value2%20", builder.ToString());
5448

5549
builder.Dispose();
5650
}
5751

5852
[Fact]
59-
public void Test_ParameterWithoutValue()
53+
public void ParameterWithoutValue()
6054
{
6155
var builder = new ValueStringBuilder(stackalloc char[512]);
62-
63-
StringUtils.AppendCanonicalQueryParameters(ref builder, "?key1&key2=value2");
56+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "?key1&key2=value2");
6457
Assert.Equal("key1=&key2=value2", builder.ToString());
6558

6659
builder.Dispose();
6760
}
6861

6962
[Fact]
70-
public void Test_ParameterWithEmptyValue()
63+
public void ParameterWithEmptyValue()
7164
{
7265
var builder = new ValueStringBuilder(stackalloc char[512]);
73-
74-
StringUtils.AppendCanonicalQueryParameters(ref builder, "?key1=&key2=");
66+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "?key1=&key2=");
7567
Assert.Equal("key1=&key2=", builder.ToString());
7668

7769
builder.Dispose();
7870
}
7971

8072
[Fact]
81-
public void Test_ParameterWithSpecialCharacters()
73+
public void ParameterWithSpecialCharacters()
8274
{
8375
var builder = new ValueStringBuilder(stackalloc char[512]);
84-
85-
StringUtils.AppendCanonicalQueryParameters(ref builder, "?key1=value%20with%20spaces&key2=value%26with%26ampersands");
76+
UrlUtils.AppendCanonicalQueryParameters(ref builder, "?key1=value%20with%20spaces&key2=value%26with%26ampersands");
8677
Assert.Equal("key1=value%20with%20spaces&key2=value%26with%26ampersands", builder.ToString());
8778

8879
builder.Dispose();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Storage.Utils;
2+
3+
namespace Storage.Tests.Utils;
4+
5+
public class UrlUtilsTests
6+
{
7+
[Theory]
8+
[InlineData("my-bucket", null, "my-bucket")]
9+
[InlineData("my-bucket", "file.txt", "my-bucket/file.txt")]
10+
[InlineData("my-bucket", "folder/file.txt", "my-bucket/folder/file.txt")]
11+
[InlineData("my-bucket", "file with spaces.txt", "my-bucket/file%20with%20spaces.txt")]
12+
[InlineData("my-bucket", "[email protected]", "my-bucket/file%40name.txt")]
13+
public void BuildFileUrl_ShouldReturnCorrectUrl(string bucket, string? fileName, string expectedUrl)
14+
{
15+
// Act
16+
var result = UrlUtils.BuildFileUrl(bucket, fileName);
17+
18+
// Assert
19+
Assert.Equal(expectedUrl, result);
20+
}
21+
}

src/Storage/S3BucketClient.Multipart.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ private async Task<bool> ExecuteMultipartUpload(
152152

153153
private async Task<S3Upload> MultipartStart(string fileName, string contentType, CancellationToken ct)
154154
{
155-
var encodedFileName = StringUtils.UrlEncodeName(fileName);
155+
var encodedFileName = UrlUtils.UrlEncodeName(fileName);
156156

157157
HttpResponseMessage response;
158158
using (var request = new HttpRequestMessage(HttpMethod.Post, $"{_bucket}/{encodedFileName}?uploads"))

src/Storage/S3BucketClient.Transport.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public partial class S3BucketClient
1111
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1212
private HttpRequestMessage CreateRequest(HttpMethod method, string? fileName = null)
1313
{
14-
var url = StringUtils.BuildFileUrl(_bucket, fileName);
14+
var url = UrlUtils.BuildFileUrl(_bucket, fileName);
1515
return new HttpRequestMessage(method, new Uri(url, UriKind.Absolute));
1616
}
1717

@@ -22,7 +22,7 @@ private Task<HttpResponseMessage> Send(HttpRequestMessage request, string payloa
2222
Errors.Disposed();
2323
}
2424

25-
var now = DateTime.UtcNow; // TODO: !!
25+
var now = _timeProvider.GetUtcNow();
2626

2727
var headers = request.Headers;
2828
headers.Add("host", _host);

src/Storage/S3BucketClient.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,20 @@ public partial class S3BucketClient : IS3BucketClient, IDisposable
2525
private readonly string _urlMiddle;
2626
private readonly string _urlStart;
2727

28+
private readonly TimeProvider _timeProvider;
29+
2830
private bool _disposed;
2931

3032
public string Bucket { get; }
3133
public Uri Endpoint { get; }
3234

33-
public S3BucketClient(HttpClient client, S3BucketSettings settings)
35+
public S3BucketClient(HttpClient client, S3BucketSettings settings, TimeProvider? timeProvider = null)
3436
{
3537
Bucket = settings.Bucket ?? throw new ArgumentException(nameof(settings.Bucket));
3638
Endpoint = new Uri(settings.Endpoint);
3739

40+
_timeProvider = timeProvider ?? TimeProvider.System;
41+
3842
_bucket = $"{Endpoint.AbsoluteUri}{Bucket.ToLowerInvariant()}";
3943
_client = client;
4044
_host = $"{Endpoint.Host}:{Endpoint.Port}";
@@ -55,21 +59,20 @@ public S3BucketClient(HttpClient client, S3BucketSettings settings)
5559
/// <returns>Возвращает подписанную ссылку на файл</returns>
5660
public string BuildFileUrl(string fileName, TimeSpan expiration)
5761
{
58-
var now = DateTime.UtcNow; // TODO!!!
62+
var now = _timeProvider.GetUtcNow();
5963
var url = BuildFileUrl(_bucket, fileName, now, expiration);
6064
var signature = _signature.Calculate(url, now);
6165

6266
return $"{url}&X-Amz-Signature={signature}";
6367
}
6468

65-
public string BuildFileUrl(string bucket, string fileName, DateTime now, TimeSpan expires)
69+
public string BuildFileUrl(string bucket, string fileName, DateTimeOffset now, TimeSpan expires)
6670
{
6771
var builder = new ValueStringBuilder(stackalloc char[512]);
6872

6973
builder.Append(bucket);
70-
builder.Append('/');
71-
72-
StringUtils.AppendEncodedName(ref builder, fileName);
74+
builder.Append('/');
75+
UrlUtils.AppendEncodedName(ref builder, fileName);
7376

7477
builder.Append(_urlStart);
7578
builder.Append(now, Signature.Iso8601Date);
@@ -190,7 +193,7 @@ public async IAsyncEnumerable<string> List(string? prefix, [EnumeratorCancellati
190193
{
191194
var url = string.IsNullOrEmpty(prefix)
192195
? $"{_bucket}?list-type=2"
193-
: $"{_bucket}?list-type=2&prefix={StringUtils.UrlEncodeName(prefix)}";
196+
: $"{_bucket}?list-type=2&prefix={UrlUtils.UrlEncodeName(prefix)}";
194197

195198
HttpResponseMessage response;
196199
using (var request = new HttpRequestMessage(HttpMethod.Get, url))

src/Storage/Utils/HeadBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal sealed class HeadBuilder(string accessKey, string region, string servic
1717

1818

1919
[SkipLocalsInit]
20-
public string BuildAuthorizationValue(DateTime now, string signature)
20+
public string BuildAuthorizationValue(DateTimeOffset now, string signature)
2121
{
2222
using var builder = new ValueStringBuilder(stackalloc char[512]);
2323

src/Storage/Utils/Signature.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal sealed class Signature(string region, string service, string secretKey)
1818
private readonly byte[] _secretKey = Encoding.UTF8.GetBytes($"AWS4{secretKey}");
1919
private readonly string _scope = $"/{region}/{service}/aws4_request\n";
2020

21-
public string Calculate(HttpRequestMessage request, string payloadHash, DateTime requestDate)
21+
public string Calculate(HttpRequestMessage request, string payloadHash, DateTimeOffset requestDate)
2222
=> Calculate(request, payloadHash, HeadBuilder.S3Headers, requestDate);
2323

2424

@@ -27,7 +27,7 @@ public string Calculate(
2727
HttpRequestMessage request,
2828
string payloadHash,
2929
string[] signedHeaders,
30-
DateTime requestDate)
30+
DateTimeOffset requestDate)
3131
{
3232
var builder = new ValueStringBuilder(stackalloc char[512]);
3333

@@ -45,7 +45,7 @@ public string Calculate(
4545

4646

4747
[SkipLocalsInit]
48-
public string Calculate(string url, DateTime requestDate)
48+
public string Calculate(string url, DateTimeOffset requestDate)
4949
{
5050
var builder = new ValueStringBuilder(stackalloc char[512]);
5151

@@ -119,9 +119,8 @@ private void AppendCanonicalRequestHash(
119119
canonical.Append(request.Method.Method);
120120
canonical.Append(newLine);
121121
canonical.Append(uri.AbsolutePath);
122-
canonical.Append(newLine);
123-
124-
StringUtils.AppendCanonicalQueryParameters(ref canonical, uri.Query);
122+
canonical.Append(newLine);
123+
UrlUtils.AppendCanonicalQueryParameters(ref canonical, uri.Query);
125124
canonical.Append(newLine);
126125

127126
AppendCanonicalHeaders(ref canonical, request, signedHeaders);
@@ -241,7 +240,7 @@ private static int Sign(ref Span<byte> buffer, ReadOnlySpan<byte> key, scoped Re
241240

242241

243242
[MethodImpl(MethodImplOptions.AggressiveInlining)]
244-
private void AppendStringToSign(ref ValueStringBuilder builder, DateTime requestDate)
243+
private void AppendStringToSign(ref ValueStringBuilder builder, DateTimeOffset requestDate)
245244
{
246245
builder.Append("AWS4-HMAC-SHA256\n");
247246
builder.Append(requestDate, Iso8601DateTime);
@@ -252,7 +251,7 @@ private void AppendStringToSign(ref ValueStringBuilder builder, DateTime request
252251

253252
[SkipLocalsInit]
254253
[MethodImpl(MethodImplOptions.AggressiveInlining)]
255-
private void CreateSigningKey(ref Span<byte> buffer, DateTime requestDate)
254+
private void CreateSigningKey(ref Span<byte> buffer, DateTimeOffset requestDate)
256255
{
257256
Span<char> dateBuffer = stackalloc char[16];
258257

0 commit comments

Comments
 (0)