/* SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. * * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using OpenSearch.OpenSearch.Ephemeral; using OpenSearch.OpenSearch.Xunit.XunitPlumbing; using FluentAssertions; using OpenSearch.Client; using OpenSearch.Client.Specification.IndicesApi; using Tests.Core.Client; using Tests.Core.Extensions; using Tests.Core.ManagedOpenSearch.Clusters; using Tests.Framework.EndpointTests.TestState; using Xunit; namespace Tests.Framework.EndpointTests { public abstract class CrudWithNoDeleteTestBase : CrudTestBase where TCreateResponse : class, IResponse where TReadResponse : class, IResponse where TUpdateResponse : class, IResponse { protected CrudWithNoDeleteTestBase(WritableCluster cluster, EndpointUsage usage) : base(cluster, usage) { } protected override bool SupportsDeletes => false; protected override bool SupportsExists => false; // https://youtrack.jetbrains.com/issue/RIDER-19912 [I] protected override Task CreateCallIsValid() => base.CreateCallIsValid(); [I] protected override Task GetAfterCreateIsValid() => base.GetAfterCreateIsValid(); [I] protected override Task ExistsAfterCreateIsValid() => base.ExistsAfterCreateIsValid(); [I] protected override Task UpdateCallIsValid() => base.UpdateCallIsValid(); [I] protected override Task GetAfterUpdateIsValid() => base.GetAfterUpdateIsValid(); [I] protected override Task DeleteCallIsValid() => base.DeleteCallIsValid(); [I] protected override Task GetAfterDeleteIsValid() => base.GetAfterDeleteIsValid(); [I] protected override Task ExistsAfterDeleteIsValid() => base.ExistsAfterDeleteIsValid(); [I] protected override Task DeleteNotFoundIsNotValid() => base.DeleteNotFoundIsNotValid(); } public abstract class CrudTestBase : CrudTestBase where TCreateResponse : class, IResponse where TReadResponse : class, IResponse where TUpdateResponse : class, IResponse where TDeleteResponse : class, IResponse { protected CrudTestBase(WritableCluster cluster, EndpointUsage usage) : base(cluster, usage) { } protected override bool SupportsExists => false; // https://youtrack.jetbrains.com/issue/RIDER-19912 [I] protected override Task CreateCallIsValid() => base.CreateCallIsValid(); [I] protected override Task GetAfterCreateIsValid() => base.GetAfterCreateIsValid(); [I] protected override Task ExistsAfterCreateIsValid() => base.ExistsAfterCreateIsValid(); [I] protected override Task UpdateCallIsValid() => base.UpdateCallIsValid(); [I] protected override Task GetAfterUpdateIsValid() => base.GetAfterUpdateIsValid(); [I] protected override Task DeleteCallIsValid() => base.DeleteCallIsValid(); [I] protected override Task GetAfterDeleteIsValid() => base.GetAfterDeleteIsValid(); [I] protected override Task ExistsAfterDeleteIsValid() => base.ExistsAfterDeleteIsValid(); [I] protected override Task DeleteNotFoundIsNotValid() => base.DeleteNotFoundIsNotValid(); } public abstract class CrudTestBase : CrudTestBase where TCluster : IEphemeralCluster, IOpenSearchClientTestCluster, new() where TCreateResponse : class, IResponse where TReadResponse : class, IResponse where TUpdateResponse : class, IResponse where TDeleteResponse : class, IResponse { protected CrudTestBase(TCluster cluster, EndpointUsage usage) : base(cluster, usage) { } protected override bool SupportsExists => false; // https://youtrack.jetbrains.com/issue/RIDER-19912 [I] protected override Task CreateCallIsValid() => base.CreateCallIsValid(); [I] protected override Task GetAfterCreateIsValid() => base.GetAfterCreateIsValid(); [I] protected override Task ExistsAfterCreateIsValid() => base.ExistsAfterCreateIsValid(); [I] protected override Task UpdateCallIsValid() => base.UpdateCallIsValid(); [I] protected override Task GetAfterUpdateIsValid() => base.GetAfterUpdateIsValid(); [I] protected override Task DeleteCallIsValid() => base.DeleteCallIsValid(); [I] protected override Task GetAfterDeleteIsValid() => base.GetAfterDeleteIsValid(); [I] protected override Task ExistsAfterDeleteIsValid() => base.ExistsAfterDeleteIsValid(); [I] protected override Task DeleteNotFoundIsNotValid() => base.DeleteNotFoundIsNotValid(); } public abstract class CrudTestBase : IClusterFixture, IClassFixture where TCluster : IEphemeralCluster, IOpenSearchClientTestCluster, new() where TCreateResponse : class, IResponse where TReadResponse : class, IResponse where TUpdateResponse : class, IResponse where TDeleteResponse : class, IResponse where TExistsResponse : ExistsResponse { private readonly Dictionary _afterCreateResponses = new Dictionary(); private readonly LazyResponses _createExistsResponse; private readonly LazyResponses _createGetResponse; private readonly LazyResponses _createResponse; private readonly LazyResponses _deleteExistsResponse; private readonly LazyResponses _deleteGetResponse; private readonly LazyResponses _deleteNotFoundResponse; private readonly LazyResponses _deleteResponse; private readonly LazyResponses _updateGetResponse; private readonly LazyResponses _updateResponse; private readonly EndpointUsage _usage; [SuppressMessage("Potential Code Quality Issues", "RECS0021:Warns about calls to virtual member functions occuring in the constructor", Justification = "Expected behaviour")] protected CrudTestBase(TCluster cluster, EndpointUsage usage) { _usage = usage; Cluster = cluster; _createResponse = usage.CallOnce(Create, 1); _createGetResponse = usage.CallOnce(Read, 2); _createExistsResponse = usage.CallOnce(Exists, 3); var i = 1; // ReSharper disable once VirtualMemberCallInConstructor foreach (var kv in AfterCreateCalls()) _afterCreateResponses[kv.Key] = usage.CallOnce(kv.Value, 3 * 10 + i++); _updateResponse = usage.CallOnce(Update, 4); _updateGetResponse = usage.CallOnce(Read, 5); _deleteResponse = usage.CallOnce(Delete, 6); _deleteGetResponse = usage.CallOnce(Read, 7); _deleteExistsResponse = usage.CallOnce(Exists, 8); _deleteNotFoundResponse = usage.CallOnce(Delete, 9); } protected virtual IOpenSearchClient Client => Cluster.Client; protected TCluster Cluster { get; } protected virtual bool SupportsDeletes => true; protected virtual bool SupportsExists => true; protected virtual bool SupportsUpdates => true; /// Helpful if you want to capture a reproduce trace with e.g fiddler protected virtual bool TestOnlyOneMethod => false; // ReSharper disable StaticMemberInGenericType private static string RandomFluent { get; } = $"fluent-{RandomString()}"; private static string RandomFluentAsync { get; } = $"fluentasync-{RandomString()}"; private static string RandomInitializer { get; } = $"ois-{RandomString()}"; private static string RandomInitializerAsync { get; } = $"oisasync-{RandomString()}"; // ReSharper restore StaticMemberInGenericType protected abstract LazyResponses Create(); protected abstract LazyResponses Read(); protected abstract LazyResponses Update(); protected virtual LazyResponses Exists() => LazyResponses.Empty; protected virtual LazyResponses Delete() => LazyResponses.Empty; protected virtual IDictionary> AfterCreateCalls() => new Dictionary>(); protected virtual void IntegrationSetup(IOpenSearchClient client) { } protected LazyResponses Calls( Func initializerBody, Func fluentBody, Func, TResponse> fluent, Func, Task> fluentAsync, Func request, Func> requestAsync ) where TResponse : class, IResponse where TDescriptor : class, TInterface where TInitializer : class, TInterface where TInterface : class { var client = Client; return new LazyResponses(async () => { var dict = new Dictionary(); if (TestClient.Configuration.RunIntegrationTests) { IntegrationSetup(client); _usage.CalledSetup = true; } var sf = Sanitize(RandomFluent); dict.Add(ClientMethod.Fluent, fluent(sf, client, f => fluentBody(sf, f))); if (TestOnlyOneMethod) return dict; var sfa = Sanitize(RandomFluentAsync); dict.Add(ClientMethod.FluentAsync, await fluentAsync(sfa, client, f => fluentBody(sfa, f))); var si = Sanitize(RandomInitializer); dict.Add(ClientMethod.Initializer, request(si, client, initializerBody(si))); var sia = Sanitize(RandomInitializerAsync); dict.Add(ClientMethod.InitializerAsync, await requestAsync(sia, client, initializerBody(sia))); return dict; }); } protected LazyResponses Call( Func, IOpenSearchClient, Task> requestAsync ) where TResponse : class, IResponse { var client = Client; return new LazyResponses(async () => { var dict = new Dictionary(); var values = new List() { Sanitize(RandomFluent) }; if (!TestOnlyOneMethod) { values.Add(Sanitize(RandomFluentAsync)); values.Add(Sanitize(RandomInitializer)); values.Add(Sanitize(RandomInitializerAsync)); } dict.Add(ClientMethod.InitializerAsync, await requestAsync(values, client)); return dict; }); } protected static string RandomString() => Guid.NewGuid().ToString("N").Substring(0, 8); protected virtual string Sanitize(string randomString) => randomString + "-" + GetType().Name.Replace("CrudTests", "").ToLowerInvariant(); protected async Task AssertOnAfterCreateResponse(string name, Action assert) where TResponse : class, IResponse { await ExecuteOnceInOrder(); if (_afterCreateResponses.ContainsKey(name)) await AssertOnAllResponses(_afterCreateResponses[name], assert); else throw new Exception($"{name} is not a keyed after create response"); } protected async Task AssertOnAllResponses(LazyResponses responses, Action assert) where TResponse : class, IResponse { await ExecuteOnceInOrder(); foreach (var kv in await responses) { if (!(kv.Value is TResponse response)) throw new Exception($"{kv.Value.GetType()} is not expected response type {typeof(TResponse)}"); //try //{ assert(response); //} #pragma warning disable 7095 //catch (Exception ex) when (false) #pragma warning restore 7095 //{ // throw new Exception($"asserting over the response from: {kv.Key} failed: {ex.Message}", ex); //} } } private async Task ExecuteOnceInOrder() { //hack to make sure these are resolved in the right order, calling twice yields cached results so should be fast await _createResponse; await _createGetResponse; if (SupportsExists) await _createExistsResponse; foreach (var kv in _afterCreateResponses) await kv.Value; if (SupportsUpdates) { await _updateResponse; await _updateGetResponse; } if (SupportsDeletes) { await _deleteResponse; await _deleteGetResponse; if (SupportsExists) await _deleteExistsResponse; await _deleteNotFoundResponse; } } protected async Task AssertOnCreate(Action assert) => await AssertOnAllResponses(_createResponse, assert); protected async Task AssertOnUpdate(Action assert) { if (!SupportsUpdates) return; await AssertOnAllResponses(_updateResponse, assert); } protected async Task AssertOnDelete(Action assert) { if (!SupportsDeletes) return; await AssertOnAllResponses(_deleteResponse, assert); } protected async Task AssertOnGetAfterCreate(Action assert) => await AssertOnAllResponses(_createGetResponse, assert); protected async Task AssertOnGetAfterUpdate(Action assert) { if (!SupportsUpdates) return; await AssertOnAllResponses(_updateGetResponse, assert); } protected async Task AssertOnGetAfterDelete(Action assert) { if (!SupportsDeletes) return; await AssertOnAllResponses(_deleteGetResponse, assert); } protected async Task AssertOnExistsAfterCreate(Action assert) { if (!SupportsExists) return; await AssertOnAllResponses(_createExistsResponse, assert); } protected async Task AssertOnExistsAfterDelete(Action assert) { if (!SupportsExists) return; await AssertOnAllResponses(_deleteExistsResponse, assert); } protected async Task AssertOnDeleteNotFoundAfterDelete(Action assert) { if (!SupportsDeletes) return; await AssertOnAllResponses(_deleteNotFoundResponse, assert); } protected virtual void ExpectAfterCreate(TReadResponse response) { } protected virtual void ExpectExistsAfterCreate(TExistsResponse response) { } protected virtual void ExpectAfterUpdate(TReadResponse response) { } protected virtual void ExpectDeleteNotFoundResponse(TDeleteResponse response) { } protected virtual void ExpectExistsAfterDelete(TExistsResponse response) { } [I] protected virtual async Task CreateCallIsValid() => await AssertOnCreate(r => r.ShouldBeValid()); [I] protected virtual async Task GetAfterCreateIsValid() => await AssertOnGetAfterCreate(r => { r.ShouldBeValid(); ExpectAfterCreate(r); }); [I] protected virtual async Task ExistsAfterCreateIsValid() => await AssertOnExistsAfterCreate(r => { r.ShouldBeValid(); r.Exists.Should().BeTrue(); ExpectExistsAfterCreate(r); }); [I] protected virtual async Task UpdateCallIsValid() => await AssertOnUpdate(r => r.ShouldBeValid()); [I] protected virtual async Task GetAfterUpdateIsValid() => await AssertOnGetAfterUpdate(r => { r.ShouldBeValid(); ExpectAfterUpdate(r); }); [I] protected virtual async Task DeleteCallIsValid() => await AssertOnDelete(r => r.ShouldBeValid()); [I] protected virtual async Task GetAfterDeleteIsValid() => await AssertOnGetAfterDelete(r => r.ShouldNotBeValid()); [I] protected virtual async Task ExistsAfterDeleteIsValid() => await AssertOnExistsAfterDelete(r => { r.ShouldNotBeValid(); r.Exists.Should().BeFalse(); ExpectExistsAfterDelete(r); }); [I] protected virtual async Task DeleteNotFoundIsNotValid() => await AssertOnDeleteNotFoundAfterDelete(r => { r.ShouldNotBeValid(); ExpectDeleteNotFoundResponse(r); }); } }