diff --git a/.gitignore b/.gitignore index 5085114..18c32db 100644 --- a/.gitignore +++ b/.gitignore @@ -177,6 +177,7 @@ launchSettings.json UpgradeLog.htm *.*.ini *.*.json +src/Documentation/python # Public repo ignores .github/pull_request_template.md \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 198a9b8..d641ca5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true true - 4.3.0 + 4.3.1 Salesforce, Inc. Salesforce, Inc. Copyright (c) 2024, Salesforce, Inc. and its licensors diff --git a/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs b/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs index 5d3be6c..bd82b89 100644 --- a/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs +++ b/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs @@ -23,7 +23,6 @@ using Tableau.Migration.Api.Rest; using Tableau.Migration.Api.Rest.Models; using Tableau.Migration.Content; -using Tableau.Migration.Content.Schedules; using Tableau.Migration.Content.Search; using Tableau.Migration.Resources; @@ -126,19 +125,6 @@ internal static class IContentReferenceFinderFactoryExtensions cancel) .ConfigureAwait(false); - public static async Task FindExtractRefreshContentAsync( - this IContentReferenceFinderFactory finderFactory, - ExtractRefreshContentType contentType, - Guid contentId, - CancellationToken cancel) - { - var finder = finderFactory.ForExtractRefreshContent(contentType); - - var content = await finder.FindByIdAsync(contentId, cancel).ConfigureAwait(false); - - return Guard.AgainstNull(content, nameof(content)); - } - private static async Task FindAsync( this IContentReferenceFinderFactory finderFactory, [NotNull] TResponse? response, diff --git a/src/Tableau.Migration/Api/TasksApiClient.cs b/src/Tableau.Migration/Api/TasksApiClient.cs index 2b7f6e8..74ca31a 100644 --- a/src/Tableau.Migration/Api/TasksApiClient.cs +++ b/src/Tableau.Migration/Api/TasksApiClient.cs @@ -32,6 +32,7 @@ using Tableau.Migration.Net.Rest; using Tableau.Migration.Paging; using Tableau.Migration.Resources; + using CloudResponses = Tableau.Migration.Api.Rest.Models.Responses.Cloud; using ServerResponses = Tableau.Migration.Api.Rest.Models.Responses.Server; @@ -70,17 +71,11 @@ public TasksApiClient( /// public IServerTasksApiClient ForServer() - => ExecuteForInstanceType( - TableauInstanceType.Server, - _sessionProvider.InstanceType, - () => this); + => ExecuteForInstanceType(TableauInstanceType.Server, _sessionProvider.InstanceType, () => this); /// public ICloudTasksApiClient ForCloud() - => ExecuteForInstanceType( - TableauInstanceType.Cloud, - _sessionProvider.InstanceType, - () => this); + => ExecuteForInstanceType(TableauInstanceType.Cloud, _sessionProvider.InstanceType, () => this); #endregion @@ -105,10 +100,7 @@ public async Task DeleteExtractRefreshTaskAsync( async Task>> ICloudTasksApiClient.GetAllExtractRefreshTasksAsync( CancellationToken cancel) => await GetAllExtractRefreshTasksAsync( - (r, c) => CloudExtractRefreshTask.CreateManyAsync( - r, - ContentFinderFactory, - c), + (r, c) => CloudExtractRefreshTask.CreateManyAsync(r, ContentFinderFactory, Logger, SharedResourcesLocalizer, c), cancel) .ConfigureAwait(false); @@ -122,14 +114,18 @@ async Task> ICloudTasksApiClient.CreateExtract .ForPostRequest() .WithXmlContent(new CreateExtractRefreshTaskRequest(options)) .SendAsync(cancel) - .ToResultAsync((r, c) => - CloudExtractRefreshTask.CreateAsync( - r.Item, - r.Schedule, - ContentFinderFactory, - c), - SharedResourcesLocalizer, - cancel) + .ToResultAsync(async (r, c) => + { + var task = Guard.AgainstNull(r.Item, () => r.Item); + var finder = ContentFinderFactory.ForExtractRefreshContent(task.GetContentType()); + + var contentReference = await finder.FindByIdAsync(task.GetContentId(), cancel).ConfigureAwait(false); + + // Since we published with a content reference, we expect the reference returned is valid/knowable. + Guard.AgainstNull(contentReference, () => contentReference); + + return CloudExtractRefreshTask.Create(task, r.Schedule, contentReference); + }, SharedResourcesLocalizer, cancel) .ConfigureAwait(false); return result; @@ -158,11 +154,7 @@ public async Task> PublishAsync( /// async Task>> IServerTasksApiClient.GetAllExtractRefreshTasksAsync(CancellationToken cancel) => await GetAllExtractRefreshTasksAsync( - (r, c) => ServerExtractRefreshTask.CreateManyAsync( - r, - ContentFinderFactory, - _contentCacheFactory, - c), + (r, c) => ServerExtractRefreshTask.CreateManyAsync(r, ContentFinderFactory, _contentCacheFactory, Logger, SharedResourcesLocalizer, c), cancel) .ConfigureAwait(false); diff --git a/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs index 79c944e..8b782bc 100644 --- a/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs +++ b/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs @@ -19,9 +19,11 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Tableau.Migration.Api.Rest.Models.Responses; using Tableau.Migration.Api.Rest.Models.Responses.Cloud; using Tableau.Migration.Content.Search; +using Tableau.Migration.Resources; namespace Tableau.Migration.Content.Schedules.Cloud { @@ -34,54 +36,33 @@ internal CloudExtractRefreshTask( ExtractRefreshContentType contentType, IContentReference contentReference, ICloudSchedule schedule) - : base( - extractRefreshId, - type, - contentType, - contentReference, - schedule) + : base(extractRefreshId, type, contentType, contentReference, schedule) { } public static async Task> CreateManyAsync( - ExtractRefreshTasksResponse? response, + ExtractRefreshTasksResponse response, IContentReferenceFinderFactory finderFactory, + ILogger logger, ISharedResourcesLocalizer localizer, CancellationToken cancel) => await CreateManyAsync( response, response => response.Items.ExceptNulls(i => i.ExtractRefresh), (r, c, cnl) => Task.FromResult(Create(r, r.Schedule, c)), - finderFactory, + finderFactory, logger, localizer, cancel) .ConfigureAwait(false); - public static async Task CreateAsync( - ICloudExtractRefreshType? response, - ICloudScheduleType? schedule, - IContentReferenceFinderFactory finderFactory, - CancellationToken cancel) - => await CreateAsync( - response, - finderFactory, - (r, c, cnl) => Task.FromResult(Create(r, schedule, c)), - cancel) - .ConfigureAwait(false); - - private static ICloudExtractRefreshTask Create( - IExtractRefreshType? response, - ICloudScheduleType? schedule, + public static ICloudExtractRefreshTask Create( + IExtractRefreshType response, + ICloudScheduleType schedule, IContentReference content) { - Guard.AgainstNull(response, nameof(response)); - return new CloudExtractRefreshTask( response.Id, response.Type!, response.GetContentType(), content, - new CloudSchedule( - Guard.AgainstNull( - schedule, - () => schedule))); + new CloudSchedule(schedule)); } } } diff --git a/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs b/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs index b57e74d..506cba8 100644 --- a/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs +++ b/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs @@ -20,10 +20,11 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; -using Tableau.Migration.Api; +using Microsoft.Extensions.Logging; using Tableau.Migration.Api.Rest.Models; using Tableau.Migration.Api.Rest.Models.Responses; using Tableau.Migration.Content.Search; +using Tableau.Migration.Resources; namespace Tableau.Migration.Content.Schedules { @@ -32,8 +33,11 @@ internal abstract class ExtractRefreshTaskBase : where TSchedule : ISchedule { public string Type { get; set; } + public ExtractRefreshContentType ContentType { get; set; } + public IContentReference Content { get; set; } + public TSchedule Schedule { get; } protected ExtractRefreshTaskBase( @@ -54,57 +58,43 @@ protected ExtractRefreshTaskBase( Schedule = schedule; } - protected static async Task CreateAsync( - TExtractRefreshType? response, - IContentReferenceFinderFactory finderFactory, - Func> modelFactory, - CancellationToken cancel) - where TExtractRefreshType : class, IExtractRefreshType - where TExtractRefreshTask: IExtractRefreshTask - { - Guard.AgainstNull(response, nameof(response)); - - var contentReference = await finderFactory - .FindExtractRefreshContentAsync( - response.GetContentType(), - response.GetContentId(), - cancel) - .ConfigureAwait(false); - - var model = await modelFactory( - response, - contentReference, - cancel) - .ConfigureAwait(false); - - return model; - } - - protected static async Task> CreateManyAsync( - TResponse? response, + protected static async Task> CreateManyAsync( + TResponse response, Func> responseItemFactory, Func> modelFactory, IContentReferenceFinderFactory finderFactory, + ILogger logger, ISharedResourcesLocalizer localizer, CancellationToken cancel) where TResponse : ITableauServerResponse where TExtractRefreshType : class, IExtractRefreshType where TExtractRefreshTask: IExtractRefreshTask { - Guard.AgainstNull(response, nameof(response)); - - var tasks = ImmutableArray.CreateBuilder(); - - var items = responseItemFactory(response).ExceptNulls(); + var items = responseItemFactory(response).ExceptNulls().ToImmutableArray(); + var tasks = ImmutableArray.CreateBuilder(items.Length); foreach (var item in items) { - tasks.Add( - await CreateAsync( - item, - finderFactory, - modelFactory, - cancel) - .ConfigureAwait(false)); + var contentType = item.GetContentType(); + + if(contentType is ExtractRefreshContentType.Unknown) + { + logger.LogWarning(localizer[SharedResourceKeys.UnknownExtractRefreshContentTypeWarning], item.Id); + continue; + } + + var finder = finderFactory.ForExtractRefreshContent(contentType); + var contentReference = await finder.FindByIdAsync(item.GetContentId(), cancel).ConfigureAwait(false); + + /* + * Content reference is null when the referenced content item (e.g. workbook/data source) + * is in a private space or other "pre-manifest" filter. + * + * We similarly filter out those extract refresh tasks. + */ + if(contentReference is not null) + { + tasks.Add(await modelFactory(item, contentReference, cancel).ConfigureAwait(false)); + } } return tasks.ToImmutable(); diff --git a/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs index 4ae212b..b1f5923 100644 --- a/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs +++ b/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs @@ -20,9 +20,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Tableau.Migration.Api.Rest.Models.Responses; using Tableau.Migration.Api.Rest.Models.Responses.Server; using Tableau.Migration.Content.Search; +using Tableau.Migration.Resources; namespace Tableau.Migration.Content.Schedules.Server { @@ -47,26 +49,25 @@ internal ServerExtractRefreshTask( { } public static async Task> CreateManyAsync( - ExtractRefreshTasksResponse? response, + ExtractRefreshTasksResponse response, IContentReferenceFinderFactory finderFactory, IContentCacheFactory contentCacheFactory, + ILogger logger, ISharedResourcesLocalizer localizer, CancellationToken cancel) => await CreateManyAsync( response, response => response.Items.ExceptNulls(i => i.ExtractRefresh), async (r, c, cnl) => await CreateAsync(r, c, contentCacheFactory, cnl).ConfigureAwait(false), - finderFactory, + finderFactory, logger, localizer, cancel) .ConfigureAwait(false); private static async Task CreateAsync( - IServerExtractRefreshType? response, + IServerExtractRefreshType response, IContentReference content, IContentCacheFactory contentCacheFactory, CancellationToken cancel) { - Guard.AgainstNull(response, nameof(response)); - var scheduleCache = contentCacheFactory.ForContentType(true); var schedule = await scheduleCache.ForIdAsync(response.Schedule.Id, cancel).ConfigureAwait(false); diff --git a/src/Tableau.Migration/Resources/SharedResourceKeys.cs b/src/Tableau.Migration/Resources/SharedResourceKeys.cs index c92eb87..941ea9c 100644 --- a/src/Tableau.Migration/Resources/SharedResourceKeys.cs +++ b/src/Tableau.Migration/Resources/SharedResourceKeys.cs @@ -142,5 +142,7 @@ internal static class SharedResourceKeys public const string UserWithCustomViewDefaultSkippedMissingReferenceWarning = "UserWithCustomViewDefaultSkippedMissingReferenceWarning"; public const string DuplicateContentTypeConfigurationMessage = "DuplicateContentTypeConfigurationMessage"; + + public const string UnknownExtractRefreshContentTypeWarning = "UnknownExtractRefreshContentTypeWarning"; } } diff --git a/src/Tableau.Migration/Resources/SharedResources.resx b/src/Tableau.Migration/Resources/SharedResources.resx index 9ce88d8..a0c186e 100644 --- a/src/Tableau.Migration/Resources/SharedResources.resx +++ b/src/Tableau.Migration/Resources/SharedResources.resx @@ -315,4 +315,7 @@ Owner with ID {OwnerID}: {owner} Duplicate content type configuration found for content type {0}. + + The extract refresh task with ID {TaskId} references an unknown content type. The task will be ignored. + \ No newline at end of file diff --git a/tests/Tableau.Migration.Tests/Unit/Api/ApiTestBase.cs b/tests/Tableau.Migration.Tests/Unit/Api/ApiTestBase.cs index cb06610..b435eb2 100644 --- a/tests/Tableau.Migration.Tests/Unit/Api/ApiTestBase.cs +++ b/tests/Tableau.Migration.Tests/Unit/Api/ApiTestBase.cs @@ -16,9 +16,7 @@ // using System; -using System.Collections.Generic; using System.Net.Http; -using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; @@ -27,11 +25,9 @@ using Tableau.Migration.Api.Publishing; using Tableau.Migration.Api.Rest.Models; using Tableau.Migration.Api.Rest.Models.Responses; -using Tableau.Migration.Api.Rest.Models.Responses.Server; using Tableau.Migration.Api.Tags; using Tableau.Migration.Config; using Tableau.Migration.Content; -using Tableau.Migration.Content.Schedules; using Tableau.Migration.Content.Schedules.Server; using Tableau.Migration.Content.Search; using Tableau.Migration.Net; @@ -129,60 +125,5 @@ protected IHttpResponseMessage SetupSuccessResponse(HttpContent? content = null) protected TService CreateService() where TService : class => ActivatorUtilities.CreateInstance(Dependencies); - - protected void SetupExtractRefreshContentFinder(IExtractRefreshType extractRefresh) - { - var contentType = extractRefresh.GetContentType(); - var contentId = extractRefresh.GetContentId(); - - var contentReference = Create>(m => - { - m.SetupGet(r => r.Id).Returns(contentId); - }) - .Object; - - switch (contentType) - { - case ExtractRefreshContentType.Workbook: - SetupContentFinder(MockWorkbookFinder); - break; - - case ExtractRefreshContentType.DataSource: - SetupContentFinder(MockDataSourceFinder); - break; - - default: - throw new NotSupportedException($"Content type {contentType} is not supported."); - } - - if (extractRefresh is IServerExtractRefreshType serverExtractRefresh) - { - Guard.AgainstNull(serverExtractRefresh.Schedule, () => serverExtractRefresh.Schedule); - - var scheduleId = serverExtractRefresh.Schedule.Id; - - var cachedSchedule = Create>(m => - { - m.SetupGet(s => s.Id).Returns(scheduleId); - }) - .Object; - - var scheduleReference = cachedSchedule.ToStub(); - - MockScheduleFinder.Setup(f => f.FindByIdAsync(scheduleId, It.IsAny())).ReturnsAsync(scheduleReference); - - MockScheduleCache.Setup(f => f.ForIdAsync(scheduleId, It.IsAny())).ReturnsAsync(cachedSchedule); - } - - void SetupContentFinder(Mock> mockFinder) - where T : IContentReference - => mockFinder.Setup(f => f.FindByIdAsync(contentId, It.IsAny())).ReturnsAsync(contentReference); - } - - protected void SetupExtractRefreshContentFinder(IEnumerable extractRefreshes) - { - foreach (var extractRefresh in extractRefreshes) - SetupExtractRefreshContentFinder(extractRefresh); - } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs index fd30095..5a26be6 100644 --- a/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs @@ -30,13 +30,15 @@ using Tableau.Migration.Content.Schedules; using Tableau.Migration.Content.Schedules.Cloud; using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Tests.Unit.Content.Schedules; using Xunit; + using CloudResponses = Tableau.Migration.Api.Rest.Models.Responses.Cloud; using ServerResponses = Tableau.Migration.Api.Rest.Models.Responses.Server; namespace Tableau.Migration.Tests.Unit.Api { - public class TasksApiClientTests + public sealed class TasksApiClientTests { public abstract class TasksApiClientTest : ApiClientTestBase { @@ -44,9 +46,13 @@ public abstract class TasksApiClientTest : ApiClientTestBase protected TableauInstanceType CurrentInstanceType { get; set; } + protected ExtractRefreshTestCaches ExtractRefreshTestCaches { get; } + public TasksApiClientTest() { MockSessionProvider.SetupGet(p => p.InstanceType).Returns(() => CurrentInstanceType); + + ExtractRefreshTestCaches = new(AutoFixture, MockDataSourceFinder, MockWorkbookFinder, MockScheduleFinder, MockScheduleCache); } protected static List AssertSuccess(IResult> result) @@ -204,7 +210,7 @@ public async Task Gets_datasource_extract_refreshes() var response = CreateCloudResponse(ExtractRefreshContentType.DataSource); - SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); SetupSuccessResponse(response); @@ -224,7 +230,7 @@ public async Task Gets_workbook_extract_refreshes() var response = CreateCloudResponse(ExtractRefreshContentType.Workbook); - SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); SetupSuccessResponse(response); @@ -236,6 +242,25 @@ public async Task Gets_workbook_extract_refreshes() Assert.Equal(expectedExtractRefreshes.Count, actualExtractRefreshes.Count); Assert.DoesNotContain(actualExtractRefreshes, item => item.ContentType == ExtractRefreshContentType.DataSource); } + + [Fact] + public async Task Ignores_personal_spaces_workbook_tasks() + { + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Cloud); + + var response = CreateCloudResponse(ExtractRefreshContentType.Workbook); + + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls().Skip(1)); + + SetupSuccessResponse(response); + + var result = await CloudTasksApiClient.GetAllExtractRefreshTasksAsync(Cancel); + + var actualExtractRefreshes = AssertSuccess(result); + var expectedExtractRefreshes = response.Items.ToList(); + + Assert.Equal(expectedExtractRefreshes.Count - 1, actualExtractRefreshes.Count); + } } #endregion @@ -263,7 +288,7 @@ public async Task Creates_extract_refresh_for_workbook_successfully() response.Schedule!.Frequency = cloudSchedule.Frequency; SetupSuccessResponse(response); - SetupExtractRefreshContentFinder(response.Item); + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Item); // Act var result = await CloudTasksApiClient.CreateExtractRefreshTaskAsync( @@ -296,7 +321,7 @@ public async Task Creates_extract_refresh_for_datasource_successfully() response.Schedule!.Frequency = cloudSchedule.Frequency; SetupSuccessResponse(response); - SetupExtractRefreshContentFinder(response.Item); + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Item); // Act var result = await CloudTasksApiClient.CreateExtractRefreshTaskAsync( @@ -334,6 +359,34 @@ public async Task Fails_to_create_extract_refresh() Assert.False(result.Success); Assert.Null(result.Value); } + + [Fact] + public async Task Fails_content_reference_not_found() + { + // Arrange + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Cloud); + var contentReference = AutoFixture.Create(); + var cloudSchedule = AutoFixture.Create(); + var createTaskOptions = new CreateExtractRefreshTaskOptions( + ExtractRefreshType.FullRefresh, + ExtractRefreshContentType.Workbook, + contentReference.Id, + cloudSchedule); + + var response = AutoFixture.CreateResponse(); + response.Item!.DataSource = null; + response.Item.Workbook!.Id = contentReference.Id; + response.Schedule!.Frequency = cloudSchedule.Frequency; + + SetupSuccessResponse(response); + + // Act + var result = await CloudTasksApiClient.CreateExtractRefreshTaskAsync(createTaskOptions, Cancel); + + // Assert + result.AssertFailure(); + Assert.IsType(result.Errors.Single()); + } } #endregion @@ -381,7 +434,7 @@ public async Task Gets_workbook_extract_refreshes() MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Server); var response = CreateServerResponse(ExtractRefreshContentType.Workbook); - SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); SetupSuccessResponse(response); @@ -401,7 +454,7 @@ public async Task Gets_datasource_extract_refreshes() var response = CreateServerResponse(ExtractRefreshContentType.DataSource); - SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); SetupSuccessResponse(response); @@ -413,6 +466,25 @@ public async Task Gets_datasource_extract_refreshes() Assert.Equal(expectedExtractRefreshes.Count, actualExtractRefreshes.Count); Assert.DoesNotContain(actualExtractRefreshes, item => item.ContentType == ExtractRefreshContentType.Workbook); } + + [Fact] + public async Task Ignores_personal_spaces_workbook_tasks() + { + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Server); + + var response = CreateServerResponse(ExtractRefreshContentType.Workbook); + + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls().Skip(1)); + + SetupSuccessResponse(response); + + var result = await ServerTasksApiClient.GetAllExtractRefreshTasksAsync(Cancel); + + var actualExtractRefreshes = AssertSuccess(result); + var expectedExtractRefreshes = response.Items.ToList(); + + Assert.Equal(expectedExtractRefreshes.Count - 1, actualExtractRefreshes.Count); + } } #endregion diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Cloud/CloudExtractRefreshTaskTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Cloud/CloudExtractRefreshTaskTests.cs index 1186a5d..424b0e1 100644 --- a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Cloud/CloudExtractRefreshTaskTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Cloud/CloudExtractRefreshTaskTests.cs @@ -16,9 +16,10 @@ // using System; +using System.Linq; +using System.Threading.Tasks; using Tableau.Migration.Api.Rest.Models.Responses; using Tableau.Migration.Api.Rest.Models.Responses.Cloud; -using Tableau.Migration.Api.Rest.Models.Types; using Tableau.Migration.Content.Schedules; using Tableau.Migration.Content.Schedules.Cloud; using Xunit; @@ -27,7 +28,7 @@ namespace Tableau.Migration.Tests.Unit.Content.Schedules.Cloud { - public class CloudExtractRefreshTaskTests + public sealed class CloudExtractRefreshTaskTests { public abstract class CloudExtractRefreshTaskTest : ExtractRefreshTaskTestBase { @@ -55,7 +56,7 @@ internal CloudExtractRefreshTask CreateExtractRefreshTask( } } - public class Ctor : CloudExtractRefreshTaskTest + public sealed class Ctor : CloudExtractRefreshTaskTest { [Theory, ExtractRefreshContentTypeData] public void Initializes(ExtractRefreshContentType contentType) @@ -73,5 +74,28 @@ public void Initializes(ExtractRefreshContentType contentType) Assert.Same(schedule, task.Schedule); } } + + public sealed class CreateManyAsync : CloudExtractRefreshTaskTest + { + [Theory, ExtractRefreshContentTypeData] + public async Task IgnoresPersonalSpaceTasksAsync(ExtractRefreshContentType contentType) + { + var response = new ExtractRefreshTasksResponse + { + Items = Enumerable.Range(1, 10) + .Select(i => new ExtractRefreshTasksResponse.TaskType + { + ExtractRefresh = CreateExtractRefreshResponse(GetRandomType(), contentType) + }) + .ToArray() + }; + + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls().Skip(1)); + + var tasks = await CloudExtractRefreshTask.CreateManyAsync(response, MockFinderFactory.Object, Logger, Localizer, Cancel); + + Assert.Equal(response.Items.Length - 1, tasks.Count); + } + } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs index e0916fc..36cd2ef 100644 --- a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs @@ -15,13 +15,42 @@ // limitations under the License. // +using Microsoft.Extensions.Logging; using Moq; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Schedules.Server; using Tableau.Migration.Content.Search; +using Tableau.Migration.Resources; namespace Tableau.Migration.Tests.Unit.Content.Schedules { public abstract class ExtractRefreshTaskTestBase : ScheduleTestBase { - protected readonly Mock MockFinderFactory = new(); + protected readonly Mock MockFinderFactory = new() { CallBase = true }; + + protected readonly Mock> MockDataSourceFinder = new(); + + protected readonly Mock> MockScheduleCache = new(); + + protected readonly Mock> MockScheduleFinder = new(); + + protected readonly Mock> MockWorkbookFinder = new(); + + protected ILogger Logger { get; } + + protected ISharedResourcesLocalizer Localizer { get; } + + protected ExtractRefreshTestCaches ExtractRefreshTestCaches { get; } + + protected ExtractRefreshTaskTestBase() + { + Logger = Create(); + Localizer = Create(); + + MockFinderFactory.Setup(x => x.ForContentType()).Returns(MockDataSourceFinder.Object); + MockFinderFactory.Setup(x => x.ForContentType()).Returns(MockWorkbookFinder.Object); + + ExtractRefreshTestCaches = new(AutoFixture, MockDataSourceFinder, MockWorkbookFinder, MockScheduleFinder, MockScheduleCache); + } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTestCaches.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTestCaches.cs new file mode 100644 index 0000000..35c9236 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTestCaches.cs @@ -0,0 +1,109 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed 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.Threading; +using AutoFixture; +using Moq; +using Tableau.Migration.Api.Rest.Models.Responses; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules +{ + public sealed class ExtractRefreshTestCaches + { + private readonly IFixture _fixture; + + private readonly Mock> _mockServerScheduleCache; + private readonly Mock> _mockDataSourceFinder; + private readonly Mock> _mockServerScheduleFinder; + private readonly Mock> _mockWorkbookFinder; + + public ExtractRefreshTestCaches(IFixture fixture, + Mock> mockDataSourceFinder, Mock> mockWorkbookFinder, + Mock> mockServerScheduleFinder, Mock> mockServerScheduleCache) + { + _fixture = fixture; + _mockDataSourceFinder = mockDataSourceFinder; + _mockServerScheduleCache = mockServerScheduleCache; + _mockServerScheduleFinder = mockServerScheduleFinder; + _mockWorkbookFinder = mockWorkbookFinder; + + // Set default finder behavior to return null on missing items. + _mockDataSourceFinder.Setup(x => x.FindByIdAsync(It.IsAny(), It.IsAny())).ReturnsAsync((IContentReference?)null); + _mockWorkbookFinder.Setup(x => x.FindByIdAsync(It.IsAny(), It.IsAny())).ReturnsAsync((IContentReference?)null); + } + + public void SetupExtractRefreshContentFinder(IExtractRefreshType extractRefresh) + { + var contentType = extractRefresh.GetContentType(); + var contentId = extractRefresh.GetContentId(); + + var mockReference = _fixture.Create>(); + mockReference.SetupGet(r => r.Id).Returns(contentId); + + var contentReference = mockReference.Object; + + switch (contentType) + { + case ExtractRefreshContentType.DataSource: + SetupContentFinder(_mockDataSourceFinder); + break; + + case ExtractRefreshContentType.Workbook: + SetupContentFinder(_mockWorkbookFinder); + break; + + default: + throw new NotSupportedException($"Content type {contentType} is not supported."); + } + + if (extractRefresh is IServerExtractRefreshType serverExtractRefresh) + { + Guard.AgainstNull(serverExtractRefresh.Schedule, () => serverExtractRefresh.Schedule); + + var scheduleId = serverExtractRefresh.Schedule.Id; + + var mockSchedule = _fixture.Create>(); + mockSchedule.SetupGet(s => s.Id).Returns(scheduleId); + + var cachedSchedule = mockSchedule.Object; + var scheduleReference = cachedSchedule.ToStub(); + + _mockServerScheduleFinder.Setup(f => f.FindByIdAsync(scheduleId, It.IsAny())).ReturnsAsync(scheduleReference); + + _mockServerScheduleCache.Setup(f => f.ForIdAsync(scheduleId, It.IsAny())).ReturnsAsync(cachedSchedule); + } + + void SetupContentFinder(Mock> mockFinder) + where T : IContentReference + => mockFinder.Setup(f => f.FindByIdAsync(contentId, It.IsAny())).ReturnsAsync(contentReference); + } + + public void SetupExtractRefreshContentFinder(IEnumerable extractRefreshes) + { + foreach (var extractRefresh in extractRefreshes) + SetupExtractRefreshContentFinder(extractRefresh); + } + + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs index f9a65cd..bb1722a 100644 --- a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs @@ -16,10 +16,11 @@ // using System; +using System.Linq; +using System.Threading.Tasks; using Moq; using Tableau.Migration.Api.Rest.Models.Responses; using Tableau.Migration.Api.Rest.Models.Responses.Server; -using Tableau.Migration.Api.Rest.Models.Types; using Tableau.Migration.Content.Schedules; using Tableau.Migration.Content.Schedules.Server; using Tableau.Migration.Content.Search; @@ -29,12 +30,11 @@ namespace Tableau.Migration.Tests.Unit.Content.Schedules.Server { - public class ServerExtractRefreshTaskTests + public sealed class ServerExtractRefreshTaskTests { public abstract class ServerExtractRefreshTaskTest : ExtractRefreshTaskTestBase { protected readonly Mock MockContentCacheFactory = new(); - protected readonly Mock> MockScheduleCache = new(); public ServerExtractRefreshTaskTest() { @@ -67,7 +67,7 @@ internal ServerExtractRefreshTask CreateExtractRefreshTask( } } - public class Ctor : ServerExtractRefreshTaskTest + public sealed class Ctor : ServerExtractRefreshTaskTest { [Theory, ExtractRefreshContentTypeData] public void Initializes(ExtractRefreshContentType contentType) @@ -85,5 +85,28 @@ public void Initializes(ExtractRefreshContentType contentType) Assert.Same(schedule, task.Schedule); } } + + public sealed class CreateManyAsync : ServerExtractRefreshTaskTest + { + [Theory, ExtractRefreshContentTypeData] + public async Task IgnoresPersonalSpaceTasksAsync(ExtractRefreshContentType contentType) + { + var response = new ExtractRefreshTasksResponse + { + Items = Enumerable.Range(1, 10) + .Select(i => new ExtractRefreshTasksResponse.TaskType + { + ExtractRefresh = CreateExtractRefreshResponse(GetRandomType(), contentType) + }) + .ToArray() + }; + + ExtractRefreshTestCaches.SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls().Skip(1)); + + var tasks = await ServerExtractRefreshTask.CreateManyAsync(response, MockFinderFactory.Object, MockContentCacheFactory.Object, Logger, Localizer, Cancel); + + Assert.Equal(response.Items.Length - 1, tasks.Count); + } + } } }