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", }; }