using DodoSSH.Client.Domain;
namespace DodoSSH.Client.ObjectStore.Tests;
///
/// What a bucket needs before it can be stored, and what the factory does with it.
///
///
/// Every refusal here exists because the failure it prevents is one whose message names neither the field
/// nor the bucket. A missing region produces an SDK error about resolving an endpoint; a hostname without a
/// scheme produces a URI parse failure; and both arrive at the first listing, long after the typing.
///
public sealed class ObjectStoreSecretTests
{
[Fact]
public void ABucketWithARegion_IsStorable()
{
Bucket().TryValidate(out var reason).ShouldBeTrue(reason);
}
[Fact]
public void ABucketWithAnEndpointAndNoRegion_IsStorable()
{
// The self-hosted case, and the reason Region is nullable rather than defaulted to us-east-1: a
// default would be a guess presented as configuration, and it is wrong for exactly this user.
var store = Bucket() with { Region = null, Endpoint = "https://minio.internal:9000" };
store.TryValidate(out var reason).ShouldBeTrue(reason);
}
[Fact]
public void ABucketWithNeitherARegionNorAnEndpoint_IsRefused()
{
var store = Bucket() with { Region = null, Endpoint = null };
store.TryValidate(out var reason).ShouldBeFalse();
reason.ShouldNotBeNull().ShouldContain("region");
}
///
/// A bare hostname is what somebody types, and the SDK's own failure for it names a URI rather than this
/// field. Refusing at the editor is the only place the message can be about what was typed.
///
[Theory]
[InlineData("minio.internal:9000")]
[InlineData("/buckets")]
[InlineData("ftp://minio.internal")]
public void AnEndpointThatIsNotAnHttpUrl_IsRefused(string endpoint)
{
var store = Bucket() with { Endpoint = endpoint };
store.TryValidate(out var reason).ShouldBeFalse();
reason.ShouldNotBeNull();
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void ABucketMissingSomethingItNeeds_IsRefused(string blank)
{
(Bucket() with { Label = blank }).TryValidate(out _).ShouldBeFalse();
(Bucket() with { Bucket = blank }).TryValidate(out _).ShouldBeFalse();
(Bucket() with { AccessKeyId = blank }).TryValidate(out _).ShouldBeFalse();
(Bucket() with { SecretAccessKey = blank }).TryValidate(out _).ShouldBeFalse();
}
[Fact]
public void ABucket_RoundTripsThroughItsCodec()
{
var store = Bucket() with
{
Endpoint = "https://minio.internal:9000",
UsePathStyle = true,
Notes = "the backups bucket",
};
ObjectStoreSecretCodec.TryDecode(ObjectStoreSecretCodec.Encode(store), out var document)
.ShouldBeTrue();
document.ShouldNotBeNull();
document.Store.ShouldBe(store);
document.SchemaVersion.ShouldBe(ObjectStoreSecretCodec.CurrentSchemaVersion);
}
///
/// The factory builds a client and contacts nothing, which is why it is synchronous — S3 is
/// request-per-operation and there is no connect step to fail. What it does do is refuse a bucket that
/// could never work, so the failure lands at the button rather than at the first listing.
///
[Fact]
public void TheFactoryRefusesABucketThatCouldNotBeStored()
{
var factory = new S3ObjectStoreFactory();
Should.Throw(
() => factory.Open(Bucket() with { Region = null, Endpoint = null }));
}
[Fact]
public void TheFactoryOpensAValidBucketWithoutContactingAnything()
{
var factory = new S3ObjectStoreFactory();
var store = factory.Open(Bucket() with
{
Endpoint = "https://minio.internal:9000",
UsePathStyle = true,
});
store.IsConnected.ShouldBeTrue("nothing is contacted, so there is nothing to be down");
store.HomeDirectory.ShouldBe("/");
}
private static ObjectStoreSecret Bucket() => new()
{
Label = "backups",
Bucket = "dodossh-backups",
AccessKeyId = "AKIAEXAMPLE",
SecretAccessKey = "an example secret access key",
Region = "eu-west-1",
};
}