mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Wire SeaweedFS credentials through to the api.core dev container
api.core now waits on seaweedfs and receives its service URL and credentials so the file storage provider can connect.
This commit is contained in:
@@ -61,6 +61,7 @@ WEBSITE_BASE_URL=http://localhost:3000
|
||||
# SeaweedFS (S3) Configuration
|
||||
# ======================
|
||||
# Credentials and default bucket for the local SeaweedFS S3-compatible storage
|
||||
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333
|
||||
SEAWEEDFS_ACCESS_KEY_ID=admin
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY=secret
|
||||
SEAWEEDFS_BUCKET=my-bucket
|
||||
|
||||
@@ -19,7 +19,10 @@ public class UploadPhotoHandlerTests
|
||||
_handler = new UploadPhotoHandler(_fileStorageProviderMock.Object, _repoMock.Object);
|
||||
}
|
||||
|
||||
private static Mock<IFormFile> ValidFile(string content = "fake-image-bytes", string contentType = "image/png")
|
||||
private static Mock<IFormFile> ValidFile(
|
||||
string content = "fake-image-bytes",
|
||||
string contentType = "image/png"
|
||||
)
|
||||
{
|
||||
Mock<IFormFile> file = new();
|
||||
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
@@ -38,16 +41,21 @@ public class UploadPhotoHandlerTests
|
||||
string? uploadedKey = null;
|
||||
string? uploadedContentType = null;
|
||||
_fileStorageProviderMock
|
||||
.Setup(p => p.UploadAsync(
|
||||
.Setup(p =>
|
||||
p.UploadAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<Stream>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<string, Stream, string, CancellationToken>((key, _, contentType, _) =>
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.Callback<string, Stream, string, CancellationToken>(
|
||||
(key, _, contentType, _) =>
|
||||
{
|
||||
uploadedKey = key;
|
||||
uploadedContentType = contentType;
|
||||
})
|
||||
}
|
||||
)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
Photo? persisted = null;
|
||||
@@ -68,9 +76,19 @@ public class UploadPhotoHandlerTests
|
||||
persisted.UploadedById.Should().Be(command.UploadedById);
|
||||
|
||||
_fileStorageProviderMock.Verify(
|
||||
p => p.UploadAsync(uploadedKey!, It.IsAny<Stream>(), "image/png", It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
_repoMock.Verify(r => r.CreateAsync(It.IsAny<Photo>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
p =>
|
||||
p.UploadAsync(
|
||||
uploadedKey!,
|
||||
It.IsAny<Stream>(),
|
||||
"image/png",
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
_repoMock.Verify(
|
||||
r => r.CreateAsync(It.IsAny<Photo>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -80,16 +98,22 @@ public class UploadPhotoHandlerTests
|
||||
UploadPhotoCommand command = new(Guid.NewGuid(), "avatars", file.Object);
|
||||
|
||||
_fileStorageProviderMock
|
||||
.Setup(p => p.UploadAsync(
|
||||
.Setup(p =>
|
||||
p.UploadAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<Stream>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ThrowsAsync(new InvalidOperationException("storage unavailable"));
|
||||
|
||||
Func<Task> act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
_repoMock.Verify(r => r.CreateAsync(It.IsAny<Photo>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
_repoMock.Verify(
|
||||
r => r.CreateAsync(It.IsAny<Photo>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ using MediatR;
|
||||
namespace Features.ImageUploads.Commands.UploadPhoto;
|
||||
|
||||
/// <summary>Handles <see cref="UploadPhotoCommand" />.</summary>
|
||||
public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhotoUploadRepository photoUploadRepository)
|
||||
: IRequestHandler<UploadPhotoCommand, Guid>
|
||||
public class UploadPhotoHandler(
|
||||
IFileStorageProvider fileStorageProvider,
|
||||
IPhotoUploadRepository photoUploadRepository
|
||||
) : IRequestHandler<UploadPhotoCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(UploadPhotoCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -16,7 +18,6 @@ public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhoto
|
||||
|
||||
await using Stream stream = request.File.OpenReadStream();
|
||||
|
||||
|
||||
await fileStorageProvider.UploadAsync(
|
||||
key,
|
||||
stream,
|
||||
@@ -24,16 +25,9 @@ public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhoto
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
Photo photo = new() { Hyperlink = key, UploadedById = request.UploadedById };
|
||||
|
||||
Photo photo = new()
|
||||
{
|
||||
Hyperlink = key,
|
||||
UploadedById = request.UploadedById,
|
||||
};
|
||||
|
||||
|
||||
await photoUploadRepository.CreateAsync(photo,cancellationToken);
|
||||
|
||||
await photoUploadRepository.CreateAsync(photo, cancellationToken);
|
||||
|
||||
return photoId;
|
||||
}
|
||||
|
||||
@@ -7,18 +7,11 @@ public class UploadPhotoValidator : AbstractValidator<UploadPhotoCommand>
|
||||
{
|
||||
public UploadPhotoValidator()
|
||||
{
|
||||
RuleFor(x => x.UploadedById)
|
||||
.NotEmpty()
|
||||
.WithMessage("UploadedById is required.");
|
||||
RuleFor(x => x.UploadedById).NotEmpty().WithMessage("UploadedById is required.");
|
||||
|
||||
RuleFor(x => x.File)
|
||||
.NotNull()
|
||||
.WithMessage("File is required.");
|
||||
|
||||
RuleFor(x => x.Key)
|
||||
.NotEmpty()
|
||||
.WithMessage("Key is required.");
|
||||
RuleFor(x => x.File).NotNull().WithMessage("File is required.");
|
||||
|
||||
RuleFor(x => x.Key).NotEmpty().WithMessage("Key is required.");
|
||||
|
||||
RuleFor(x => x.File.Length)
|
||||
.GreaterThan(0)
|
||||
@@ -26,7 +19,9 @@ public class UploadPhotoValidator : AbstractValidator<UploadPhotoCommand>
|
||||
.WithMessage("File cannot be empty.");
|
||||
|
||||
RuleFor(x => x.File.ContentType)
|
||||
.Must(contentType => contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
.Must(contentType =>
|
||||
contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
.When(x => x.File is not null)
|
||||
.WithMessage("File must be an image.");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Features.ImageUploads</RootNamespace>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,6 +18,4 @@
|
||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory)
|
||||
{
|
||||
public async Task CreateAsync(Photo photo, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
await using DbTransaction transaction = await connection.BeginTransactionAsync();
|
||||
|
||||
@@ -48,11 +47,7 @@ public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory)
|
||||
INSERT INTO dbo.Photo (Hyperlink, UploadedByID)
|
||||
VALUES (@Hyperlink, @UploadedById)
|
||||
""",
|
||||
new
|
||||
{
|
||||
photo.Hyperlink,
|
||||
photo.UploadedById
|
||||
},
|
||||
new { photo.Hyperlink, photo.UploadedById },
|
||||
transaction
|
||||
)
|
||||
);
|
||||
@@ -65,5 +60,4 @@ public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ services:
|
||||
depends_on:
|
||||
database.seed:
|
||||
condition: service_completed_successfully
|
||||
seaweedfs:
|
||||
condition: service_started
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: API/API.Core/Dockerfile
|
||||
@@ -101,6 +103,10 @@ services:
|
||||
REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}"
|
||||
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
|
||||
WEBSITE_BASE_URL: "${WEBSITE_BASE_URL}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- devnet
|
||||
|
||||
Reference in New Issue
Block a user