diff --git a/examples/Csharp.ExampleApplication/Hooks/Transformers/SimpleScheduleStartAtTransformer.cs b/examples/Csharp.ExampleApplication/Hooks/Transformers/SimpleScheduleStartAtTransformer.cs new file mode 100644 index 00000000..2524905c --- /dev/null +++ b/examples/Csharp.ExampleApplication/Hooks/Transformers/SimpleScheduleStartAtTransformer.cs @@ -0,0 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Engine.Hooks.Transformers; +using Tableau.Migration.Resources; + +namespace Csharp.ExampleApplication.Hooks.Transformers +{ + #region class + public class SimpleScheduleStartAtTransformer + : ContentTransformerBase + where T : IWithSchedule + { + private readonly ILogger>? _logger; + + public SimpleScheduleStartAtTransformer( + ISharedResourcesLocalizer localizer, + ILogger> logger) + : base( + localizer, + logger) + { + _logger = logger; + } + + public override async Task TransformAsync( + T itemToTransform, + CancellationToken cancel) + { + // In this example, the `Start At` time is in the UTC time zone. + if (itemToTransform.Schedule.FrequencyDetails.StartAt is not null) + { + // A simple conversion to the EDT time zone. + var updatedStartAt = itemToTransform.Schedule.FrequencyDetails.StartAt.Value.AddHours(-4); + + _logger?.LogInformation( + @"Adjusting the 'Start At' from {previousStartAt} to {updatedStartAt}.", + itemToTransform.Schedule.FrequencyDetails.StartAt.Value, + updatedStartAt); + + itemToTransform.Schedule.FrequencyDetails.StartAt = updatedStartAt; + } + + return await Task.FromResult(itemToTransform); + } + } + #endregion +} diff --git a/examples/Python.ExampleApplication/Hooks/transformers/schedule_startat_transformer.py b/examples/Python.ExampleApplication/Hooks/transformers/schedule_startat_transformer.py new file mode 100644 index 00000000..f67cb229 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/transformers/schedule_startat_transformer.py @@ -0,0 +1,15 @@ +from datetime import time +from tableau_migration import ( + ContentTransformerBase, + ICloudExtractRefreshTask +) + +class SimpleScheduleStartAtTransformer(ContentTransformerBase[ICloudExtractRefreshTask]): + def transform(self, itemToTransform: ICloudExtractRefreshTask) -> ICloudExtractRefreshTask: + # In this example, the `Start At` time is in the UTC time zone. + if itemToTransform.schedule.frequency_details.start_at: + prev_start_at = itemToTransform.schedule.frequency_details.start_at + # A simple conversion to the EDT time zone. + itemToTransform.schedule.frequency_details.start_at = time(prev_start_at.hour - 4, prev_start_at.minute, prev_start_at.second, prev_start_at.microsecond); + + return itemToTransform \ No newline at end of file diff --git a/src/Documentation/articles/configuration/basic.md b/src/Documentation/articles/configuration/basic.md new file mode 100644 index 00000000..883267ad --- /dev/null +++ b/src/Documentation/articles/configuration/basic.md @@ -0,0 +1,40 @@ +# Basic Configuration + +## Migration Plan + +The migration plan is a required input in the migration process. It defines the Source and Destination servers and the hooks executed during the migration. Consider the Migration Plan as the steps the Migration SDK follows to migrate the information from a given source to a given destination. + +The [`IMigrationPlan`](xref:Tableau.Migration.IMigrationPlan) interface defines the Migration Plan structure. The easiest way to generate a new Migration Plan is using [`MigrationPlanBuilder`](xref:Tableau.Migration.Engine.MigrationPlanBuilder). Following are the steps needed before [building](#build) a new plan: + +- [Define the required Source server](#source). +- [Define the required Destination server](#destination). +- [Define the required Migration Type](#migration-type). + +### Source + + The method [`MigrationPlanBuilder.FromSourceTableauServer`](xref:Tableau.Migration.Engine.MigrationPlanBuilder.FromSourceTableauServer(System.Uri,System.String,System.String,System.String,System.Boolean)) defines the source server by instantiating a new [`TableauSiteConnectionConfiguration`](xref:Tableau.Migration.Api.TableauSiteConnectionConfiguration) with the following parameters: + +- **serverUrl** +- **siteContentUrl:**(optional) +- **accessTokenName** +- **accessToken** + +### Destination + +The method [`MigrationPlanBuilder.ToDestinationTableauCloud`](xref:Tableau.Migration.Engine.MigrationPlanBuilder.ToDestinationTableauCloud(System.Uri,System.String,System.String,System.String,System.Boolean)) defines the destination server by instantiating a new [`TableauSiteConnectionConfiguration`](xref:Tableau.Migration.Api.TableauSiteConnectionConfiguration) with the following parameters: + +- **podUrl** +- **siteContentUrl** The site name on Tableau Cloud. +- **accessTokenName** +- **accessToken** + +> [!Important] +> Personal access tokens (PATs) are long-lived authentication tokens that allow you to sign in to the Tableau REST API without requiring hard-coded credentials or interactive sign-in. Revoke and generate a new PAT every day to keep your server secure. Access tokens should not be stored in plain text in application configuration files, and should instead use secure alternatives such as encryption or a secrets management system. If the source and destination sites are on the same server, separate PATs should be used. + +### Migration Type + +The method [`MigrationPlanBuilder.ForServerToCloud`](xref:Tableau.Migration.Engine.MigrationPlanBuilder.ForServerToCloud) will define the migration type and load all default hooks for a **Server to Cloud** migration. + +### Build + +The method [`MigrationPlanBuilder.Build`](xref:Tableau.Migration.Engine.MigrationPlanBuilder.Build) generates a Migration Plan ready to be used as an input to a migration process. \ No newline at end of file diff --git a/src/Documentation/articles/index.md b/src/Documentation/articles/index.md new file mode 100644 index 00000000..a8f3304f --- /dev/null +++ b/src/Documentation/articles/index.md @@ -0,0 +1,58 @@ +# Introduction + +Welcome to the developer documentation for the Tableau Migration SDK. The Migration SDK is primarily written in C# using the [.NET](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet-framework) Framework. It includes a [Python API](~/api-python/index.md) to enable interoperability with Python. + +## Supported languages + +You can develop your migration application using one of the supported languages + +- [Python](https://www.python.org/) +- C# using the [.NET Framework](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet-framework) + +## Prerequisites + +To develop your application using the [Migration SDK](https://github.com/tableau/tableau-migration-sdk), you should + +- Understand framework and language concepts for one of the languages the SDK supports. +- Be able to design and write applications in one of the supported languages. +- Understand how to import and use third party packages +- Understand logging and general troubleshooting of applications. + +## Quick Start + +- Install a [.NET Runtime](https://dotnet.microsoft.com/en-us/download). +- Install the Migration SDK + +## [Python](#tab/Python) + + Install using PIP + - [PIP CLI](https://pip.pypa.io/en/stable/cli/pip_install): `pip install tableau_migration` + +## [C#](#tab/CSharp) + + Install using NuGet + - [dotnet CLI](https://learn.microsoft.com/en-us/nuget/quickstart/install-and-use-a-package-using-the-dotnet-cli): `dotnet add package Tableau.Migration` + - [Nuget Package Manager](https://learn.microsoft.com/en-us/nuget/quickstart/install-and-use-a-package-in-visual-studio): Search for `Tableau.Migration`. + +--- + +- Use the sample code in one of the [Reference](#getting-started-and-api-reference) sections to get started. +- Use the [Resource](#resources) sections to further customize your application. + +## Getting started and API Reference + +- [Python API Reference](~/api-python/index.md) : Getting started sample and the complete Python API Reference for comprehensive documentation. +- [C# API Reference](~/api-csharp/index.md): Getting started sample and the complete C# API Reference for detailed documentation. + +## Resources + +- [Articles](~/articles/index.md): Articles covering a range of topics, including customization of the Migration SDK. +- [Code Samples](~/samples/index.md): Code samples to kickstart your development process. + +## Source Code + +The Tableau Migration SDK is open source. The source code is in our [GitHub repo](https://github.com/tableau/tableau-migration-sdk). + +## Contributing + +Refer to this handy [contribution guide](https://github.com/tableau/tableau-migration-sdk/blob/main/CONTRIBUTING.md) if you would like to contribute to the Migration SDK. diff --git a/src/Documentation/images/configuration.svg b/src/Documentation/images/configuration.svg new file mode 100644 index 00000000..e31e40a1 --- /dev/null +++ b/src/Documentation/images/configuration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Documentation/images/hooks.svg b/src/Documentation/images/hooks.svg new file mode 100644 index 00000000..f2e5f662 --- /dev/null +++ b/src/Documentation/images/hooks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Documentation/samples/batch-migration-completed/index.md b/src/Documentation/samples/batch-migration-completed/index.md new file mode 100644 index 00000000..f22e29c3 --- /dev/null +++ b/src/Documentation/samples/batch-migration-completed/index.md @@ -0,0 +1,7 @@ +# Batch Migration Completed + +Batch Migration Completed Hooks allow custom logic to run when a migration batch completes. + +The following samples cover some common scenarios: + +- [Sample: Migration batch logging](~/samples/batch-migration-completed/log_migration_batches.md) diff --git a/src/Documentation/samples/bulk-post-publish/index.md b/src/Documentation/samples/bulk-post-publish/index.md new file mode 100644 index 00000000..1ba0bd3a --- /dev/null +++ b/src/Documentation/samples/bulk-post-publish/index.md @@ -0,0 +1,7 @@ +# Bulk Post-Publish Hooks + +Bulk post-publish hooks allow you to update content item batches after they are migrated to the destination. + +The following samples cover some common scenarios: + +- [Sample: Bulk logging](~/samples/bulk-post-publish/bulk_logging.md) diff --git a/src/Documentation/samples/filters/index.md b/src/Documentation/samples/filters/index.md new file mode 100644 index 00000000..d8e7b6de --- /dev/null +++ b/src/Documentation/samples/filters/index.md @@ -0,0 +1,12 @@ +# Filters + +Filters allow you to skip migrating certain content items. + +> [!Note] +> Filters do not have a cascading effect. You will need to write similar filters for the related content items as well. + +The following samples cover some common scenarios: + +- [Sample: Filter projects by name](~/samples/filters/filter_projects_by_name.md) + +- [Sample: Filter users by site role](~/samples/filters/filter_users_by_site_role.md) diff --git a/src/Documentation/samples/index.md b/src/Documentation/samples/index.md new file mode 100644 index 00000000..5de560fa --- /dev/null +++ b/src/Documentation/samples/index.md @@ -0,0 +1,48 @@ +# Code Samples + +Once you have started building your migration using the example code in [C#](~/api-csharp/index.md) or [Python](~/api-python/index.md), you may want to further customize your migration using hooks. This section provides code samples to assist you. + +[Learn more about hooks here.](~/articles/hooks/index.md) + +## Hook Registration + +To use hooks, you need to register them with the [plan builder](~/articles/configuration.md#migration-plan). + +The process of registering hooks differs slightly between C# and Python, as described below. + +# [Python](#tab/Python) + +In Python, injecting an object into the class constructor is not supported, and in most cases, it is unnecessary. + +To register a Python hook object, simply create the object and add it to the appropriate hook type collection. + +**Generic Form:** `plan_builder.[hook type collection].add([hook object])` + +**Example:** +``` +plan_builder.filters.add(UnlicensedUsersFilter) +``` + +# [C#](#tab/CSharp) + +In C#, the hook object should be registered with Dependency Injection (DI). This allows hooks to have any object injected into the constructor without concern for the source of the object. + +Learn more about [dependency injection](~/articles/dependency_injection.md). + +To register your hook object with the dependency injection service provider, simply add it. + +**Generic Form:** `services.AddScoped<[Object type]>();` + +**Example:** +``` +services.AddScoped(); +``` + +Once the hook is registered with DI, it must be added to the appropriate [hook collection](~/articles/hooks/custom_hooks.md). + +**Generic Form:** `_planBuilder.[hook type collection].Add<[object name], [hook type]>();` + +**Example:** +``` +_planBuilder.Filters.Add(); +``` diff --git a/src/Documentation/samples/mappings/index.md b/src/Documentation/samples/mappings/index.md new file mode 100644 index 00000000..4a1d8694 --- /dev/null +++ b/src/Documentation/samples/mappings/index.md @@ -0,0 +1,12 @@ +# Mappings + +Mappings allow you to change the locations of content items before they are migrated to the destination. + +> [!Note] +> Mappings do not have a cascading effect. You will need to write similar mappings for the related content items as well. + +The following samples cover some common scenarios: + +- [Sample: Username email](~/samples/mappings/username_email_mapping.md) +- [Sample: Rename projects](~/samples/mappings/rename_projects.md) +- [Sample: Change projects](~/samples/mappings/change_projects.md) diff --git a/src/Documentation/samples/migration-action-completed/index.md b/src/Documentation/samples/migration-action-completed/index.md new file mode 100644 index 00000000..0934c331 --- /dev/null +++ b/src/Documentation/samples/migration-action-completed/index.md @@ -0,0 +1,7 @@ +# Migration Action Completed + +Migration Action Completed Hooks allow custom logic to run when a migration action completes. + +The following samples cover some common scenarios: + +- [Sample: Migration action logging](~/samples/migration-action-completed/log_migration_actions.md) diff --git a/src/Documentation/samples/post-publish/index.md b/src/Documentation/samples/post-publish/index.md new file mode 100644 index 00000000..7da70bc5 --- /dev/null +++ b/src/Documentation/samples/post-publish/index.md @@ -0,0 +1,7 @@ +# Post-Publish Hooks + +Post-publish hooks allow you to update content items after they are migrated to the destination. + +The following samples cover some common scenarios: + +- [Sample: Update permissions](~/samples/post-publish/update_permissions.md) diff --git a/src/Documentation/samples/transformers/index.md b/src/Documentation/samples/transformers/index.md new file mode 100644 index 00000000..3d147372 --- /dev/null +++ b/src/Documentation/samples/transformers/index.md @@ -0,0 +1,9 @@ +# Transformers + +Transformers allow you to update content items before they are migrated to the destination. + +The following samples cover some common scenarios: + +- [Sample: Add tags to content](~/samples/transformers/migrated_tag_transformer.md) +- [Sample: Encrypt Extracts](~/samples/transformers/encrypt_extracts_transformer.md) +- [Sample: Adjust 'Start At' to Scheduled Tasks](~/samples/transformers/start_at_transformer.md) \ No newline at end of file diff --git a/src/Documentation/samples/transformers/start_at_transformer.md b/src/Documentation/samples/transformers/start_at_transformer.md new file mode 100644 index 00000000..efa75fa3 --- /dev/null +++ b/src/Documentation/samples/transformers/start_at_transformer.md @@ -0,0 +1,42 @@ +# Sample: Adjust 'Start At' to Scheduled Tasks + +This sample demonstrates how to adjust the 'Start At' of a Scheduled Task. + +Both the Python and C# transformer classes inherit from a base class responsible for the core functionality. + +## [Python](#tab/Python) + +### Transformer Class + +To adjust the 'Start At' in Python, you can use the following transformer class: + +[!code-python[](../../../../examples/Python.ExampleApplication/hooks/transformers/schedule_startat_transformer.py)] + +### Registration + +Refer to the [documentation](~/samples/index.md?tabs=Python#hook-registration) for instructions on registering the transformer. + +[//]: <> (Adding this as code as regions are not supported in python snippets) +```Python +plan_builder.transformers.add(SimpleScheduleStartAtTransformer) +``` + +## [C#](#tab/CSharp) + +### Transformer Class + +In C#, the transformer class for adjusting the 'Start At' of a given Scheduled Task is implemented as follows: + +[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Hooks/Transformers/SimpleScheduleStartAtTransformer.cs#class)] + +### Registration + +To register the transformer in C#, follow the guidance provided in the [documentation](~/samples/index.md?tabs=CSharp#hook-registration). + +[!code-csharp[](../../../../examples/Csharp.ExampleApplication/MyMigrationApplication.cs#StartAtTransformer-Registration)] + +### Dependency Injection + +Learn more about dependency injection [here](~/articles/dependency_injection.md). + +[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Program.cs#StartAtTransformer-DI)] diff --git a/src/Documentation/templates/tableau/partials/navbar-li.tmpl.partial b/src/Documentation/templates/tableau/partials/navbar-li.tmpl.partial new file mode 100644 index 00000000..3a395574 --- /dev/null +++ b/src/Documentation/templates/tableau/partials/navbar-li.tmpl.partial @@ -0,0 +1,30 @@ +{{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}} + + diff --git a/src/Python/.gitignore b/src/Python/.gitignore new file mode 100644 index 00000000..d694bdb9 --- /dev/null +++ b/src/Python/.gitignore @@ -0,0 +1,4 @@ +/Documentation/_build +/Documentation/_static +/Python.pyproj.user +/Documentation/generated diff --git a/src/Python/src/tableau_migration/migration_api_rest.py b/src/Python/src/tableau_migration/migration_api_rest.py new file mode 100644 index 00000000..301243ab --- /dev/null +++ b/src/Python/src/tableau_migration/migration_api_rest.py @@ -0,0 +1,47 @@ +# 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. + +"""Wrapper for classes in Tableau.Migration.Api.Rest namespace.""" + +# region _generated + +from uuid import UUID # noqa: E402, F401 + +from System import Guid # noqa: E402, F401 +from Tableau.Migration.Api.Rest import IRestIdentifiable # noqa: E402, F401 + +class PyRestIdentifiable(): + """Interface for an object that uses a REST API-style LUID identifier.""" + + _dotnet_base = IRestIdentifiable + + def __init__(self, rest_identifiable: IRestIdentifiable) -> None: + """Creates a new PyRestIdentifiable object. + + Args: + rest_identifiable: A IRestIdentifiable object. + + Returns: None. + """ + self._dotnet = rest_identifiable + + @property + def id(self) -> UUID: + """Gets the unique identifier.""" + return None if self._dotnet.Id is None else UUID(self._dotnet.Id.ToString()) + + +# endregion + diff --git a/src/Python/src/tableau_migration/migration_content_schedules.py b/src/Python/src/tableau_migration/migration_content_schedules.py new file mode 100644 index 00000000..ef1f4b00 --- /dev/null +++ b/src/Python/src/tableau_migration/migration_content_schedules.py @@ -0,0 +1,244 @@ +# 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. + +"""Wrapper for classes in Tableau.Migration.Content.Schedules namespace.""" + +# region _generated + +from datetime import time # noqa: E402, F401 +from enum import IntEnum # noqa: E402, F401 +from tableau_migration.migration import ( # noqa: E402, F401 + _generic_wrapper, + PyContentReference +) +from typing import ( # noqa: E402, F401 + Generic, + TypeVar, + List +) + +from System import TimeOnly # noqa: E402, F401 +from System.Collections.Generic import List as DotnetList # noqa: E402, F401 +from Tableau.Migration.Content.Schedules import ( # noqa: E402, F401 + IExtractRefreshTask, + IFrequencyDetails, + IInterval, + ISchedule, + IWithSchedule +) + +TSchedule = TypeVar("TSchedule") + +class PyExtractRefreshContentType(IntEnum): + """Enum of extract refresh content types.""" + + """Unknown content type.""" + UNKNOWN = 0 + + """Workbook content type.""" + WORKBOOK = 1 + + """Data source content type.""" + DATA_SOURCE = 2 + +class PyWithSchedule(Generic[TSchedule], PyContentReference): + """Interface to be inherited by content items with a schedule.""" + + _dotnet_base = IWithSchedule + + def __init__(self, with_schedule: IWithSchedule) -> None: + """Creates a new PyWithSchedule object. + + Args: + with_schedule: A IWithSchedule object. + + Returns: None. + """ + self._dotnet = with_schedule + + @property + def schedule(self) -> TSchedule: + """Gets the content item's schedule.""" + return None if self._dotnet.Schedule is None else _generic_wrapper(self._dotnet.Schedule) + +class PyExtractRefreshTask(Generic[TSchedule], PyWithSchedule[TSchedule]): + """Interface for an extract refresh task content item.""" + + _dotnet_base = IExtractRefreshTask + + def __init__(self, extract_refresh_task: IExtractRefreshTask) -> None: + """Creates a new PyExtractRefreshTask object. + + Args: + extract_refresh_task: A IExtractRefreshTask object. + + Returns: None. + """ + self._dotnet = extract_refresh_task + + @property + def type(self) -> str: + """Gets the extract refresh type.""" + return self._dotnet.Type + + @type.setter + def type(self, value: str) -> None: + """Gets the extract refresh type.""" + self._dotnet.Type = value + + @property + def content_type(self) -> PyExtractRefreshContentType: + """Gets the extract refresh task's content type.""" + return None if self._dotnet.ContentType is None else PyExtractRefreshContentType(self._dotnet.ContentType.value__) + + @content_type.setter + def content_type(self, value: PyExtractRefreshContentType) -> None: + """Gets the extract refresh task's content type.""" + self._dotnet.ContentType.value__ = PyExtractRefreshContentType(value) + + @property + def content(self) -> PyContentReference: + """Gets the extract refresh task's content.""" + return None if self._dotnet.Content is None else PyContentReference(self._dotnet.Content) + + @content.setter + def content(self, value: PyContentReference) -> None: + """Gets the extract refresh task's content.""" + self._dotnet.Content = None if value is None else value._dotnet + +class PyInterval(): + """Interface for a schedule interval.""" + + _dotnet_base = IInterval + + def __init__(self, interval: IInterval) -> None: + """Creates a new PyInterval object. + + Args: + interval: A IInterval object. + + Returns: None. + """ + self._dotnet = interval + + @property + def hours(self) -> int: + """Gets the interval hour value.""" + return self._dotnet.Hours + + @property + def minutes(self) -> int: + """Gets the interval minute value.""" + return self._dotnet.Minutes + + @property + def month_day(self) -> str: + """Gets the interval day of month value.""" + return self._dotnet.MonthDay + + @property + def week_day(self) -> str: + """Gets the interval day of week value.""" + return self._dotnet.WeekDay + +class PyFrequencyDetails(): + """Interface for a schedule's frequency details.""" + + _dotnet_base = IFrequencyDetails + + def __init__(self, frequency_details: IFrequencyDetails) -> None: + """Creates a new PyFrequencyDetails object. + + Args: + frequency_details: A IFrequencyDetails object. + + Returns: None. + """ + self._dotnet = frequency_details + + @property + def start_at(self) -> time: + """Gets the schedule's start time.""" + return None if self._dotnet.StartAt is None else time(self._dotnet.StartAt.Hour, self._dotnet.StartAt.Minute, self._dotnet.StartAt.Second, self._dotnet.StartAt.Millisecond * 1000) + + @start_at.setter + def start_at(self, value: time) -> None: + """Gets the schedule's start time.""" + self._dotnet.StartAt = None if value is None else TimeOnly.Parse(str(value)) + + @property + def end_at(self) -> time: + """Gets the schedule's end time.""" + return None if self._dotnet.EndAt is None else time(self._dotnet.EndAt.Hour, self._dotnet.EndAt.Minute, self._dotnet.EndAt.Second, self._dotnet.EndAt.Millisecond * 1000) + + @end_at.setter + def end_at(self, value: time) -> None: + """Gets the schedule's end time.""" + self._dotnet.EndAt = None if value is None else TimeOnly.Parse(str(value)) + + @property + def intervals(self) -> List[PyInterval]: + """Gets the schedule's intervals.""" + return [] if self._dotnet.Intervals is None else [PyInterval(x) for x in self._dotnet.Intervals if x is not None] + + @intervals.setter + def intervals(self, value: List[PyInterval]) -> None: + """Gets the schedule's intervals.""" + if value is None: + self._dotnet.Intervals = DotnetList[IInterval]() + else: + dotnet_collection = DotnetList[IInterval]() + for x in filter(None,value): + dotnet_collection.Add(x._dotnet) + self._dotnet.Intervals = dotnet_collection + +class PySchedule(): + """Interface for an API client schedule model.""" + + _dotnet_base = ISchedule + + def __init__(self, schedule: ISchedule) -> None: + """Creates a new PySchedule object. + + Args: + schedule: A ISchedule object. + + Returns: None. + """ + self._dotnet = schedule + + @property + def frequency(self) -> str: + """Gets the schedule's frequency.""" + return self._dotnet.Frequency + + @frequency.setter + def frequency(self, value: str) -> None: + """Gets the schedule's frequency.""" + self._dotnet.Frequency = value + + @property + def frequency_details(self) -> PyFrequencyDetails: + """Gets the schedule's frequency details.""" + return None if self._dotnet.FrequencyDetails is None else PyFrequencyDetails(self._dotnet.FrequencyDetails) + + @property + def next_run_at(self) -> str: + """Gets the schedule's next run time.""" + return self._dotnet.NextRunAt + + +# endregion + diff --git a/src/Python/src/tableau_migration/migration_content_schedules_cloud.py b/src/Python/src/tableau_migration/migration_content_schedules_cloud.py new file mode 100644 index 00000000..c8903eda --- /dev/null +++ b/src/Python/src/tableau_migration/migration_content_schedules_cloud.py @@ -0,0 +1,59 @@ +# 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. + +"""Wrapper for classes in Tableau.Migration.Content.Schedules.Cloud namespace.""" + +# region _generated + +from tableau_migration.migration_content_schedules import PySchedule # noqa: E402, F401 + +from Tableau.Migration.Content.Schedules.Cloud import ICloudSchedule # noqa: E402, F401 + +class PyCloudSchedule(PySchedule): + """Interface for a Tableau Cloud schedule.""" + + _dotnet_base = ICloudSchedule + + def __init__(self, cloud_schedule: ICloudSchedule) -> None: + """Creates a new PyCloudSchedule object. + + Args: + cloud_schedule: A ICloudSchedule object. + + Returns: None. + """ + self._dotnet = cloud_schedule + + +# endregion + +from Tableau.Migration.Content.Schedules.Cloud import ICloudExtractRefreshTask # noqa: E402, F401 +from tableau_migration.migration_content_schedules import PyExtractRefreshTask # noqa: E402, F401 + +class PyCloudExtractRefreshTask(PyExtractRefreshTask[PyCloudSchedule]): + """Interface for a cloud extract refresh task content item.""" + + _dotnet_base = ICloudExtractRefreshTask + + def __init__(self, cloud_extract_refresh_task: ICloudExtractRefreshTask) -> None: + """Creates a new PyCloudExtractRefreshTask object. + + Args: + cloud_extract_refresh_task: A ICloudExtractRefreshTask object. + + Returns: None. + """ + self._dotnet = cloud_extract_refresh_task + diff --git a/src/Python/src/tableau_migration/migration_content_schedules_server.py b/src/Python/src/tableau_migration/migration_content_schedules_server.py new file mode 100644 index 00000000..79249b2d --- /dev/null +++ b/src/Python/src/tableau_migration/migration_content_schedules_server.py @@ -0,0 +1,80 @@ +# 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. + +"""Wrapper for classes in Tableau.Migration.Content.Schedules.Server namespace.""" + +# region _generated + +from tableau_migration.migration import PyContentReference # noqa: E402, F401 +from tableau_migration.migration_content_schedules import PySchedule # noqa: E402, F401 + +from Tableau.Migration.Content.Schedules.Server import IServerSchedule # noqa: E402, F401 + +class PyServerSchedule(PySchedule, PyContentReference): + """Interface for server extract refresh schedule.""" + + _dotnet_base = IServerSchedule + + def __init__(self, server_schedule: IServerSchedule) -> None: + """Creates a new PyServerSchedule object. + + Args: + server_schedule: A IServerSchedule object. + + Returns: None. + """ + self._dotnet = server_schedule + + @property + def type(self) -> str: + """Gets the schedule's type.""" + return self._dotnet.Type + + @property + def state(self) -> str: + """Gets the schedule's state.""" + return self._dotnet.State + + @property + def created_at(self) -> str: + """Gets the schedule's created timestamp.""" + return self._dotnet.CreatedAt + + @property + def updated_at(self) -> str: + """Gets the schedule's updated timestamp.""" + return self._dotnet.UpdatedAt + + +# endregion + +from Tableau.Migration.Content.Schedules.Server import IServerExtractRefreshTask # noqa: E402, F401 +from tableau_migration.migration_content_schedules import PyExtractRefreshTask # noqa: E402, F401 + +class PyServerExtractRefreshTask(PyExtractRefreshTask[PyServerSchedule]): + """Interface for a server extract refresh task content item.""" + + _dotnet_base = IServerExtractRefreshTask + + def __init__(self, server_extract_refresh_task: IServerExtractRefreshTask) -> None: + """Creates a new PyServerExtractRefreshTask object. + + Args: + server_extract_refresh_task: A IServerExtractRefreshTask object. + + Returns: None. + """ + self._dotnet = server_extract_refresh_task + diff --git a/src/Python/tests/test_migration_api_rest.py b/src/Python/tests/test_migration_api_rest.py new file mode 100644 index 00000000..06acc31a --- /dev/null +++ b/src/Python/tests/test_migration_api_rest.py @@ -0,0 +1,41 @@ +# 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. + +# region _generated + +from uuid import UUID # noqa: E402, F401 + +from System import Guid # noqa: E402, F401 +from Tableau.Migration.Api.Rest import IRestIdentifiable # noqa: E402, F401 + +from tableau_migration.migration_api_rest import PyRestIdentifiable # noqa: E402, F401 +# Extra imports for tests. +from tests.helpers.autofixture import AutoFixtureTestBase # noqa: E402, F401 + +class TestPyRestIdentifiableGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IRestIdentifiable) + py = PyRestIdentifiable(dotnet) + assert py._dotnet == dotnet + + def test_id_getter(self): + dotnet = self.create(IRestIdentifiable) + py = PyRestIdentifiable(dotnet) + assert py.id == None if dotnet.Id is None else UUID(dotnet.Id.ToString()) + + +# endregion + diff --git a/src/Python/tests/test_migration_content_schedules.py b/src/Python/tests/test_migration_content_schedules.py new file mode 100644 index 00000000..5b7850df --- /dev/null +++ b/src/Python/tests/test_migration_content_schedules.py @@ -0,0 +1,267 @@ +# 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. + +# region _generated + +from datetime import time # noqa: E402, F401 +from enum import IntEnum # noqa: E402, F401 +from tableau_migration.migration import ( # noqa: E402, F401 + _generic_wrapper, + PyContentReference +) +from typing import ( # noqa: E402, F401 + Generic, + TypeVar, + List +) + +from System import TimeOnly # noqa: E402, F401 +from System.Collections.Generic import List as DotnetList # noqa: E402, F401 +from Tableau.Migration.Content.Schedules import ( # noqa: E402, F401 + IExtractRefreshTask, + IFrequencyDetails, + IInterval, + ISchedule, + IWithSchedule +) + +from tableau_migration.migration_content_schedules import ( # noqa: E402, F401 + PyExtractRefreshContentType, + PyExtractRefreshTask, + PyFrequencyDetails, + PyInterval, + PySchedule, + PyWithSchedule +) + + +from Tableau.Migration.Content.Schedules import ExtractRefreshContentType + +# Extra imports for tests. +from Tableau.Migration import IContentReference # noqa: E402, F401 +from System import ( # noqa: E402, F401 + Nullable, + TimeOnly, + String +) +from System.Collections.Generic import List as DotnetList # noqa: E402, F401 +from Tableau.Migration.Content.Schedules import ExtractRefreshContentType # noqa: E402, F401 +from tests.helpers.autofixture import AutoFixtureTestBase # noqa: E402, F401 + +class TestPyExtractRefreshTaskGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + assert py._dotnet == dotnet + + def test_type_getter(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + assert py.type == dotnet.Type + + def test_type_setter(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + + # create test data + testValue = self.create(String) + + # set property to new test value + py.type = testValue + + # assert value + assert py.type == testValue + + def test_content_type_getter(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + assert py.content_type.value == (None if dotnet.ContentType is None else PyExtractRefreshContentType(dotnet.ContentType.value__)).value + + def test_content_type_setter(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + + # create test data + testValue = self.create(ExtractRefreshContentType) + + # set property to new test value + py.content_type = None if testValue is None else PyExtractRefreshContentType(testValue.value__) + + # assert value + assert py.content_type == None if testValue is None else PyExtractRefreshContentType(testValue.value__) + + def test_content_getter(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + assert py.content == None if dotnet.Content is None else PyContentReference(dotnet.Content) + + def test_content_setter(self): + dotnet = self.create(IExtractRefreshTask[ISchedule]) + py = PyExtractRefreshTask[PySchedule](dotnet) + + # create test data + testValue = self.create(IContentReference) + + # set property to new test value + py.content = None if testValue is None else PyContentReference(testValue) + + # assert value + assert py.content == None if testValue is None else PyContentReference(testValue) + +class TestPyFrequencyDetailsGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + assert py._dotnet == dotnet + + def test_start_at_getter(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + assert py.start_at == None if dotnet.StartAt is None else time(dotnet.StartAt.Hour, dotnet.StartAt.Minute, dotnet.StartAt.Second, dotnet.StartAt.Millisecond * 1000) + + def test_start_at_setter(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + + # create test data + testValue = self.create(Nullable[TimeOnly]) + + # set property to new test value + py.start_at = None if testValue is None else time(testValue.Hour, testValue.Minute, testValue.Second, testValue.Millisecond * 1000) + + # assert value + assert py.start_at == None if testValue is None else time(testValue.Hour, testValue.Minute, testValue.Second, testValue.Millisecond * 1000) + + def test_end_at_getter(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + assert py.end_at == None if dotnet.EndAt is None else time(dotnet.EndAt.Hour, dotnet.EndAt.Minute, dotnet.EndAt.Second, dotnet.EndAt.Millisecond * 1000) + + def test_end_at_setter(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + + # create test data + testValue = self.create(Nullable[TimeOnly]) + + # set property to new test value + py.end_at = None if testValue is None else time(testValue.Hour, testValue.Minute, testValue.Second, testValue.Millisecond * 1000) + + # assert value + assert py.end_at == None if testValue is None else time(testValue.Hour, testValue.Minute, testValue.Second, testValue.Millisecond * 1000) + + def test_intervals_getter(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + assert len(dotnet.Intervals) != 0 + assert len(py.intervals) == len(dotnet.Intervals) + + def test_intervals_setter(self): + dotnet = self.create(IFrequencyDetails) + py = PyFrequencyDetails(dotnet) + assert len(dotnet.Intervals) != 0 + assert len(py.intervals) == len(dotnet.Intervals) + + # create test data + dotnetCollection = DotnetList[IInterval]() + dotnetCollection.Add(self.create(IInterval)) + dotnetCollection.Add(self.create(IInterval)) + testCollection = [] if dotnetCollection is None else [PyInterval(x) for x in dotnetCollection if x is not None] + + # set property to new test value + py.intervals = testCollection + + # assert value + assert len(py.intervals) == len(testCollection) + +class TestPyIntervalGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IInterval) + py = PyInterval(dotnet) + assert py._dotnet == dotnet + + def test_hours_getter(self): + dotnet = self.create(IInterval) + py = PyInterval(dotnet) + assert py.hours == dotnet.Hours + + def test_minutes_getter(self): + dotnet = self.create(IInterval) + py = PyInterval(dotnet) + assert py.minutes == dotnet.Minutes + + def test_month_day_getter(self): + dotnet = self.create(IInterval) + py = PyInterval(dotnet) + assert py.month_day == dotnet.MonthDay + + def test_week_day_getter(self): + dotnet = self.create(IInterval) + py = PyInterval(dotnet) + assert py.week_day == dotnet.WeekDay + +class TestPyScheduleGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(ISchedule) + py = PySchedule(dotnet) + assert py._dotnet == dotnet + + def test_frequency_getter(self): + dotnet = self.create(ISchedule) + py = PySchedule(dotnet) + assert py.frequency == dotnet.Frequency + + def test_frequency_setter(self): + dotnet = self.create(ISchedule) + py = PySchedule(dotnet) + + # create test data + testValue = self.create(String) + + # set property to new test value + py.frequency = testValue + + # assert value + assert py.frequency == testValue + + def test_frequency_details_getter(self): + dotnet = self.create(ISchedule) + py = PySchedule(dotnet) + assert py.frequency_details == None if dotnet.FrequencyDetails is None else PyFrequencyDetails(dotnet.FrequencyDetails) + + def test_next_run_at_getter(self): + dotnet = self.create(ISchedule) + py = PySchedule(dotnet) + assert py.next_run_at == dotnet.NextRunAt + +class TestPyWithScheduleGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IWithSchedule[ISchedule]) + py = PyWithSchedule[PySchedule](dotnet) + assert py._dotnet == dotnet + + def test_schedule_getter(self): + dotnet = self.create(IWithSchedule[ISchedule]) + py = PyWithSchedule[PySchedule](dotnet) + assert py.schedule == None if dotnet.Schedule is None else _generic_wrapper(dotnet.Schedule) + + +# endregion + diff --git a/src/Python/tests/test_migration_content_schedules_cloud.py b/src/Python/tests/test_migration_content_schedules_cloud.py new file mode 100644 index 00000000..f639b79b --- /dev/null +++ b/src/Python/tests/test_migration_content_schedules_cloud.py @@ -0,0 +1,26 @@ +# 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. + +from tests.helpers.autofixture import AutoFixtureTestBase # noqa: E402, F401 +from Tableau.Migration.Content.Schedules.Cloud import ICloudExtractRefreshTask # noqa: E402, F401 + +from tableau_migration.migration_content_schedules_cloud import PyCloudExtractRefreshTask # noqa: E402, F401 + +class TestPyCloudExtractRefreshTask(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(ICloudExtractRefreshTask) + py = PyCloudExtractRefreshTask(dotnet) + assert py._dotnet == dotnet \ No newline at end of file diff --git a/src/Python/tests/test_migration_content_schedules_server.py b/src/Python/tests/test_migration_content_schedules_server.py new file mode 100644 index 00000000..ee9c57ef --- /dev/null +++ b/src/Python/tests/test_migration_content_schedules_server.py @@ -0,0 +1,67 @@ +# 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. + +# region _generated + +from tableau_migration.migration import PyContentReference # noqa: E402, F401 +from tableau_migration.migration_content_schedules import PySchedule # noqa: E402, F401 + +from Tableau.Migration.Content.Schedules.Server import IServerSchedule # noqa: E402, F401 + +from tableau_migration.migration_content_schedules_server import PyServerSchedule # noqa: E402, F401 +# Extra imports for tests. +from tests.helpers.autofixture import AutoFixtureTestBase # noqa: E402, F401 + +class TestPyServerScheduleGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IServerSchedule) + py = PyServerSchedule(dotnet) + assert py._dotnet == dotnet + + def test_type_getter(self): + dotnet = self.create(IServerSchedule) + py = PyServerSchedule(dotnet) + assert py.type == dotnet.Type + + def test_state_getter(self): + dotnet = self.create(IServerSchedule) + py = PyServerSchedule(dotnet) + assert py.state == dotnet.State + + def test_created_at_getter(self): + dotnet = self.create(IServerSchedule) + py = PyServerSchedule(dotnet) + assert py.created_at == dotnet.CreatedAt + + def test_updated_at_getter(self): + dotnet = self.create(IServerSchedule) + py = PyServerSchedule(dotnet) + assert py.updated_at == dotnet.UpdatedAt + + +# endregion + +from Tableau.Migration.Content.Schedules.Server import IServerExtractRefreshTask # noqa: E402, F401 + +from tableau_migration.migration_content_schedules_server import PyServerExtractRefreshTask # noqa: E402, F401 + +class TestPyServerExtractRefreshTask(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IServerExtractRefreshTask) + py = PyServerExtractRefreshTask(dotnet) + assert py._dotnet == dotnet + diff --git a/src/Python/tests/test_migration_engine_manifest.py b/src/Python/tests/test_migration_engine_manifest.py new file mode 100644 index 00000000..f42f913e --- /dev/null +++ b/src/Python/tests/test_migration_engine_manifest.py @@ -0,0 +1,47 @@ +# 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. + +# region _generated + +from enum import IntEnum # noqa: E402, F401 +from tableau_migration.migration import ( # noqa: E402, F401 + PyContentLocation, + PyContentReference +) +from typing_extensions import Self # noqa: E402, F401 + +from Tableau.Migration.Engine.Manifest import IMigrationManifestEntryEditor # noqa: E402, F401 + +from tableau_migration.migration_engine_manifest import ( # noqa: E402, F401 + PyMigrationManifestEntryEditor, + PyMigrationManifestEntryStatus +) + + +from Tableau.Migration.Engine.Manifest import MigrationManifestEntryStatus + +# Extra imports for tests. +from tests.helpers.autofixture import AutoFixtureTestBase # noqa: E402, F401 + +class TestPyMigrationManifestEntryEditorGenerated(AutoFixtureTestBase): + + def test_ctor(self): + dotnet = self.create(IMigrationManifestEntryEditor) + py = PyMigrationManifestEntryEditor(dotnet) + assert py._dotnet == dotnet + + +# endregion + diff --git a/src/Tableau.Migration.PythonGenerator/GenerationListHelper.cs b/src/Tableau.Migration.PythonGenerator/GenerationListHelper.cs new file mode 100644 index 00000000..73187e7a --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/GenerationListHelper.cs @@ -0,0 +1,29 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using System; + +namespace Tableau.Migration.PythonGenerator +{ + internal static class GenerationListHelper + { + internal static ImmutableHashSet ToTypeNameHash(params Type[] types) + => types.Select(t => t.FullName!).ToImmutableHashSet(); + } +} \ No newline at end of file diff --git a/src/Tableau.Migration.PythonGenerator/PythonTestGenerationList.cs b/src/Tableau.Migration.PythonGenerator/PythonTestGenerationList.cs new file mode 100644 index 00000000..d6d2bd8e --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonTestGenerationList.cs @@ -0,0 +1,188 @@ +// +// 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.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Tableau.Migration.Api.Rest.Models; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Permissions; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Engine; +using Tableau.Migration.Engine.Actions; +using Tableau.Migration.Engine.Hooks.Mappings; +using Tableau.Migration.Engine.Hooks.PostPublish; +using Tableau.Migration.Engine.Manifest; +using Tableau.Migration.Engine.Migrators; +using Tableau.Migration.Engine.Migrators.Batch; +using Tableau.Migration.PythonGenerator.Writers.Imports; +using Dotnet = Tableau.Migration.PythonGenerator.Keywords.Dotnet; + + +namespace Tableau.Migration.PythonGenerator +{ + internal class PythonTestGenerationList + { + private static readonly ImmutableHashSet TYPES_TO_EXCLUDE = GenerationListHelper.ToTypeNameHash( + + #region - Tableau.Migration.Engine.Manifest - + + typeof(IMigrationManifestEntry), + + #endregion + + #region - Tableau.Migration.Engine - + + typeof(ContentMigrationItem<>), + + #endregion + + #region - Tableau.Migration.Engine.Hooks.PostPublish - + + typeof(ContentItemPostPublishContext<,>), + + #endregion + + #region - Tableau.Migration.Engine.Hooks.Mappings - + + typeof(ContentMappingContext<>), + + #endregion + + #region - Tableau.Migration.Engine.Hooks.PostPublish - + typeof(BulkPostPublishContext<>), + + #endregion + + #region - Tableau.Migration.Engine.Migrators - + typeof(IContentItemMigrationResult<>), + + #endregion + + #region - Tableau.Migration.Engine.Migrators.Batch - + + typeof(IContentBatchMigrationResult<>), + + #endregion + + #region - Tableau.Migration.Engine.Actions - + + typeof(IMigrationActionResult), + + #endregion + + #region - Tableau.Migration.Api.Rest.Models - + + typeof(AdministratorLevels), + typeof(ContentPermissions), + typeof(ExtractEncryptionModes), + typeof(LabelCategories), + typeof(LicenseLevels), + typeof(PermissionsCapabilityModes), + typeof(PermissionsCapabilityNames), + typeof(SiteRoles), + + #endregion + + #region - Tableau.Migration.Api.Rest.Models.Types - + + typeof(AuthenticationTypes), + typeof(DataSourceFileTypes), + typeof(WorkbookFileTypes), + + #endregion + + #region - Tableau.Migration.Content.Schedules.Cloud - + + // Excluded because ICloudSchedule needs wrapped but there is nothing in it to test. + typeof(ICloudSchedule) + + #endregion + ); + + private static readonly ImportedModule IContentReferenceImport = new( + Dotnet.Namespaces.TABLEAU_MIGRATION, + new ImportedType(nameof(IContentReference))); + + private static readonly ImportedModule DotnetListImport = new( + Dotnet.Namespaces.SYSTEM_COLLECTIONS_GENERIC, + new ImportedType(Dotnet.Types.LIST, Dotnet.TypeAliases.LIST)); + + private static readonly Dictionary> NAMESPACE_IMPORTS = new() + { + { + $"{typeof(IUser).Namespace}", + new List() + { + IContentReferenceImport, + new(Dotnet.Namespaces.SYSTEM, [new ImportedType(Dotnet.Types.BOOLEAN), new ImportedType(Dotnet.Types.NULLABLE)]) + } + }, + { + $"{typeof(IPermissions).Namespace}", + new List() + { + IContentReferenceImport, + new(Dotnet.Namespaces.SYSTEM,new ImportedType(Dotnet.Types.NULLABLE)), + DotnetListImport + } + }, + { + $"{typeof(ISchedule).Namespace}", + new List() + { + IContentReferenceImport, + new(Dotnet.Namespaces.SYSTEM, [new ImportedType(Dotnet.Types.NULLABLE), new ImportedType(Dotnet.Types.TIME_ONLY), new ImportedType(Dotnet.Types.STRING)]), + DotnetListImport, + new($"{typeof(ExtractRefreshContentType).Namespace}",new ImportedType(nameof(ExtractRefreshContentType))) + } + } + }; + + internal static bool ShouldGenerateTests(INamedTypeSymbol? type) + { + if (type == null) + return false; + + var argCount = type.TypeArguments.Length; + + var typeName = $"{type?.ContainingNamespace}.{type?.Name}{(argCount > 0 ? $"`{argCount}" : string.Empty)}"; + + return !string.IsNullOrEmpty(typeName) && !TYPES_TO_EXCLUDE.Contains(typeName); + } + + internal static List GetExtraImportsByNamespace(string nameSpace, PythonTypeCache pyTypeCache) + { + var types = pyTypeCache + .Types + .Where(x + => x.DotNetType?.ContainingNamespace?.ToDisplayString() != null + && x.DotNetType.ContainingNamespace.ToDisplayString() == nameSpace) + .ToArray(); + + if (types.Length == 0 || !NAMESPACE_IMPORTS.ContainsKey(nameSpace)) + { + return []; + } + + return NAMESPACE_IMPORTS[nameSpace]; + } + } +} diff --git a/src/Tableau.Migration/Api/ICloudTasksApiClient.cs b/src/Tableau.Migration/Api/ICloudTasksApiClient.cs new file mode 100644 index 00000000..c70ac3bb --- /dev/null +++ b/src/Tableau.Migration/Api/ICloudTasksApiClient.cs @@ -0,0 +1,61 @@ +// +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Api.Models.Cloud; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; + +namespace Tableau.Migration.Api +{ + /// + /// Interface for API client Cloud tasks operations. + /// + public interface ICloudTasksApiClient : + IContentApiClient, + IPublishApiClient + { + /// + /// Gets a list of extract refresh tasks. + /// + /// The cancellation token to obey. + Task>> GetAllExtractRefreshTasksAsync(CancellationToken cancel); + + /// + /// Create an extract refresh task. + /// + /// The new extract refresh task's details. + /// The cancellation token to obey. + /// The published extract refresh task. + Task> CreateExtractRefreshTaskAsync( + ICreateExtractRefreshTaskOptions options, + CancellationToken cancel); + + /// + /// Deletes an extract refresh task. + /// + /// The extract refresh task's ID. + /// The cancellation token to obey. + /// + Task DeleteExtractRefreshTaskAsync( + Guid extractRefreshTaskId, + CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration/Api/ISchedulesApiClient.cs b/src/Tableau.Migration/Api/ISchedulesApiClient.cs new file mode 100644 index 00000000..1b3e2dfd --- /dev/null +++ b/src/Tableau.Migration/Api/ISchedulesApiClient.cs @@ -0,0 +1,58 @@ +// +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Paging; + +namespace Tableau.Migration.Api +{ + /// + /// Interface for API client job operations. + /// + public interface ISchedulesApiClient : + IContentApiClient, + IReadApiClient + { + /// + /// Gets the paged list of extract refresh tasks for a given schedule ID. + /// + /// The schedule's ID. + /// The 1-indexed page number. + /// The size of the page. + /// The cancellation token to obey. + /// The extract refresh tasks for the given schedule ID. + Task> GetScheduleExtractRefreshTasksAsync( + Guid scheduleId, + int pageNumber, + int pageSize, + CancellationToken cancel); + + /// + /// Gets all the extract refresh tasks for a given schedule ID. + /// + /// The schedule's ID. + /// The cancellation token to obey. + /// The extract refresh tasks for the given schedule ID. + Task>> GetAllScheduleExtractRefreshTasksAsync( + Guid scheduleId, + CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration/Api/IServerTasksApiClient.cs b/src/Tableau.Migration/Api/IServerTasksApiClient.cs new file mode 100644 index 00000000..f4863306 --- /dev/null +++ b/src/Tableau.Migration/Api/IServerTasksApiClient.cs @@ -0,0 +1,42 @@ +// +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Content.Schedules.Server; + +namespace Tableau.Migration.Api +{ + /// + /// Interface for API client Server tasks operations. + /// + public interface IServerTasksApiClient : + IContentApiClient, + IPullApiClient, + IApiPageAccessor, + IPagedListApiClient + { + /// + /// Gets a list of extract refresh tasks. + /// + /// The cancellation token to obey. + Task>> GetAllExtractRefreshTasksAsync(CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration/Api/ITasksApiClient.cs b/src/Tableau.Migration/Api/ITasksApiClient.cs new file mode 100644 index 00000000..540defe1 --- /dev/null +++ b/src/Tableau.Migration/Api/ITasksApiClient.cs @@ -0,0 +1,35 @@ +// +// 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. +// + +namespace Tableau.Migration.Api +{ + /// + /// Interface for API client tasks operations. + /// + public interface ITasksApiClient : IServerTasksApiClient, ICloudTasksApiClient + { + /// + /// Gets the server tasks API client + /// + IServerTasksApiClient ForServer(); + + /// + /// Gets the cloud tasks API client + /// + ICloudTasksApiClient ForCloud(); + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Models/Cloud/CreateExtractRefreshTaskOptions.cs b/src/Tableau.Migration/Api/Models/Cloud/CreateExtractRefreshTaskOptions.cs new file mode 100644 index 00000000..a4651317 --- /dev/null +++ b/src/Tableau.Migration/Api/Models/Cloud/CreateExtractRefreshTaskOptions.cs @@ -0,0 +1,60 @@ +// +// 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 Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; + +namespace Tableau.Migration.Api.Models.Cloud +{ + /// + /// Class for Tableau Cloud API client extract refresh task creation options. + /// + public class CreateExtractRefreshTaskOptions : ICreateExtractRefreshTaskOptions + { + /// + public string Type { get; } + + /// + public ExtractRefreshContentType ContentType { get; } + + /// + public Guid ContentId { get; } + + /// + public ICloudSchedule Schedule { get; } + + /// + /// Creates a new instance. + /// + /// The extract refresh type. + /// The extract refresh task's content type. + /// The extract refresh task's content ID. + /// The extract refresh task's schedule. + public CreateExtractRefreshTaskOptions( + string type, + ExtractRefreshContentType contentType, + Guid contentId, + ICloudSchedule schedule) + { + Type = type; + ContentType = contentType; + ContentId = contentId; + Schedule = schedule; + } + } +} diff --git a/src/Tableau.Migration/Api/Models/Cloud/ICreateExtractRefreshTaskOptions.cs b/src/Tableau.Migration/Api/Models/Cloud/ICreateExtractRefreshTaskOptions.cs new file mode 100644 index 00000000..25d4de55 --- /dev/null +++ b/src/Tableau.Migration/Api/Models/Cloud/ICreateExtractRefreshTaskOptions.cs @@ -0,0 +1,49 @@ +// +// 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 Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; + +namespace Tableau.Migration.Api.Models.Cloud +{ + /// + /// Interface for an API client extract refresh task creation model. + /// + public interface ICreateExtractRefreshTaskOptions + { + /// + /// Gets the type of extract refresh. FullRefresh or IncrementalRefresh. + /// + string Type { get; } + + /// + /// Gets the extract refresh task's content type. + /// + ExtractRefreshContentType ContentType { get; } + + /// + /// Gets the extract refresh task's content ID. + /// + Guid ContentId { get; } + + /// + /// Gets the extract refresh task's schedule. + /// + ICloudSchedule Schedule { get; } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/IScheduleType.cs b/src/Tableau.Migration/Api/Rest/Models/IScheduleType.cs new file mode 100644 index 00000000..3ce13f1b --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/IScheduleType.cs @@ -0,0 +1,42 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses; + +namespace Tableau.Migration.Api.Rest.Models +{ + /// + /// Interface for a schedule response item. + /// + public interface IScheduleType + { + /// + /// Gets or sets the schedule's frequency. + /// + string? Frequency { get; set; } + + /// + /// Gets the schedule's frequency details. + /// + IScheduleFrequencyDetailsType? FrequencyDetails { get; } + + /// + /// Gets or sets the schedule's next run time. + /// + string? NextRunAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Rest/Models/IWithDataSourceReferenceType.cs b/src/Tableau.Migration/Api/Rest/Models/IWithDataSourceReferenceType.cs new file mode 100644 index 00000000..8259e5a3 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/IWithDataSourceReferenceType.cs @@ -0,0 +1,30 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models +{ + /// + /// Interface for an object that has a data source reference. + /// + public interface IWithDataSourceReferenceType + { + /// + /// Gets the data source for the response. + /// + IRestIdentifiable? DataSource { get; } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/IWithScheduleReferenceType.cs b/src/Tableau.Migration/Api/Rest/Models/IWithScheduleReferenceType.cs new file mode 100644 index 00000000..42df31af --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/IWithScheduleReferenceType.cs @@ -0,0 +1,32 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses; + +namespace Tableau.Migration.Api.Rest.Models +{ + /// + /// Interface for an object that has a schedule reference. + /// + public interface IWithScheduleReferenceType + { + /// + /// Gets the schedule for the response. + /// + IScheduleReferenceType? Schedule { get; } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/IWithWorkbookReferenceType.cs b/src/Tableau.Migration/Api/Rest/Models/IWithWorkbookReferenceType.cs new file mode 100644 index 00000000..a74f02f4 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/IWithWorkbookReferenceType.cs @@ -0,0 +1,30 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models +{ + /// + /// Interface for an object that has a workbook reference. + /// + public interface IWithWorkbookReferenceType + { + /// + /// Gets the workbook for the response. + /// + IRestIdentifiable? Workbook { get; } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Requests/Cloud/CreateExtractRefreshTaskRequest.cs b/src/Tableau.Migration/Api/Rest/Models/Requests/Cloud/CreateExtractRefreshTaskRequest.cs new file mode 100644 index 00000000..d10a6dbc --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Requests/Cloud/CreateExtractRefreshTaskRequest.cs @@ -0,0 +1,320 @@ +// +// 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.Linq; +using System.Xml.Serialization; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using CloudModels = Tableau.Migration.Api.Models.Cloud; + +namespace Tableau.Migration.Api.Rest.Models.Requests.Cloud +{ + /// + /// + /// Class representing a create extract refresh task request. + /// + /// + /// See https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#create_cloud_extract_refresh_task for documentation. + /// + /// + [XmlType(XmlTypeName)] + public class CreateExtractRefreshTaskRequest : TableauServerRequest + { + /// + /// Gets or sets the extract refresh for the request. + /// + [XmlElement("extractRefresh")] + public ExtractRefreshType? ExtractRefresh { get; set; } + + /// + /// Gets or sets the schedule for the request. + /// + [XmlElement("schedule")] + public ScheduleType? Schedule { get; set; } + + /// + /// Creates a new instance. + /// + public CreateExtractRefreshTaskRequest() + { } + + /// + /// Creates a new instance. + /// + /// The extract refresh type. + /// The extract refresh task's content type. + /// The extract refresh task's content ID. + /// The extract refresh task's schedule. + public CreateExtractRefreshTaskRequest( + string type, + ExtractRefreshContentType contentType, + Guid contentId, + ICloudSchedule schedule) + { + ExtractRefresh = new ExtractRefreshType( + type, + contentType, + contentId); + Schedule = new ScheduleType( + schedule); + } + + /// + /// Creates a new instance. + /// + /// The extract refresh task creation options. + public CreateExtractRefreshTaskRequest( + CloudModels.ICreateExtractRefreshTaskOptions options) + : this( + options.Type, + options.ContentType, + options.ContentId, + options.Schedule) + { } + + /// + /// Class representing a request extract refresh item. + /// + public class ExtractRefreshType + { + /// + /// Gets or sets the type of extract refresh to the request. + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + /// Gets or sets the data source for the request. + /// + [XmlElement("datasource")] + public DataSourceType? DataSource { get; set; } + + /// + /// Gets or sets the workbook for the request. + /// + [XmlElement("workbook")] + public WorkbookType? Workbook { get; set; } + + /// + /// Creates a new instance. + /// + public ExtractRefreshType() + { } + + /// + /// Creates a new instance. + /// + /// The extract refresh type. + /// The extract refresh task's content type. + /// The extract refresh task's content ID. + public ExtractRefreshType( + string type, + ExtractRefreshContentType contentType, + Guid contentId) + { + Type = type; + switch (contentType) + { + case ExtractRefreshContentType.Workbook: + Workbook = new(contentId); + break; + + case ExtractRefreshContentType.DataSource: + DataSource = new(contentId); + break; + } + } + + /// + /// Class representing a request data source item. + /// + public class DataSourceType + { + /// + /// Gets or sets the ID for the request. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + /// Creates a new instance. + /// + public DataSourceType() + { } + + /// + /// Creates a new instance. + /// + public DataSourceType(Guid dataSourceId) + { + Id = dataSourceId; + } + } + + /// + /// Class representing a request workbook item. + /// + public class WorkbookType + { + /// + /// Gets or sets the ID for the request. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + /// Creates a new instance. + /// + public WorkbookType() + { } + + /// + /// Creates a new instance. + /// + public WorkbookType(Guid workbookId) + { + Id = workbookId; + } + } + } + + /// + /// Class representing a request schedule item. + /// + public class ScheduleType + { + /// + /// Gets or sets the frequency for the request. + /// + [XmlAttribute("frequency")] + public string? Frequency { get; set; } + + /// + /// Gets or sets the frequency details for the request. + /// + [XmlElement("frequencyDetails")] + public FrequencyDetailsType? FrequencyDetails { get; set; } + + /// + /// Creates a new instance. + /// + public ScheduleType() + { } + + /// + /// Creates a new instance. + /// + /// The schedule to copy from. + public ScheduleType(ICloudSchedule schedule) + { + Frequency = schedule.Frequency; + FrequencyDetails = new(schedule.FrequencyDetails); + } + + /// + /// Class representing a request frequency details item. + /// + public class FrequencyDetailsType + { + /// + /// Gets or sets the start time for the request. + /// + [XmlAttribute("start")] + public string? Start { get; set; } + + /// + /// Gets or sets the end time for the request. + /// + [XmlAttribute("end")] + public string? End { get; set; } + + /// + /// Gets or sets the intervals for the request. + /// + [XmlArray("intervals")] + [XmlArrayItem("interval")] + public IntervalType[] Intervals { get; set; } = Array.Empty(); + + /// + /// Creates a new instance. + /// + public FrequencyDetailsType() + { } + + /// + /// Creates a new instance. + /// + /// The frequency details to copy from. + public FrequencyDetailsType(IFrequencyDetails frequencyDetails) + { + Start = frequencyDetails.StartAt?.ToString(Constants.FrequencyTimeFormat); + End = frequencyDetails.EndAt?.ToString(Constants.FrequencyTimeFormat); + Intervals = frequencyDetails.Intervals.Select(i => new IntervalType(i)).ToArray(); + } + + /// + /// Class representing a request interval item. + /// + public class IntervalType + { + /// + /// Gets or sets the hours for the request. + /// + [XmlAttribute("hours")] + public string? Hours { get; set; } + + /// + /// Gets or sets the minutes for the request. + /// + [XmlAttribute("minutes")] + public string? Minutes { get; set; } + + /// + /// Gets or sets the weekday for the request. + /// + [XmlAttribute("weekDay")] + public string? WeekDay { get; set; } + + /// + /// Gets or sets the month/day for the request. + /// + [XmlAttribute("monthDay")] + public string? MonthDay { get; set; } + + /// + /// Creates a new instance. + /// + public IntervalType() + { } + + /// + /// Creates a new instance. + /// + /// The interval to copy from. + public IntervalType(IInterval interval) + { + Hours = interval.Hours?.ToString(); + Minutes = interval.Minutes?.ToString(); + WeekDay = interval.WeekDay; + MonthDay = interval.MonthDay; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/CreateExtractRefreshTaskResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/CreateExtractRefreshTaskResponse.cs new file mode 100644 index 00000000..de9de396 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/CreateExtractRefreshTaskResponse.cs @@ -0,0 +1,195 @@ +// +// 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.Xml.Serialization; + +namespace Tableau.Migration.Api.Rest.Models.Responses.Cloud +{ + /// + /// Class representing an extract refresh task creation response. + /// https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#create_cloud_extract_refresh_task + /// + [XmlType(XmlTypeName)] + public class CreateExtractRefreshTaskResponse : TableauServerResponse + { + /// + /// Gets or sets the extract refresh for the response. + /// + [XmlElement("extractRefresh")] + public override ExtractRefreshType? Item { get; set; } + + /// + /// Gets or sets the schedule for the response. + /// + [XmlElement("schedule")] + public ScheduleType Schedule { get; set; } = new(); + + /// + /// Class representing a response extract refresh item. + /// + public class ExtractRefreshType : ICloudExtractRefreshType + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + /// Gets or sets the priority for the response. + /// + [XmlAttribute("priority")] + public int Priority { get; set; } + + /// + /// Gets or sets the type for the response. + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + /// Gets or sets the consecutive failed count for the response. + /// + [XmlAttribute("consecutiveFailedCount")] + public int ConsecutiveFailedCount { get; set; } + + /// + /// Gets or sets the data source for the response. + /// + [XmlElement("datasource")] + public DataSourceType? DataSource { get; set; } + + /// + /// Gets or sets the workbook for the response. + /// + [XmlElement("workbook")] + public WorkbookType? Workbook { get; set; } + + IRestIdentifiable? IWithDataSourceReferenceType.DataSource => DataSource; + IRestIdentifiable? IWithWorkbookReferenceType.Workbook => Workbook; + + /// + /// Class representing a response data source item. + /// + public class DataSourceType : IRestIdentifiable + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + + /// + /// Class representing a response workbook item. + /// + public class WorkbookType : IRestIdentifiable + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + } + + /// + /// Class representing a response schedule item. + /// + public class ScheduleType : ICloudScheduleType + { + /// + /// Gets or sets the frequency for the response. + /// + [XmlAttribute("frequency")] + public string? Frequency { get; set; } + + /// + /// Gets or sets the next run time for the response. + /// + [XmlAttribute("nextRunAt")] + public string? NextRunAt { get; set; } + + /// + /// Gets or sets the frequency details for the response. + /// + [XmlElement("frequencyDetails")] + public FrequencyDetailsType? FrequencyDetails { get; set; } + + IScheduleFrequencyDetailsType? IScheduleType.FrequencyDetails => FrequencyDetails; + + /// + /// Class representing a response frequency details item. + /// + public class FrequencyDetailsType : IScheduleFrequencyDetailsType + { + /// + /// Gets or sets the start time for the response. + /// + [XmlAttribute("start")] + public string? Start { get; set; } + + /// + /// Gets or sets the end time for the response. + /// + [XmlAttribute("end")] + public string? End { get; set; } + + /// + /// Gets or sets the intervals for the response. + /// + [XmlArray("intervals")] + [XmlArrayItem("interval")] + public IntervalType[] Intervals { get; set; } = Array.Empty(); + + /// + IScheduleIntervalType[] IScheduleFrequencyDetailsType.Intervals => Intervals; + + /// + /// Class representing a response interval item. + /// + public class IntervalType : IScheduleIntervalType + { + /// + /// Gets or sets the hours for the response. + /// + [XmlAttribute("hours")] + public string? Hours { get; set; } + + /// + /// Gets or sets the minutes for the response. + /// + [XmlAttribute("minutes")] + public string? Minutes { get; set; } + + /// + /// Gets or sets the weekday for the response. + /// + [XmlAttribute("weekDay")] + public string? WeekDay { get; set; } + + /// + /// Gets or sets the month/day for the response. + /// + [XmlAttribute("monthDay")] + public string? MonthDay { get; set; } + } + } + } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ExtractRefreshTasksResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ExtractRefreshTasksResponse.cs new file mode 100644 index 00000000..0456540d --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ExtractRefreshTasksResponse.cs @@ -0,0 +1,200 @@ +// +// 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.Xml.Serialization; + +namespace Tableau.Migration.Api.Rest.Models.Responses.Cloud +{ + /// + /// Class representing a cloud extract refresh tasks response. + /// https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#tableau-cloud-request3 + /// + [XmlType(XmlTypeName)] + public class ExtractRefreshTasksResponse : TableauServerListResponse + { + /// + /// Gets or sets the extract refresh tasks for the response. + /// + [XmlArray("tasks")] + [XmlArrayItem("task")] + public override TaskType[] Items { get; set; } = Array.Empty(); + + /// + /// Class representing a response task item. + /// + public class TaskType + { + /// + /// Gets or sets the extract refresh for the response. + /// + [XmlElement("extractRefresh")] + public ExtractRefreshType? ExtractRefresh { get; set; } + + /// + /// Class representing a response extract refresh item. + /// + public class ExtractRefreshType : ICloudExtractRefreshType + { + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + [XmlAttribute("priority")] + public int Priority { get; set; } + + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + [XmlAttribute("consecutiveFailedCount")] + public int ConsecutiveFailedCount { get; set; } + + /// + /// Gets or sets the schedule for the response. + /// + [XmlElement("schedule")] + public ScheduleType Schedule { get; set; } = new(); + + /// + /// Gets or sets the data source for the response. + /// + [XmlElement("datasource")] + public DataSourceType? DataSource { get; set; } + + /// + /// Gets or sets the workbook for the response. + /// + [XmlElement("workbook")] + public WorkbookType? Workbook { get; set; } + + IRestIdentifiable? IWithWorkbookReferenceType.Workbook => Workbook; + IRestIdentifiable? IWithDataSourceReferenceType.DataSource => DataSource; + + /// + /// Class representing a response schedule item. + /// + public class ScheduleType : ICloudScheduleType + { + /// + /// Gets or sets the frequency for the response. + /// + [XmlAttribute("frequency")] + public string? Frequency { get; set; } + + /// + /// Gets or sets the frequency details for the response. + /// + [XmlElement("frequencyDetails")] + public FrequencyDetailsType? FrequencyDetails { get; set; } + + /// + /// Gets or sets the next run at for the response. + /// + [XmlAttribute("nextRunAt")] + public string? NextRunAt { get; set; } + + IScheduleFrequencyDetailsType? IScheduleType.FrequencyDetails => FrequencyDetails; + + /// + /// Class representing a response frequency details item. + /// + public class FrequencyDetailsType : IScheduleFrequencyDetailsType + { + /// + /// Gets or sets the start time for the response. + /// + [XmlAttribute("start")] + public string? Start { get; set; } + + /// + /// Gets or sets the end time for the response. + /// + [XmlAttribute("end")] + public string? End { get; set; } + + /// + /// Gets or sets the intervals for the response. + /// + [XmlArray("intervals")] + [XmlArrayItem("interval")] + public IntervalType[] Intervals { get; set; } = Array.Empty(); + + /// + IScheduleIntervalType[] IScheduleFrequencyDetailsType.Intervals => Intervals; + + /// + /// Class representing a response interval item. + /// + public class IntervalType : IScheduleIntervalType + { + /// + /// Gets or sets the hours for the response. + /// + [XmlAttribute("hours")] + public string? Hours { get; set; } + + /// + /// Gets or sets the minutes for the response. + /// + [XmlAttribute("minutes")] + public string? Minutes { get; set; } + + /// + /// Gets or sets the weekday for the response. + /// + [XmlAttribute("weekDay")] + public string? WeekDay { get; set; } + + /// + /// Gets or sets the month/day for the response. + /// + [XmlAttribute("monthDay")] + public string? MonthDay { get; set; } + } + } + } + + /// + /// Class representing a response data source item. + /// + public class DataSourceType : IRestIdentifiable + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + + /// + /// Class representing a response workbook item. + /// + public class WorkbookType : IRestIdentifiable + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + } + } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ICloudExtractRefreshType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ICloudExtractRefreshType.cs new file mode 100644 index 00000000..750e833f --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ICloudExtractRefreshType.cs @@ -0,0 +1,34 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses.Cloud +{ + /// + /// Interface for a Cloud extract refresh response type. + /// + public interface ICloudExtractRefreshType : IExtractRefreshType + { + } + + /// + /// Interface for a Cloud extract refresh response type. + /// + public interface ICloudExtractRefreshType : ICloudExtractRefreshType, IExtractRefreshType + where TWorkbook : IRestIdentifiable + where TDataSource : IRestIdentifiable + { } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ICloudScheduleType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ICloudScheduleType.cs new file mode 100644 index 00000000..0d98ae07 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Cloud/ICloudScheduleType.cs @@ -0,0 +1,25 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses.Cloud +{ + /// + /// Interface for a Tableau Cloud schedule response item. + /// + public interface ICloudScheduleType : IScheduleType + { } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/IExtractRefreshType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/IExtractRefreshType.cs new file mode 100644 index 00000000..5ced193d --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/IExtractRefreshType.cs @@ -0,0 +1,58 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses +{ + /// + /// Interface for an extract refresh response type. + /// + public interface IExtractRefreshType : IRestIdentifiable, IWithWorkbookReferenceType, IWithDataSourceReferenceType + { + /// + /// Gets or sets the priority for the response. + /// + public int Priority { get; set; } + + /// + /// Gets or sets the type for the response. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the consecutive failed count for the response. + /// + public int ConsecutiveFailedCount { get; set; } + } + + /// + /// Interface for an extract refresh response type. + /// + public interface IExtractRefreshType : IExtractRefreshType + where TWorkbook : IRestIdentifiable + where TDataSource : IRestIdentifiable + { + /// + /// Gets or sets the workbook for the response. + /// + new TWorkbook? Workbook { get; set; } + + /// + /// Gets or sets the data source for the response. + /// + new TDataSource? DataSource { get; set; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/IExtractRefreshTypeExtensions.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/IExtractRefreshTypeExtensions.cs new file mode 100644 index 00000000..7f06bfd3 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/IExtractRefreshTypeExtensions.cs @@ -0,0 +1,37 @@ +// +// 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 Tableau.Migration.Content.Schedules; + +namespace Tableau.Migration.Api.Rest.Models.Responses +{ + internal static class IExtractRefreshTypeExtensions + { + public static ExtractRefreshContentType GetContentType(this IExtractRefreshType extractRefreshType) + => extractRefreshType is IWithWorkbookReferenceType { Workbook: not null } + ? ExtractRefreshContentType.Workbook + : extractRefreshType is IWithDataSourceReferenceType { DataSource: not null } + ? ExtractRefreshContentType.DataSource + : ExtractRefreshContentType.Unknown; + + public static Guid GetContentId(this IExtractRefreshType extractRefreshType) + => (extractRefreshType as IWithWorkbookReferenceType)?.Workbook?.Id ?? + (extractRefreshType as IWithDataSourceReferenceType)?.DataSource?.Id ?? + throw new ArgumentException($"{nameof(extractRefreshType)} must contain a valid workbook or data source ID."); + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleFrequencyDetailsType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleFrequencyDetailsType.cs new file mode 100644 index 00000000..289ad9c8 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleFrequencyDetailsType.cs @@ -0,0 +1,40 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses +{ + /// + /// Interface for schedule frequency eetails. + /// + public interface IScheduleFrequencyDetailsType + { + /// + /// Gets the frequency start time. + /// + string? Start { get; set; } + + /// + /// Gets the frequency end time. + /// + string? End { get; set; } + + /// + /// Gets the frequency detail intervals. + /// + IScheduleIntervalType[] Intervals { get; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleIntervalType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleIntervalType.cs new file mode 100644 index 00000000..6693296d --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleIntervalType.cs @@ -0,0 +1,45 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses +{ + /// + /// Interface for a schedule interval response. + /// + public interface IScheduleIntervalType + { + /// + /// Gets the interval's hours. + /// + string? Hours { get; set; } + + /// + /// Gets the interval's minutes. + /// + string? Minutes { get; set; } + + /// + /// Gets the interval's month/day. + /// + string? MonthDay { get; set; } + + /// + /// Gets the interval's weekday. + /// + string? WeekDay { get; set; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleReferenceType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleReferenceType.cs new file mode 100644 index 00000000..da50f028 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/IScheduleReferenceType.cs @@ -0,0 +1,25 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses +{ + /// + /// Interface for a Tableau Server schedule reference response item. + /// + public interface IScheduleReferenceType : IRestIdentifiable + { } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ExtractRefreshTasksResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ExtractRefreshTasksResponse.cs new file mode 100644 index 00000000..290dda81 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ExtractRefreshTasksResponse.cs @@ -0,0 +1,177 @@ +// +// 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.Xml.Serialization; + +namespace Tableau.Migration.Api.Rest.Models.Responses.Server +{ + /// + /// Class representing a server extract refresh tasks response. + /// https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#tableau-server-request3 + /// + [XmlType(XmlTypeName)] + public class ExtractRefreshTasksResponse : TableauServerListResponse + { + /// + /// Gets or sets the extract refresh tasks for the response. + /// + [XmlArray("tasks")] + [XmlArrayItem("task")] + public override TaskType[] Items { get; set; } = Array.Empty(); + + /// + /// Class representing a response task item. + /// + public class TaskType + { + /// + /// Gets or sets the extract refresh for the response. + /// + [XmlElement("extractRefresh")] + public ExtractRefreshType? ExtractRefresh { get; set; } + + /// + /// Class representing a response extract refresh item. + /// + public class ExtractRefreshType : IServerExtractRefreshType + { + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + [XmlAttribute("priority")] + public int Priority { get; set; } + + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + [XmlAttribute("consecutiveFailedCount")] + public int ConsecutiveFailedCount { get; set; } + + /// + /// Gets or sets the schedule for the response. + /// + [XmlElement("schedule")] + public ScheduleType? Schedule { get; set; } + + /// + /// Gets or sets the data source for the response. + /// + [XmlElement("datasource")] + public DataSourceType? DataSource { get; set; } + + /// + /// Gets or sets the workbook for the response. + /// + [XmlElement("workbook")] + public WorkbookType? Workbook { get; set; } + + IRestIdentifiable? IWithWorkbookReferenceType.Workbook => Workbook; + IRestIdentifiable? IWithDataSourceReferenceType.DataSource => DataSource; + IScheduleReferenceType? IWithScheduleReferenceType.Schedule => Schedule; + + /// + /// Class representing a response schedule item. + /// + public class ScheduleType : IScheduleReferenceType + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + /// Gets or sets the name for the response. + /// + [XmlAttribute("name")] + public string? Name { get; set; } + + /// + /// Gets or sets the state for the response. + /// + [XmlAttribute("state")] + public string? State { get; set; } + + /// + /// Gets or sets the priority for the response. + /// + [XmlAttribute("priority")] + public int Priority { get; set; } + + /// + /// Gets or sets the created time for the response. + /// + [XmlAttribute("createdAt")] + public string? CreatedAt { get; set; } + + /// + /// Gets or sets the updated time for the response. + /// + [XmlAttribute("updatedAt")] + public string? UpdatedAt { get; set; } + + /// + /// Gets or sets the type for the response. + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + /// Gets or sets the frequency for the response. + /// + [XmlAttribute("frequency")] + public string? Frequency { get; set; } + + /// + /// Gets or sets the next run time for the response. + /// + [XmlAttribute("nextRunAt")] + public string? NextRunAt { get; set; } + } + + /// + /// Class representing a response data source item. + /// + public class DataSourceType : IRestIdentifiable + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + + /// + /// Class representing a response workbook item. + /// + public class WorkbookType : IRestIdentifiable + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + } + } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Server/IServerExtractRefreshType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/IServerExtractRefreshType.cs new file mode 100644 index 00000000..16eb43cd --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/IServerExtractRefreshType.cs @@ -0,0 +1,33 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses.Server +{ + /// + /// Interface for a Server extract refresh response type. + /// + public interface IServerExtractRefreshType : IExtractRefreshType, IWithScheduleReferenceType + { } + + /// + /// Interface for a Server extract refresh response type. + /// + public interface IServerExtractRefreshType : IServerExtractRefreshType, IExtractRefreshType + where TWorkbook : IRestIdentifiable + where TDataSource : IRestIdentifiable + { } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Server/IServerScheduleType.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/IServerScheduleType.cs new file mode 100644 index 00000000..8ab8e119 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/IServerScheduleType.cs @@ -0,0 +1,50 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Responses.Server +{ + /// + /// Interface for a Tableau Server schedule response item. + /// + public interface IServerScheduleType : IScheduleType, IRestIdentifiable, INamedContent + { + /// + /// Gets or sets the schedule's intervals. + /// + int Priority { get; set; } + + /// + /// Gets or sets the schedule's state. + /// + string? State { get; set; } + + /// + /// Gets or sets the schedule's type. + /// + string? Type { get; set; } + + /// + /// Gets or sets the schedule's creation time. + /// + string? CreatedAt { get; set; } + + /// + /// Gets or sets the schedule's updated time. + /// + string? UpdatedAt { get; set; } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ScheduleExtractRefreshTasksResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ScheduleExtractRefreshTasksResponse.cs new file mode 100644 index 00000000..ee650a71 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ScheduleExtractRefreshTasksResponse.cs @@ -0,0 +1,96 @@ +// +// 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.Xml.Serialization; + +namespace Tableau.Migration.Api.Rest.Models.Responses.Server +{ + /// + /// Class representing a schedule response. + /// + [XmlType(XmlTypeName)] + public class ScheduleExtractRefreshTasksResponse : PagedTableauServerResponse + { + /// + /// Gets or sets the items for the response. + /// + [XmlArray("extracts")] + [XmlArrayItem("extract")] + public override ExtractType[] Items { get; set; } = Array.Empty(); + + /// + /// Class representing the extract type in the response. + /// + public class ExtractType + { + /// + /// Gets or sets the id for the extract. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + /// Gets or sets the priority for the extract. + /// + [XmlAttribute("priority")] + public int Priority { get; set; } + + /// + /// Gets or sets the type for the extract. + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + /// Gets or sets the workbook for the extract. + /// + [XmlElement("workbook")] + public WorkbookType? Workbook { get; set; } + + /// + /// Gets or sets the datasource for the extract. + /// + [XmlElement("datasource")] + public DataSourceType? DataSource { get; set; } + + /// + /// Class representing the workbook in the response. + /// + public class WorkbookType + { + /// + /// Gets or sets the id for the workbook. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + + /// + /// Class representing the data source in the response. + /// + public class DataSourceType + { + /// + /// Gets or sets the id for the datasource. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + } + } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ScheduleResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ScheduleResponse.cs new file mode 100644 index 00000000..a9363468 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Responses/Server/ScheduleResponse.cs @@ -0,0 +1,166 @@ +// +// 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.Xml.Serialization; + +namespace Tableau.Migration.Api.Rest.Models.Responses.Server +{ + /// + /// Class representing a schedule response. + /// + [XmlType(XmlTypeName)] + public class ScheduleResponse : TableauServerResponse + { + /// + /// Gets or sets the schedule for the response. + /// + [XmlElement("schedule")] + public override ScheduleType? Item { get; set; } + + /// + /// Class representing a schedule response. + /// + public class ScheduleType : IServerScheduleType + { + /// + /// Gets or sets the ID for the response. + /// + [XmlAttribute("id")] + public Guid Id { get; set; } + + /// + /// Gets or sets the name for the response. + /// + [XmlAttribute("name")] + public string? Name { get; set; } + + /// + /// Gets or sets the state for the response. + /// + [XmlAttribute("state")] + public string? State { get; set; } + + /// + /// Gets or sets the priority for the response. + /// + [XmlAttribute("priority")] + public int Priority { get; set; } + + /// + /// Gets or sets the created timestamp for the response. + /// + [XmlAttribute("createdAt")] + public string? CreatedAt { get; set; } + + /// + /// Gets or sets the updated timestamp for the response. + /// + [XmlAttribute("updatedAt")] + public string? UpdatedAt { get; set; } + + /// + /// Gets or sets the type for the response. + /// + [XmlAttribute("type")] + public string? Type { get; set; } + + /// + /// Gets or sets the frequency for the response. + /// + [XmlAttribute("frequency")] + public string? Frequency { get; set; } + + /// + /// Gets or sets the next run at for the response. + /// + [XmlAttribute("nextRunAt")] + public string? NextRunAt { get; set; } + + /// + /// Gets or sets the execution order for the response. + /// + [XmlAttribute("executionOrder")] + public string? ExecutionOrder { get; set; } + + /// + /// Gets or sets the frequency details for the response. + /// + [XmlElement("frequencyDetails")] + public FrequencyDetailsType FrequencyDetails { get; set; } = new(); + + IScheduleFrequencyDetailsType? IScheduleType.FrequencyDetails => FrequencyDetails; + + /// + /// Class representing a REST API frequency details response. + /// + public class FrequencyDetailsType : IScheduleFrequencyDetailsType + { + /// + /// Gets or sets the start time for the response. + /// + [XmlAttribute("start")] + public string? Start { get; set; } + + /// + /// Gets or sets the end time for the response. + /// + [XmlAttribute("end")] + public string? End { get; set; } + + /// + /// Gets or sets the intervals for the response. + /// + [XmlArray("intervals")] + [XmlArrayItem("interval")] + public IntervalType[] Intervals { get; set; } = Array.Empty(); + + IScheduleIntervalType[] IScheduleFrequencyDetailsType.Intervals => Intervals; + + /// + /// Class representing a REST API interval response. + /// + public class IntervalType : IScheduleIntervalType + { + /// + /// Gets or sets the hours for the response. + /// + [XmlAttribute("hours")] + public string? Hours { get; set; } + + /// + /// Gets or sets the minutes for the response. + /// + [XmlAttribute("minutes")] + public string? Minutes { get; set; } + + /// + /// Gets or sets the week day for the response. + /// + [XmlAttribute("weekDay")] + public string? WeekDay { get; set; } + + /// + /// Gets or sets the week day for the response. + /// + [XmlAttribute("monthDay")] + public string? MonthDay { get; set; } + } + } + } + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/ScheduleExtractRefreshTasksResponsePager.cs b/src/Tableau.Migration/Api/Rest/Models/ScheduleExtractRefreshTasksResponsePager.cs new file mode 100644 index 00000000..b603b1ff --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/ScheduleExtractRefreshTasksResponsePager.cs @@ -0,0 +1,44 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Paging; + +namespace Tableau.Migration.Api.Rest.Models +{ + internal sealed class ScheduleExtractRefreshTasksResponsePager + : IndexedPagerBase, IPager + { + private readonly ISchedulesApiClient _apiClient; + private readonly Guid _scheduleId; + + public ScheduleExtractRefreshTasksResponsePager( + ISchedulesApiClient apiClient, + Guid scheduleId, + int pageSize) + : base(pageSize) + { + _apiClient = apiClient; + _scheduleId = scheduleId; + } + protected override async Task> GetPageAsync(int pageNumber, int pageSize, CancellationToken cancel) + => await _apiClient.GetScheduleExtractRefreshTasksAsync(_scheduleId, pageNumber, pageSize, cancel).ConfigureAwait(false); + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Types/ExtractRefreshType.cs b/src/Tableau.Migration/Api/Rest/Models/Types/ExtractRefreshType.cs new file mode 100644 index 00000000..a6e0ed89 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Types/ExtractRefreshType.cs @@ -0,0 +1,40 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Types +{ + /// + /// Enum of extract refresh content types. + /// + public class ExtractRefreshType : StringEnum + { + /// + /// Full refresh extract refresh type. + /// + public const string FullRefresh = "FullRefresh"; + + /// + /// Incremental refresh extract refresh type for Tableau Server. + /// + public const string ServerIncrementalRefresh = "IncrementalRefresh"; + + /// + /// Incremental refresh extract refresh type for Tableau Cloud. + /// + public const string CloudIncrementalRefresh = "IncrementalExtract"; + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Types/ScheduleFrequencies.cs b/src/Tableau.Migration/Api/Rest/Models/Types/ScheduleFrequencies.cs new file mode 100644 index 00000000..db191d5b --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Types/ScheduleFrequencies.cs @@ -0,0 +1,50 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Types +{ + /// + /// + /// Class containing schedule frequency constants. + /// + /// + /// See https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_extract_and_encryption.htm#create_cloud_extract_refresh_task for documentation. + /// + /// + public class ScheduleFrequencies : StringEnum + { + /// + /// Gets the name of the hourly schedule frequency. + /// + public const string Hourly = "Hourly"; + + /// + /// Gets the name of the daily schedule frequency. + /// + public const string Daily = "Daily"; + + /// + /// Gets the name of the weekly schedule frequency. + /// + public const string Weekly = "Weekly"; + + /// + /// Gets the name of the monthly schedule frequency. + /// + public const string Monthly = "Monthly"; + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Types/ScheduleTypes.cs b/src/Tableau.Migration/Api/Rest/Models/Types/ScheduleTypes.cs new file mode 100644 index 00000000..5a04b0f2 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Types/ScheduleTypes.cs @@ -0,0 +1,45 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Types +{ + /// + /// + /// Class containing schedule type constants. + /// + /// + /// See https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm#create_schedule for documentation. + /// + /// + public class ScheduleTypes : StringEnum + { + /// + /// Gets the name of the extract schedule type. + /// + public const string Extract = "Extract"; + + /// + /// Gets the name of the flow schedule type. + /// + public const string Flow = "Flow "; + + /// + /// Gets the name of the subscription schedule type. + /// + public const string Subscription = "Subscription"; + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/Types/WeekDays.cs b/src/Tableau.Migration/Api/Rest/Models/Types/WeekDays.cs new file mode 100644 index 00000000..69d709d9 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/Models/Types/WeekDays.cs @@ -0,0 +1,60 @@ +// +// 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. +// + +namespace Tableau.Migration.Api.Rest.Models.Types +{ + /// + /// Class containing week day constants. + /// + public class WeekDays : StringEnum + { + /// + /// Gets the name of the Sunday week day. + /// + public const string Sunday = "Sunday"; + + /// + /// Gets the name of the Monday week day. + /// + public const string Monday = "Monday"; + + /// + /// Gets the name of the Tuesday week day. + /// + public const string Tuesday = "Tuesday"; + + /// + /// Gets the name of the Wednesday week day. + /// + public const string Wednesday = "Wednesday"; + + /// + /// Gets the name of the Thursday week day. + /// + public const string Thursday = "Thursday"; + + /// + /// Gets the name of the Friday week day. + /// + public const string Friday = "Friday"; + + /// + /// Gets the name of the Saturday week day. + /// + public const string Saturday = "Saturday"; + } +} diff --git a/src/Tableau.Migration/Api/Rest/StringExtensions.cs b/src/Tableau.Migration/Api/Rest/StringExtensions.cs new file mode 100644 index 00000000..961a5d54 --- /dev/null +++ b/src/Tableau.Migration/Api/Rest/StringExtensions.cs @@ -0,0 +1,69 @@ +// +// 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; + +namespace Tableau.Migration.Api.Rest +{ + internal static class StringExtensions + { + public static TValue To(this string? value, Func convert) + where TValue : class + => convert(value); + + public static TValue? To(this string? value, Func convert) + where TValue : struct + => convert(value); + + public static DateTime? ToDateTimeOrNull(this string? value, bool isIso8601 = true) + => value.To( + v => + { + if (isIso8601) + { + return v.TryParseFromIso8601(false); + } + else + { + if (DateTime.TryParse(v, out var result)) + return result; + } + + return null; + }); + + public static TimeOnly? ToTimeOrNull(this string? value) + => value.To( + v => + { + if (TimeOnly.TryParse(v, out var result)) + return result; + + return null; + }); + + public static int? ToIntOrNull(this string? value) + => value.To( + v => + { + if (int.TryParse(v, out var result)) + return result; + + return null; + }); + } +} diff --git a/src/Tableau.Migration/Api/SchedulesApiClient.cs b/src/Tableau.Migration/Api/SchedulesApiClient.cs new file mode 100644 index 00000000..18c30a00 --- /dev/null +++ b/src/Tableau.Migration/Api/SchedulesApiClient.cs @@ -0,0 +1,126 @@ +// +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Api.Rest.Models; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Tableau.Migration.Config; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Content.Search; +using Tableau.Migration.Net.Rest; +using Tableau.Migration.Paging; +using Tableau.Migration.Resources; + +namespace Tableau.Migration.Api +{ + internal class SchedulesApiClient : ContentApiClientBase, ISchedulesApiClient + { + private readonly IContentCacheFactory _contentCacheFactory; + private readonly IConfigReader _configReader; + + public SchedulesApiClient( + IRestRequestBuilderFactory restRequestBuilderFactory, + IContentReferenceFinderFactory finderFactory, + IContentCacheFactory contentCacheFactory, + ILoggerFactory loggerFactory, + ISharedResourcesLocalizer sharedResourcesLocalizer, + IConfigReader configReader) + : base(restRequestBuilderFactory, finderFactory, loggerFactory, sharedResourcesLocalizer) + { + _contentCacheFactory = contentCacheFactory; + _configReader = configReader; + } + + #region - IReadApiClient Implementation - + + /// + public async Task> GetByIdAsync( + Guid contentId, + CancellationToken cancel) + { + var scheduleResult = await RestRequestBuilderFactory + .CreateUri($"/schedules/{contentId.ToUrlSegment()}") + .WithSiteId(null) + .ForGetRequest() + .SendAsync(cancel) + .ToResultAsync(ServerSchedule.FromServerResponse, SharedResourcesLocalizer) + .ConfigureAwait(false); + + if (!scheduleResult.Success) + { + return scheduleResult; + } + + var serverSchedule = scheduleResult.Value; + + var refreshTasks = await GetAllScheduleExtractRefreshTasksAsync(contentId, cancel).ConfigureAwait(false); + if (refreshTasks.Value != null) + { + serverSchedule.ExtractRefreshTasks.AddRange([.. refreshTasks.Value]); + } + + var cache = _contentCacheFactory.ForContentType(true); + cache.AddOrUpdate(serverSchedule); + + return scheduleResult; + } + + #endregion + + /// + public async Task> GetScheduleExtractRefreshTasksAsync( + Guid scheduleId, + int pageNumber, + int pageSize, + CancellationToken cancel) + { + var extractsResult = await RestRequestBuilderFactory + .CreateUri($"/schedules/{scheduleId.ToUrlSegment()}/extracts") + .WithPage(pageNumber, pageSize) + .ForGetRequest() + .SendAsync(cancel) + .ToPagedResultAsync( + (response) => + (new ScheduleExtractRefreshTasks(scheduleId, response)).ExtractRefreshTasks.ToImmutableList(), + SharedResourcesLocalizer) + .ConfigureAwait(false); + + return extractsResult; + } + + /// + public async Task>> GetAllScheduleExtractRefreshTasksAsync( + Guid scheduleId, + CancellationToken cancel) + { + var configReader = _configReader; + var pageSize = configReader.Get().BatchSize; + + IPager pager = new ScheduleExtractRefreshTasksResponsePager( + this, + scheduleId, + pageSize); + + var refreshTasks = await pager.GetAllPagesAsync(cancel).ConfigureAwait(false); + return refreshTasks; + } + } +} diff --git a/src/Tableau.Migration/Api/Search/ApiContentCache.cs b/src/Tableau.Migration/Api/Search/ApiContentCache.cs new file mode 100644 index 00000000..5c7049d6 --- /dev/null +++ b/src/Tableau.Migration/Api/Search/ApiContentCache.cs @@ -0,0 +1,105 @@ +// +// 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.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Config; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Api.Search +{ + /// + /// Thread-safe implementation. + /// + /// The content type. + public class ApiContentCache : BulkApiContentReferenceCache, IContentCache + where TContent : class, IContentReference + { + private readonly ConcurrentDictionary _innerCache; + + /// + /// Creates a new instance. + /// + /// An API client. + /// A config reader. + public ApiContentCache(ISitesApiClient? apiClient, IConfigReader configReader) + : this(apiClient, configReader, new()) + { } + + /// + /// Creates a new instance. + /// + /// An API client. + /// A config reader. + /// The inner content dictionary for testing. + internal ApiContentCache(ISitesApiClient? apiClient, IConfigReader configReader, ConcurrentDictionary innerCache) + : base(apiClient, configReader) + { + _innerCache = innerCache; + } + + /// + new public async Task ForLocationAsync(ContentLocation location, CancellationToken cancel) + => await ForReferenceAsync(() => base.ForLocationAsync(location, cancel)).ConfigureAwait(false); + + /// + new public async Task ForIdAsync(Guid id, CancellationToken cancel) + => await ForReferenceAsync(() => base.ForIdAsync(id, cancel)).ConfigureAwait(false); + + /// + public async Task ForReferenceAsync(IContentReference reference, CancellationToken cancel) + => await ForReferenceAsync(() => Task.FromResult(reference)).ConfigureAwait(false); + + /// + public TContent AddOrUpdate(TContent item) + => _innerCache.AddOrUpdate(item.ToStub(), (_) => item, (_, __) => item); + + /// + public IImmutableList AddOrUpdateRange(IEnumerable items) + { + var results = ImmutableArray.CreateBuilder(); + + foreach (var item in items) + results.Add(AddOrUpdate(item)); + + return results.ToImmutable(); + } + + private async Task ForReferenceAsync(Func> getReferenceAsync) + { + var reference = await getReferenceAsync().ConfigureAwait(false); + + if (reference is null) + return null; + + return _innerCache.TryGetValue(reference.ToStub(), out var content) ? content : null; + } + + /// + protected override void ItemLoaded(TContent item) + { + AddOrUpdate(item); + + base.ItemLoaded(item); + } + } +} diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Api/SchedulesRestApiSimulator.cs b/src/Tableau.Migration/Api/Simulation/Rest/Api/SchedulesRestApiSimulator.cs new file mode 100644 index 00000000..761d99bb --- /dev/null +++ b/src/Tableau.Migration/Api/Simulation/Rest/Api/SchedulesRestApiSimulator.cs @@ -0,0 +1,68 @@ +// +// 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.Linq; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Tableau.Migration.Api.Simulation.Rest.Net; +using Tableau.Migration.Api.Simulation.Rest.Net.Requests; +using Tableau.Migration.Net.Simulation; + +using static Tableau.Migration.Api.Simulation.Rest.Net.Requests.RestUrlPatterns; + +namespace Tableau.Migration.Api.Simulation.Rest.Api +{ + /// + /// Object that defines simulation of Tableau REST API schedule methods. + /// + public sealed class SchedulesRestApiSimulator + { + /// + /// Gets the simulated schedule query API method. + /// + public MethodSimulator GetServerSchedule { get; } + + /// + /// Gets the simulated schedules extract refresh tasks query API method. + /// + public MethodSimulator GetExtractRefreshTasks { get; } + + /// + /// Creates a new object. + /// + /// A response simulator to setup with REST API methods. + public SchedulesRestApiSimulator(TableauApiResponseSimulator simulator) + { + GetServerSchedule = simulator.SetupRestGetById( + EntityUrl("schedules"), d => d.Schedules); + GetExtractRefreshTasks = simulator.SetupRestPagedList( + SiteEntityUrl("schedules", "extracts"), (d, r) => + { + var scheduleId = r.GetIdAfterSegment("schedules"); + + if (scheduleId is null || !d.ScheduleExtracts.ContainsKey(scheduleId.Value)) + { + return Array.Empty().ToList(); + } + + return d.ScheduleExtractRefreshTasks + .Where(t => d.ScheduleExtracts[scheduleId.Value].Contains(t.Id)) + .ToList(); + }); + } + } +} diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Api/TasksRestApiSimulator.cs b/src/Tableau.Migration/Api/Simulation/Rest/Api/TasksRestApiSimulator.cs new file mode 100644 index 00000000..a8772131 --- /dev/null +++ b/src/Tableau.Migration/Api/Simulation/Rest/Api/TasksRestApiSimulator.cs @@ -0,0 +1,87 @@ +// +// 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.Linq; +using System.Net; +using System.Net.Http; +using Tableau.Migration.Api.Simulation.Rest.Net; +using Tableau.Migration.Api.Simulation.Rest.Net.Requests; +using Tableau.Migration.Api.Simulation.Rest.Net.Responses; +using Tableau.Migration.Net.Simulation; +using static Tableau.Migration.Api.Simulation.Rest.Net.Requests.RestUrlPatterns; +using CloudResponse = Tableau.Migration.Api.Rest.Models.Responses.Cloud; +using ServerResponse = Tableau.Migration.Api.Rest.Models.Responses.Server; + +namespace Tableau.Migration.Api.Simulation.Rest.Api +{ + /// + /// Object that defines simulation of Tableau REST API schedule methods. + /// + public sealed class TasksRestApiSimulator + { + /// + /// Gets the simulated schedule query API method. + /// + public MethodSimulator ListServerExtractRefreshTasks { get; } + + /// + /// Creates a new object. + /// + /// A response simulator to setup with REST API methods. + public TasksRestApiSimulator(TableauApiResponseSimulator simulator) + { + ListServerExtractRefreshTasks = + simulator.Data.IsTableauServer + ? simulator.SetupRestGetList( + SiteUrl("tasks/extractRefreshes"), + (d,r) => d.ServerExtractRefreshTasks, + null, + true) + : simulator.SetupRestGetList( + SiteUrl("tasks/extractRefreshes"), + (d, r) => d.CloudExtractRefreshTasks, + null, + true); + + simulator.SetupRestPost( + SiteUrl("tasks/extractRefreshes"), + new RestExtractRefreshTaskCreateResponseBuilder(simulator.Data, simulator.Serializer)); + + simulator.SetupRestDelete( + SiteEntityUrl("tasks/extractRefreshes"), + new RestDeleteResponseBuilder(simulator.Data, DeleteExtractRefresh, simulator.Serializer)); + } + + private HttpStatusCode DeleteExtractRefresh(TableauData data, HttpRequestMessage request) + { + var extractRefreshId = request.GetIdAfterSegment("extractRefreshes"); + + if (extractRefreshId is null) + { + return HttpStatusCode.BadRequest; + } + var extractRefreshTask = data.CloudExtractRefreshTasks.FirstOrDefault(cert => cert.ExtractRefresh!.Id == extractRefreshId); + + if (extractRefreshTask is not null) + { + data.CloudExtractRefreshTasks.Remove(extractRefreshTask); + } + + return HttpStatusCode.NoContent; + } + } +} diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestExtractRefreshTaskCreateResponseBuilder.cs b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestExtractRefreshTaskCreateResponseBuilder.cs new file mode 100644 index 00000000..c5d193ea --- /dev/null +++ b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestExtractRefreshTaskCreateResponseBuilder.cs @@ -0,0 +1,206 @@ +// +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Api.Rest.Models.Requests.Cloud; +using Tableau.Migration.Api.Rest.Models.Responses.Cloud; +using Tableau.Migration.Api.Simulation.Rest.Net.Requests; +using Tableau.Migration.Net; + +namespace Tableau.Migration.Api.Simulation.Rest.Net.Responses +{ + internal class RestExtractRefreshTaskCreateResponseBuilder : RestApiResponseBuilderBase + { + public RestExtractRefreshTaskCreateResponseBuilder(TableauData data, IHttpContentSerializer serializer) + : base(data, serializer, requiresAuthentication: true) + { } + + protected override ValueTask<(CreateExtractRefreshTaskResponse Response, HttpStatusCode ResponseCode)> BuildResponseAsync( + HttpRequestMessage request, + CancellationToken cancel) + { + if (request?.Content is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.BadRequest, + 0, + "Request or content cannot be null.", + ""); + } + + var createRequest = request.GetTableauServerRequest(); + var extractRefresh = createRequest?.ExtractRefresh; + var schedule = createRequest?.Schedule; + + if (extractRefresh is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.BadRequest, + 0, + $"The request property {nameof(CreateExtractRefreshTaskRequest.ExtractRefreshType)} must not be null", + ""); + } + + if (extractRefresh.Workbook is null && + extractRefresh.DataSource is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.BadRequest, + 0, + $"The request must contain a single reference to a Workbook or a Data Source", + ""); + } + else if (extractRefresh.Workbook is not null && + extractRefresh.DataSource is not null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.BadRequest, + 0, + $"The request must contain a single reference to a Workbook or a Data Source", + ""); + } + else if (extractRefresh.Workbook is not null && + Data.Workbooks.SingleOrDefault(w => w.Id == extractRefresh.Workbook.Id) is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.NotFound, + 0, + $"The workbook with ID {extractRefresh.Workbook.Id} could not be found.", + ""); + } + else if (extractRefresh.DataSource is not null && + Data.DataSources.SingleOrDefault(w => w.Id == extractRefresh.DataSource.Id) is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.NotFound, + 0, + $"The data source with ID {extractRefresh.DataSource.Id} could not be found.", + ""); + } + + if (schedule is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.BadRequest, + 0, + $"The request property {nameof(CreateExtractRefreshTaskRequest.ScheduleType)} must not be null", + ""); + } + + if (schedule.FrequencyDetails is null) + { + return BuildEmptyErrorResponseAsync( + HttpStatusCode.BadRequest, + 0, + $"The request property {nameof(CreateExtractRefreshTaskRequest.ScheduleType.FrequencyDetailsType)} must not be null", + ""); + } + + var extractRefreshTask = new ExtractRefreshTasksResponse.TaskType + { + ExtractRefresh = new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType + { + Id = Guid.NewGuid(), + Type = extractRefresh.Type, + Workbook = extractRefresh.Workbook is not null + ? new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType.WorkbookType + { + Id = extractRefresh.Workbook.Id + } + : null, + DataSource = extractRefresh.DataSource is not null + ? new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType.DataSourceType + { + Id = extractRefresh.DataSource.Id + } + : null, + Schedule = new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType.ScheduleType + { + Frequency = schedule.Frequency, + FrequencyDetails = new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType.ScheduleType.FrequencyDetailsType + { + Start = schedule.FrequencyDetails.Start, + End = schedule.FrequencyDetails.End, + Intervals = schedule + .FrequencyDetails + .Intervals + .Select(interval => new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType.ScheduleType.FrequencyDetailsType.IntervalType + { + Hours = interval.Hours, + Minutes = interval.Minutes, + MonthDay = interval.MonthDay, + WeekDay = interval.WeekDay + }) + .ToArray() + } + } + } + }; + + Data.CloudExtractRefreshTasks.Add(extractRefreshTask); + + return ValueTask.FromResult((new CreateExtractRefreshTaskResponse + { + Item = new CreateExtractRefreshTaskResponse.ExtractRefreshType + { + Id = extractRefreshTask.ExtractRefresh.Id, + Type = extractRefreshTask.ExtractRefresh.Type, + Workbook = extractRefreshTask.ExtractRefresh.Workbook is null + ? null + : new CreateExtractRefreshTaskResponse.ExtractRefreshType.WorkbookType + { + Id = extractRefreshTask.ExtractRefresh.Workbook.Id + }, + DataSource = extractRefreshTask.ExtractRefresh.DataSource is null + ? null + : new CreateExtractRefreshTaskResponse.ExtractRefreshType.DataSourceType + { + Id = extractRefreshTask.ExtractRefresh.DataSource.Id + } + }, + Schedule = new CreateExtractRefreshTaskResponse.ScheduleType + { + Frequency = extractRefreshTask.ExtractRefresh.Schedule.Frequency, + FrequencyDetails = new CreateExtractRefreshTaskResponse.ScheduleType.FrequencyDetailsType + { + Start = extractRefreshTask.ExtractRefresh.Schedule.FrequencyDetails.Start, + End = extractRefreshTask.ExtractRefresh.Schedule.FrequencyDetails.End, + Intervals = extractRefreshTask + .ExtractRefresh + .Schedule + .FrequencyDetails + .Intervals + .Select(interval => new CreateExtractRefreshTaskResponse.ScheduleType.FrequencyDetailsType.IntervalType + { + Hours = interval.Hours, + Minutes = interval.Minutes, + MonthDay = interval.MonthDay, + WeekDay = interval.WeekDay + }) + .ToArray() + } + } + }, + HttpStatusCode.Created)); + } + } +} diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestGetSitesResponseBuilder.cs b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestGetSitesResponseBuilder.cs new file mode 100644 index 00000000..d967b359 --- /dev/null +++ b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestGetSitesResponseBuilder.cs @@ -0,0 +1,52 @@ +// +// 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.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Net; + +namespace Tableau.Migration.Api.Simulation.Rest.Net.Responses +{ + internal class RestGetSitesResponseBuilder : EmptyRestResponseBuilder + { + public RestGetSitesResponseBuilder( + TableauData data, + IHttpContentSerializer serializer) + : base(data, serializer, false) + { + } + + protected override Task BuildResponseAsync(HttpRequestMessage request, CancellationToken cancel) + { + if (Data.IsTableauServer) + { + return Task.FromResult(new HttpResponseMessage()); + } + + return Task.FromResult( + BuildErrorResponse( + request, + new StaticRestErrorBuilder( + HttpStatusCode.Forbidden, + 69, + string.Empty, + string.Empty))); + } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Api/TasksApiClient.cs b/src/Tableau.Migration/Api/TasksApiClient.cs new file mode 100644 index 00000000..2b7f6e8b --- /dev/null +++ b/src/Tableau.Migration/Api/TasksApiClient.cs @@ -0,0 +1,254 @@ +// +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Api.Models.Cloud; +using Tableau.Migration.Api.Rest; +using Tableau.Migration.Api.Rest.Models.Requests.Cloud; +using Tableau.Migration.Api.Rest.Models.Responses; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Content.Search; +using Tableau.Migration.Net; +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; + +namespace Tableau.Migration.Api +{ + /// + /// Interface for API client tasks operations. + /// + internal class TasksApiClient : ContentApiClientBase, ITasksApiClient + { + private readonly IServerSessionProvider _sessionProvider; + private readonly IContentCacheFactory _contentCacheFactory; + private readonly IHttpContentSerializer _serializer; + + public TasksApiClient( + IRestRequestBuilderFactory restRequestBuilderFactory, + IContentReferenceFinderFactory finderFactory, + IContentCacheFactory contentCacheFactory, + IServerSessionProvider sessionProvider, + ILoggerFactory loggerFactory, + ISharedResourcesLocalizer sharedResourcesLocalizer, + IHttpContentSerializer serializer) + : base( + restRequestBuilderFactory, + finderFactory, + loggerFactory, + sharedResourcesLocalizer, + RestUrlPrefixes.Tasks) + { + _sessionProvider = sessionProvider; + _contentCacheFactory = contentCacheFactory; + _serializer = serializer; + } + + #region - ITasksApiClient - + + /// + public IServerTasksApiClient ForServer() + => ExecuteForInstanceType( + TableauInstanceType.Server, + _sessionProvider.InstanceType, + () => this); + + /// + public ICloudTasksApiClient ForCloud() + => ExecuteForInstanceType( + TableauInstanceType.Cloud, + _sessionProvider.InstanceType, + () => this); + + #endregion + + #region - ICloudTasksApiClient - + + /// + public async Task DeleteExtractRefreshTaskAsync( + Guid extractRefreshTaskId, + CancellationToken cancel) + { + var result = await RestRequestBuilderFactory + .CreateUri($"{UrlPrefix}/extractRefreshes/{extractRefreshTaskId.ToUrlSegment()}") + .ForDeleteRequest() + .SendAsync(cancel) + .ToResultAsync(_serializer, SharedResourcesLocalizer, cancel) + .ConfigureAwait(false); + + return result; + } + + /// + async Task>> ICloudTasksApiClient.GetAllExtractRefreshTasksAsync( + CancellationToken cancel) + => await GetAllExtractRefreshTasksAsync( + (r, c) => CloudExtractRefreshTask.CreateManyAsync( + r, + ContentFinderFactory, + c), + cancel) + .ConfigureAwait(false); + + /// + async Task> ICloudTasksApiClient.CreateExtractRefreshTaskAsync( + ICreateExtractRefreshTaskOptions options, + CancellationToken cancel) + { + var result = await RestRequestBuilderFactory + .CreateUri($"{UrlPrefix}/extractRefreshes") + .ForPostRequest() + .WithXmlContent(new CreateExtractRefreshTaskRequest(options)) + .SendAsync(cancel) + .ToResultAsync((r, c) => + CloudExtractRefreshTask.CreateAsync( + r.Item, + r.Schedule, + ContentFinderFactory, + c), + SharedResourcesLocalizer, + cancel) + .ConfigureAwait(false); + + return result; + } + + /// + public async Task> PublishAsync( + ICloudExtractRefreshTask item, + CancellationToken cancel) + { + var options = new CreateExtractRefreshTaskOptions( + item.Type, + item.ContentType, + item.Content.Id, + item.Schedule); + + return await ForCloud() + .CreateExtractRefreshTaskAsync(options, cancel) + .ConfigureAwait(false); + } + + #endregion + + #region - IServerTasksApiClient - + + /// + async Task>> IServerTasksApiClient.GetAllExtractRefreshTasksAsync(CancellationToken cancel) + => await GetAllExtractRefreshTasksAsync( + (r, c) => ServerExtractRefreshTask.CreateManyAsync( + r, + ContentFinderFactory, + _contentCacheFactory, + c), + cancel) + .ConfigureAwait(false); + + /// + public Task> PullAsync( + IServerExtractRefreshTask contentItem, + CancellationToken cancel) + { + ICloudExtractRefreshTask cloudExtractRefreshTask = new CloudExtractRefreshTask( + contentItem.Id, + contentItem.Type, + contentItem.ContentType, + contentItem.Content, + new CloudSchedule(contentItem.Schedule.Frequency, contentItem.Schedule.FrequencyDetails)); + + return Task.FromResult(new ResultBuilder() + .Build(cloudExtractRefreshTask)); + } + + #endregion + + #region - IPagedListApiClient Implementation - + + /// + public IPager GetPager( + int pageSize) + => new ApiListPager( + this, + pageSize); + + #endregion + + #region - IApiPageAccessor Implementation - + + /// + public async Task> GetPageAsync( + int pageNumber, + int pageSize, + CancellationToken cancel) + { + if (pageNumber != 1) + { + return PagedResult.Succeeded( + ImmutableArray.Empty, + pageNumber, + pageSize, + 0, + true); + } + + var loadResult = await ForServer() + .GetAllExtractRefreshTasksAsync(cancel) + .ConfigureAwait(false); + + if (!loadResult.Success) + { + return PagedResult.Failed( + loadResult.Errors); + } + + return PagedResult.Succeeded( + loadResult.Value!, + pageNumber, + loadResult.Value.Count, + loadResult.Value.Count, + true); + } + + #endregion + + private async Task>> GetAllExtractRefreshTasksAsync( + Func>> responseItemFactory, + CancellationToken cancel) + where TResponse : TableauServerResponse + where TExtractRefreshTask: IExtractRefreshTask + where TSchedule : ISchedule + { + return await RestRequestBuilderFactory + .CreateUri($"{UrlPrefix}/extractRefreshes") + .ForGetRequest() + .SendAsync(cancel) + .ToResultAsync( + (r, c) => responseItemFactory(r, c), + SharedResourcesLocalizer, + cancel) + .ConfigureAwait(false); + } + } +} diff --git a/src/Tableau.Migration/Content/IContentReferenceExtensions.cs b/src/Tableau.Migration/Content/IContentReferenceExtensions.cs new file mode 100644 index 00000000..276c4599 --- /dev/null +++ b/src/Tableau.Migration/Content/IContentReferenceExtensions.cs @@ -0,0 +1,25 @@ +// +// 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. +// + +namespace Tableau.Migration.Content +{ + internal static class IContentReferenceExtensions + { + public static ContentReferenceStub ToStub(this IContentReference contentReference) + => new(contentReference); + } +} diff --git a/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs new file mode 100644 index 00000000..79c944e7 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Cloud/CloudExtractRefreshTask.cs @@ -0,0 +1,87 @@ +// +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Api.Rest.Models.Responses; +using Tableau.Migration.Api.Rest.Models.Responses.Cloud; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Content.Schedules.Cloud +{ + internal sealed class CloudExtractRefreshTask : + ExtractRefreshTaskBase, ICloudExtractRefreshTask + { + internal CloudExtractRefreshTask( + Guid extractRefreshId, + string type, + ExtractRefreshContentType contentType, + IContentReference contentReference, + ICloudSchedule schedule) + : base( + extractRefreshId, + type, + contentType, + contentReference, + schedule) + { } + + public static async Task> CreateManyAsync( + ExtractRefreshTasksResponse? response, + IContentReferenceFinderFactory finderFactory, + CancellationToken cancel) + => await CreateManyAsync( + response, + response => response.Items.ExceptNulls(i => i.ExtractRefresh), + (r, c, cnl) => Task.FromResult(Create(r, r.Schedule, c)), + finderFactory, + 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, + IContentReference content) + { + Guard.AgainstNull(response, nameof(response)); + + return new CloudExtractRefreshTask( + response.Id, + response.Type!, + response.GetContentType(), + content, + new CloudSchedule( + Guard.AgainstNull( + schedule, + () => schedule))); + } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/Cloud/CloudSchedule.cs b/src/Tableau.Migration/Content/Schedules/Cloud/CloudSchedule.cs new file mode 100644 index 00000000..13633e2d --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Cloud/CloudSchedule.cs @@ -0,0 +1,32 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses.Cloud; + +namespace Tableau.Migration.Content.Schedules.Cloud +{ + internal sealed class CloudSchedule : ScheduleBase, ICloudSchedule + { + public CloudSchedule(ICloudScheduleType response) + : base(response) + { } + + public CloudSchedule(string frequency, IFrequencyDetails frequencyDetails) + : base(frequency, frequencyDetails, null) + { } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Cloud/ICloudExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Cloud/ICloudExtractRefreshTask.cs new file mode 100644 index 00000000..4af67400 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Cloud/ICloudExtractRefreshTask.cs @@ -0,0 +1,25 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules.Cloud +{ + /// + /// Interface for a cloud extract refresh task content item. + /// + public interface ICloudExtractRefreshTask : IExtractRefreshTask + { } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Cloud/ICloudSchedule.cs b/src/Tableau.Migration/Content/Schedules/Cloud/ICloudSchedule.cs new file mode 100644 index 00000000..6289f007 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Cloud/ICloudSchedule.cs @@ -0,0 +1,25 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules.Cloud +{ + /// + /// Interface for a Tableau Cloud schedule. + /// + public interface ICloudSchedule : ISchedule + { } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs b/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs new file mode 100644 index 00000000..b642f95a --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs @@ -0,0 +1,23 @@ +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Enum of extract refresh content types. + /// + public enum ExtractRefreshContentType + { + /// + /// Unknown content type. + /// + Unknown, + + /// + /// Workbook content type. + /// + Workbook, + + /// + /// Data source content type. + /// + DataSource + } +} diff --git a/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs b/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs new file mode 100644 index 00000000..b57e74d3 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/ExtractRefreshTaskBase.cs @@ -0,0 +1,113 @@ +// +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Api; +using Tableau.Migration.Api.Rest.Models; +using Tableau.Migration.Api.Rest.Models.Responses; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Content.Schedules +{ + internal abstract class ExtractRefreshTaskBase : + ContentBase, IExtractRefreshTask + where TSchedule : ISchedule + { + public string Type { get; set; } + public ExtractRefreshContentType ContentType { get; set; } + public IContentReference Content { get; set; } + public TSchedule Schedule { get; } + + protected ExtractRefreshTaskBase( + Guid extractRefreshId, + string type, + ExtractRefreshContentType contentType, + IContentReference content, + TSchedule schedule) + : base( + new ContentReferenceStub( + extractRefreshId, + string.Empty, + new (extractRefreshId.ToString()))) + { + Type = type; + ContentType = contentType; + Content = content; + 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, + Func> responseItemFactory, + Func> modelFactory, + IContentReferenceFinderFactory finderFactory, + 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(); + + foreach (var item in items) + { + tasks.Add( + await CreateAsync( + item, + finderFactory, + modelFactory, + cancel) + .ConfigureAwait(false)); + } + + return tasks.ToImmutable(); + } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/FrequencyDetails.cs b/src/Tableau.Migration/Content/Schedules/FrequencyDetails.cs new file mode 100644 index 00000000..9630f758 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/FrequencyDetails.cs @@ -0,0 +1,49 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using Tableau.Migration.Api.Rest; +using Tableau.Migration.Api.Rest.Models.Responses; + +namespace Tableau.Migration.Content.Schedules +{ + internal sealed class FrequencyDetails : IFrequencyDetails + { + public TimeOnly? StartAt { get; set; } + public TimeOnly? EndAt { get; set; } + + public IList Intervals { get; set; } + + public FrequencyDetails(IScheduleFrequencyDetailsType response) + : this(response.Start.ToTimeOrNull(), response.End.ToTimeOrNull(), response.Intervals.Select(i => new Interval(i) as IInterval)) + { } + + public FrequencyDetails(TimeOnly? startAt, TimeOnly? endAt, IEnumerable intervals) + { + StartAt = startAt; + EndAt = endAt; + Intervals = intervals.ToImmutableArray(); + } + + public FrequencyDetails(TimeOnly? startAt, TimeOnly? endAt, params IInterval[] intervals) + : this(startAt, endAt, (IEnumerable)intervals) + { } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/IExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/IExtractRefreshTask.cs new file mode 100644 index 00000000..1c7e6584 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IExtractRefreshTask.cs @@ -0,0 +1,41 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Interface for an extract refresh task content item. + /// + public interface IExtractRefreshTask : IWithSchedule + where TSchedule : ISchedule + { + /// + /// Gets the extract refresh type. + /// + string Type { get; set; } + + /// + /// Gets the extract refresh task's content type. + /// + ExtractRefreshContentType ContentType { get; set; } + + /// + /// Gets the extract refresh task's content. + /// + IContentReference Content { get; set; } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/IFrequencyDetails.cs b/src/Tableau.Migration/Content/Schedules/IFrequencyDetails.cs new file mode 100644 index 00000000..5aa30e04 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IFrequencyDetails.cs @@ -0,0 +1,43 @@ +// +// 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; + +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Interface for a schedule's frequency details. + /// + public interface IFrequencyDetails + { + /// + /// Gets the schedule's start time. + /// + TimeOnly? StartAt { get; set; } + + /// + /// Gets the schedule's end time. + /// + TimeOnly? EndAt { get; set; } + + /// + /// Gets the schedule's intervals. + /// + IList Intervals { get; set; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/IFrequencyExtensions.cs b/src/Tableau.Migration/Content/Schedules/IFrequencyExtensions.cs new file mode 100644 index 00000000..1b26bee5 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IFrequencyExtensions.cs @@ -0,0 +1,75 @@ +// +// 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.Collections.Generic; +using System.Linq; +using Tableau.Migration.Api.Rest.Models.Types; + +namespace Tableau.Migration.Content.Schedules +{ + internal static class IFrequencyExtensions + { + private const int ALLOWED_SELECTED_WEEKDAYS_WEEKLY = 1; + private const int ALLOWED_CLOUD_MINUTES = 60; + + public static IList ToCloudCompatible( + this string? frequency, + IList intervals) + { + if (frequency.IsCloudCompatible(intervals)) + { + return intervals; + } + + if (ScheduleFrequencies.IsAMatch(frequency, ScheduleFrequencies.Hourly)) + { + return intervals + .Select(i => i.Minutes is not null && i.Minutes.Value != ALLOWED_CLOUD_MINUTES + ? Interval.WithHours(1) + : i) + .ToList(); + } + + return intervals + .Where(i => i.WeekDay is not null) + .Take(ALLOWED_SELECTED_WEEKDAYS_WEEKLY) + .ToList(); + } + + public static bool IsCloudCompatible( + this string? frequency, + IList intervals) + { + if (ScheduleFrequencies.IsAMatch(frequency, ScheduleFrequencies.Hourly)) + { + return intervals.All(i => + i.Minutes is null || + ( + i.Minutes is not null && + i.Minutes.Value == ALLOWED_CLOUD_MINUTES + )); + } + + if (ScheduleFrequencies.IsAMatch(frequency, ScheduleFrequencies.Weekly)) + { + return intervals.Where(i => i.WeekDay is not null).Count() == ALLOWED_SELECTED_WEEKDAYS_WEEKLY; + } + + return true; + } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/IInterval.cs b/src/Tableau.Migration/Content/Schedules/IInterval.cs new file mode 100644 index 00000000..52ad6f87 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IInterval.cs @@ -0,0 +1,45 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Interface for a schedule interval. + /// + public interface IInterval + { + /// + /// Gets the interval hour value. + /// + int? Hours { get; } + + /// + /// Gets the interval minute value. + /// + int? Minutes { get; } + + /// + /// Gets the interval day of month value. + /// + string? MonthDay { get; } + + /// + /// Gets the interval day of week value. + /// + string? WeekDay { get; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/ILoggerExtensions.cs b/src/Tableau.Migration/Content/Schedules/ILoggerExtensions.cs new file mode 100644 index 00000000..08c798c3 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/ILoggerExtensions.cs @@ -0,0 +1,49 @@ +// +// 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 Microsoft.Extensions.Logging; + +namespace Tableau.Migration.Content.Schedules +{ + internal static class ILoggerExtensions + { + public static bool LogIntervalsChanges( + this ILogger logger, + string localizedMessage, + Guid refreshTaskId, + IList oldIntervals, + IList newIntervals, + IEqualityComparer>? comparer = null) + { + comparer ??= ScheduleComparers.Intervals; + + if (comparer.Equals(oldIntervals, newIntervals)) + { + return false; + } + + logger.LogWarning( + localizedMessage, + refreshTaskId, + string.Join($",{Environment.NewLine}", oldIntervals), + string.Join($",{Environment.NewLine}", newIntervals)); + return true; + } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/ISchedule.cs b/src/Tableau.Migration/Content/Schedules/ISchedule.cs new file mode 100644 index 00000000..087787bc --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/ISchedule.cs @@ -0,0 +1,40 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Interface for an API client schedule model. + /// + public interface ISchedule + { + /// + /// Gets the schedule's frequency. + /// + string Frequency { get; set; } + + /// + /// Gets the schedule's frequency details. + /// + IFrequencyDetails FrequencyDetails { get; } + + /// + /// Gets the schedule's next run time. + /// + string? NextRunAt { get; } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/IWithSchedule.cs b/src/Tableau.Migration/Content/Schedules/IWithSchedule.cs new file mode 100644 index 00000000..7442503b --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IWithSchedule.cs @@ -0,0 +1,31 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Interface to be inherited by content items with a schedule. + /// + public interface IWithSchedule : IContentReference + where TSchedule : ISchedule + { + /// + /// Gets the content item's schedule. + /// + TSchedule Schedule { get; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Interval.cs b/src/Tableau.Migration/Content/Schedules/Interval.cs new file mode 100644 index 00000000..8a0fb90b --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Interval.cs @@ -0,0 +1,62 @@ +// +// 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 Tableau.Migration.Api.Rest; +using Tableau.Migration.Api.Rest.Models.Responses; + +namespace Tableau.Migration.Content.Schedules +{ + internal readonly record struct Interval : IInterval + { + public int? Hours { get; } + public int? Minutes { get; } + public string? WeekDay { get; } + public string? MonthDay { get; } + + public Interval(IScheduleIntervalType response) + : this( + response.Hours.ToIntOrNull(), + response.Minutes.ToIntOrNull(), + response.WeekDay, + response.MonthDay) + { } + + internal Interval( + int? hours = null, + int? minutes = null, + string? weekDay = null, + string? monthDay = null) + { + Hours = hours; + Minutes = minutes; + WeekDay = NormalizeValue(weekDay); + MonthDay = NormalizeValue(monthDay); + } + + #region - Factory Methods - + + public static IInterval WithHours(int hours) => new Interval(hours: hours); + public static IInterval WithMinutes(int minutes) => new Interval(minutes: minutes); + public static IInterval WithWeekday(string weekDay) => new Interval(weekDay: weekDay); + public static IInterval WithMonthDay(string monthDay) => new Interval(monthDay: monthDay); + public static IInterval WithWeekdayMonthDay(string weekDay, string monthDay) => new Interval(weekDay: weekDay, monthDay: monthDay); + + #endregion + + private static string? NormalizeValue(string? value) => string.IsNullOrWhiteSpace(value) ? null : value; + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/IntervalComparer.cs b/src/Tableau.Migration/Content/Schedules/IntervalComparer.cs new file mode 100644 index 00000000..62d9adef --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IntervalComparer.cs @@ -0,0 +1,67 @@ +// +// 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.Collections.Generic; + +namespace Tableau.Migration.Content.Schedules +{ + internal class IntervalComparer + : ComparerBase + { + private static readonly IList AllWeekDays = [.. IntervalValues.WeekDaysValues]; + private static readonly IList AllMonthDays = [.. IntervalValues.MonthDaysValues]; + + protected override int CompareItems( + IInterval x, + IInterval y) + { + if (x is null && y is null) + return 0; + + if (x is null) + return -1; + + if (y is null) + return 1; + + var diff = (x.Minutes ?? 0) - (y.Minutes ?? 0); + + if (diff != 0) + { + return diff; + } + + diff = (x.Hours ?? 0) - (y.Hours ?? 0); + + if (diff != 0) + { + return diff; + } + + diff = AllWeekDays.IndexOf(x.WeekDay ?? string.Empty) - AllWeekDays.IndexOf(y.WeekDay ?? string.Empty); + + if (diff != 0) + { + return diff; + } + + diff = AllMonthDays.IndexOf(x.WeekDay ?? string.Empty) - AllMonthDays.IndexOf(y.WeekDay ?? string.Empty); + + return diff; + } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/IntervalValues.cs b/src/Tableau.Migration/Content/Schedules/IntervalValues.cs new file mode 100644 index 00000000..a3a87437 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/IntervalValues.cs @@ -0,0 +1,56 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using Tableau.Migration.Api.Rest.Models.Types; + +namespace Tableau.Migration.Content.Schedules +{ + internal static class IntervalValues + { + public static readonly IImmutableList HoursValues = new int?[] { 1, 2, 4, 6, 8, 12, 24 } + .Prepend(null) + .ToImmutableList(); + + public static readonly IImmutableList MinutesValues = new int?[] { 15, 30, 60 } + .Prepend(null) + .ToImmutableList(); + + public static readonly IImmutableList WeekDaysValues = WeekDays.GetAll() + .Prepend(null) + .ToImmutableList(); + + public const string First = "First"; + public const string Second = "Second"; + public const string Third = "Third"; + public const string Fourth = "Fourth"; + public const string Fifth = "Fifth"; + public const string LastDay = "LastDay"; + + public static readonly IImmutableList MonthDaysValues = Enumerable.Range(1, 31) + .Select(d => d.ToString()) + .Prepend(null) + .Append(First) + .Append(Second) + .Append(Third) + .Append(Fourth) + .Append(Fifth) + .Append(LastDay) + .ToImmutableList(); + } +} diff --git a/src/Tableau.Migration/Content/Schedules/ScheduleBase.cs b/src/Tableau.Migration/Content/Schedules/ScheduleBase.cs new file mode 100644 index 00000000..ed2af3d3 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/ScheduleBase.cs @@ -0,0 +1,58 @@ +// +// 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 Tableau.Migration.Api.Rest.Models; + +namespace Tableau.Migration.Content.Schedules +{ + internal abstract class ScheduleBase : ISchedule + { + /// + public string Frequency { get; set; } + + /// + public IFrequencyDetails FrequencyDetails { get; } + + /// + public string? NextRunAt { get; } + + /// + /// Creates a new instance. + /// + /// The REST API schedule response. + public ScheduleBase(IScheduleType response) + { + Frequency = Guard.AgainstNullEmptyOrWhiteSpace(response.Frequency, () => response.Frequency); + + NextRunAt = response.NextRunAt; + + var frequencyDetails = Guard.AgainstNull(response.FrequencyDetails, () => response.FrequencyDetails); + + FrequencyDetails = new FrequencyDetails(frequencyDetails); + } + + /// + /// Creates a new instance. + /// + public ScheduleBase(string frequency, IFrequencyDetails frequencyDetails, string? nextRunAt) + { + Frequency = Guard.AgainstNullEmptyOrWhiteSpace(frequency, nameof(frequency)); + NextRunAt = nextRunAt; + FrequencyDetails = frequencyDetails; + } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/ScheduleComparers.cs b/src/Tableau.Migration/Content/Schedules/ScheduleComparers.cs new file mode 100644 index 00000000..5d25f0c6 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/ScheduleComparers.cs @@ -0,0 +1,24 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules +{ + internal class ScheduleComparers + { + public static readonly IntervalComparer Intervals = new(); + } +} diff --git a/src/Tableau.Migration/Content/Schedules/Server/IScheduleExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Server/IScheduleExtractRefreshTask.cs new file mode 100644 index 00000000..7595378b --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/IScheduleExtractRefreshTask.cs @@ -0,0 +1,51 @@ +// +// 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; + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// The interface for an extract. + /// + public interface IScheduleExtractRefreshTask + { + /// + /// The extract ID. + /// + Guid Id { get; } + + /// + /// The extract priority. + /// + int Priority { get; set; } + /// + /// The extract type. This is either full or incremental. + /// + string Type { get; set; } + + /// + /// The ID of the workbook this extract is linked to. + /// + Guid? WorkbookId { get; set; } + + /// + /// The ID of the Data Source this extract is linked to. + /// + Guid? DatasourceId { get; set; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Server/IScheduleExtractRefreshTasks.cs b/src/Tableau.Migration/Content/Schedules/Server/IScheduleExtractRefreshTasks.cs new file mode 100644 index 00000000..87f97724 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/IScheduleExtractRefreshTasks.cs @@ -0,0 +1,32 @@ +// +// 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.Collections.Generic; + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// Interface for a collection of extracts. + /// + public interface IScheduleExtractRefreshTasks + { + /// + /// List of extracts for the schedule. + /// + List ExtractRefreshTasks { get; set; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Server/IServerExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Server/IServerExtractRefreshTask.cs new file mode 100644 index 00000000..94d763ee --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/IServerExtractRefreshTask.cs @@ -0,0 +1,25 @@ +// +// 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. +// + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// Interface for a server extract refresh task content item. + /// + public interface IServerExtractRefreshTask : IExtractRefreshTask + { } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Server/IServerSchedule.cs b/src/Tableau.Migration/Content/Schedules/Server/IServerSchedule.cs new file mode 100644 index 00000000..7d957151 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/IServerSchedule.cs @@ -0,0 +1,47 @@ +// +// 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 Tableau.Migration.Api.Rest; + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// Interface for server extract refresh schedule. + /// + public interface IServerSchedule : ISchedule, INamedContent, IContentReference, IScheduleExtractRefreshTasks + { + /// + /// Gets the schedule's type. + /// + string Type { get; } + + /// + /// Gets the schedule's state. + /// + string? State { get; } + + /// + /// Gets the schedule's created timestamp. + /// + string? CreatedAt { get; } + + /// + /// Gets the schedule's updated timestamp. + /// + string? UpdatedAt { get; } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration/Content/Schedules/Server/ScheduleExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Server/ScheduleExtractRefreshTask.cs new file mode 100644 index 00000000..1f830597 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/ScheduleExtractRefreshTask.cs @@ -0,0 +1,58 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses.Server; + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// The class for an extract. + /// + public class ScheduleExtractRefreshTask : IScheduleExtractRefreshTask + { + /// + /// Constructor to build from a . + /// + /// + public ScheduleExtractRefreshTask(ScheduleExtractRefreshTasksResponse.ExtractType response) + { + Guard.AgainstNull(response, nameof(response)); + Id = Guard.AgainstDefaultValue(response.Id, () => response.Id); + Priority = response.Priority; + Type = Guard.AgainstNull(response.Type, () => response.Type); + WorkbookId = response.Workbook?.Id; + DatasourceId = response.DataSource?.Id; + } + + /// + public Guid Id { get; } + + /// + public int Priority { get; set; } + + /// + public string Type { get; set; } + + /// + public Guid? WorkbookId { get; set; } + + /// + public Guid? DatasourceId { get; set; } + + } +} diff --git a/src/Tableau.Migration/Content/Schedules/Server/ScheduleExtractRefreshTasks.cs b/src/Tableau.Migration/Content/Schedules/Server/ScheduleExtractRefreshTasks.cs new file mode 100644 index 00000000..510b54bb --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/ScheduleExtractRefreshTasks.cs @@ -0,0 +1,49 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses.Server; + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// Class for a collection of extracts. + /// + public class ScheduleExtractRefreshTasks : IScheduleExtractRefreshTasks + { + /// + /// Constructor to build from . + /// + /// The schedule ID. + /// + public ScheduleExtractRefreshTasks(Guid scheduleId, ScheduleExtractRefreshTasksResponse response) + { + Id = scheduleId; + foreach (var item in response.Items) + { + ExtractRefreshTasks.Add(new ScheduleExtractRefreshTask(item)); + } + } + + /// + public List ExtractRefreshTasks { get; set; } = []; + + /// + public Guid Id { get; } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs b/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs new file mode 100644 index 00000000..4ae212b5 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/ServerExtractRefreshTask.cs @@ -0,0 +1,86 @@ +// +// 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.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Api.Rest.Models.Responses; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Content.Schedules.Server +{ + /// + /// Class for Server extract refresh task. + /// + internal sealed class ServerExtractRefreshTask : + ExtractRefreshTaskBase, IServerExtractRefreshTask + { + internal ServerExtractRefreshTask( + Guid extractRefreshId, + string type, + ExtractRefreshContentType contentType, + IContentReference content, + IServerSchedule schedule) + : base( + extractRefreshId, + type, + contentType, + content, + schedule) + { } + + public static async Task> CreateManyAsync( + ExtractRefreshTasksResponse? response, + IContentReferenceFinderFactory finderFactory, + IContentCacheFactory contentCacheFactory, + 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, + cancel) + .ConfigureAwait(false); + + private static async Task CreateAsync( + 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); + + Guard.AgainstNull(schedule, nameof(schedule)); + + var taskFromCache = schedule.ExtractRefreshTasks.Where(tsk => tsk.Id == response.Id).FirstOrDefault(); + + return new ServerExtractRefreshTask( + response.Id, + taskFromCache== null ? string.Empty: taskFromCache.Type, + response.GetContentType(), + content, + schedule); + } + } +} diff --git a/src/Tableau.Migration/Content/Schedules/Server/ServerSchedule.cs b/src/Tableau.Migration/Content/Schedules/Server/ServerSchedule.cs new file mode 100644 index 00000000..b2d36269 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/Server/ServerSchedule.cs @@ -0,0 +1,59 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses.Server; + +namespace Tableau.Migration.Content.Schedules.Server +{ + internal sealed class ServerSchedule : ScheduleBase, IServerSchedule + { + public Guid Id { get; } + public string Name { get; } + public string ContentUrl { get; } = String.Empty; + public ContentLocation Location { get; } + + public string Type { get; } + public string State { get; } + public string? CreatedAt { get; } + public string? UpdatedAt { get; } + public List ExtractRefreshTasks { get; set; } = []; + + public ServerSchedule(ScheduleResponse response) + : this(Guard.AgainstNull(response.Item, () => response.Item)) + { } + + public ServerSchedule(IServerScheduleType response) + : base(response) + { + Id = Guard.AgainstDefaultValue(response.Id, () => response.Id); + Name = Guard.AgainstNullEmptyOrWhiteSpace(response.Name, () => response.Name); + Location = new(Name); + + Type = Guard.AgainstNullEmptyOrWhiteSpace(response.Type, () => response.Type); + State = Guard.AgainstNullEmptyOrWhiteSpace(response.State, () => response.State); + CreatedAt = response.CreatedAt; + UpdatedAt = response.UpdatedAt; + } + + public bool Equals(IContentReference? other) => (other as ServerSchedule)?.Id == Id; + + public static IServerSchedule FromServerResponse(ScheduleResponse response) + => new ServerSchedule(response); + } +} diff --git a/src/Tableau.Migration/Content/Schedules/VolatileCache.cs b/src/Tableau.Migration/Content/Schedules/VolatileCache.cs new file mode 100644 index 00000000..e671d935 --- /dev/null +++ b/src/Tableau.Migration/Content/Schedules/VolatileCache.cs @@ -0,0 +1,82 @@ +// +// 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 System.Threading.Tasks; + +namespace Tableau.Migration.Content.Schedules +{ + /// + /// Class that can efficiently cache TContent objects for a given TKey, and release resources whenever they are read. + /// + /// Implementations should be thread safe due to parallel migration processing. + internal class VolatileCache + where TKey : struct + where TContent : class + { + private Dictionary _cache = new(); + + private readonly SemaphoreSlim _writeSemaphore = new(1, 1); + + private readonly Func>> _loadCache; + + private bool _loaded = false; + + public VolatileCache(Func>> loadCache) + { + _loadCache = loadCache; + } + + /// + /// Single-threaded read all values linked to a given key reference, and release it when the key is found. + /// + /// The content key. + /// The cancellation token to obey. + /// The cached value, or null. + public async Task GetAndRelease( + TKey keyValue, + CancellationToken cancel) + { + TContent? cachedResult = null; + + await _writeSemaphore.WaitAsync(cancel).ConfigureAwait(false); + + try + { + if (!_loaded) + { + _cache = await _loadCache(cancel).ConfigureAwait(false); + + _loaded = true; + } + + if (_cache.TryGetValue(keyValue, out cachedResult)) + { + _cache.Remove(keyValue); + } + } + finally + { + _writeSemaphore.Release(); + } + + return cachedResult; + } + } +} diff --git a/src/Tableau.Migration/Content/Search/ContentCacheFactory.cs b/src/Tableau.Migration/Content/Search/ContentCacheFactory.cs new file mode 100644 index 00000000..4b19c842 --- /dev/null +++ b/src/Tableau.Migration/Content/Search/ContentCacheFactory.cs @@ -0,0 +1,46 @@ +// +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace Tableau.Migration.Content.Search +{ + internal class ContentCacheFactory : IContentCacheFactory + { + private readonly IServiceProvider _services; + + public ContentCacheFactory(IServiceProvider services) + { + _services = services; + } + + public IContentCache? ForContentType([DoesNotReturnIf(true)] bool throwIfNotAvailable) + where TContent : class, IContentReference + { + var cache = _services.GetService>(); + + if (cache is null && throwIfNotAvailable) + { + throw new ArgumentException($"No content cache was found for content type {typeof(TContent).Name}.", nameof(TContent)); + } + + return cache; + } + } +} diff --git a/src/Tableau.Migration/Content/Search/IContentCache.cs b/src/Tableau.Migration/Content/Search/IContentCache.cs new file mode 100644 index 00000000..2a551cfa --- /dev/null +++ b/src/Tableau.Migration/Content/Search/IContentCache.cs @@ -0,0 +1,71 @@ +// +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Tableau.Migration.Content.Search +{ + /// + /// Interface for an object that can efficiently cache objects for a given endpoint and content type. + /// + /// Implementations should be thread safe due to parallel migration processing. + public interface IContentCache : IContentReferenceCache + where TContent : class, IContentReference + { + /// + /// Finds the content item for a given endpoint location. + /// + /// The location. + /// The cancellation token to obey. + /// The content item, or null if no item was found. + new Task ForLocationAsync(ContentLocation location, CancellationToken cancel); + + /// + /// Finds the content item for a given endpoint ID. + /// + /// The ID. + /// The cancellation token to obey. + /// The content item, or null if no item was found. + new Task ForIdAsync(Guid id, CancellationToken cancel); + + /// + /// Finds the item for a given . + /// + /// The content reference. + /// The cancellation token to obey. + /// The item, or null if no item was found. + Task ForReferenceAsync(IContentReference reference, CancellationToken cancel); + + /// + /// Adds or updates the content item in the cache. + /// + /// The content item. + /// The added or updated content item. + TContent AddOrUpdate(TContent item); + + /// + /// Adds or updates the content items in the cache. + /// + /// The content items. + /// The added or updated content items. + IImmutableList AddOrUpdateRange(IEnumerable items); + } +} diff --git a/src/Tableau.Migration/Content/Search/IContentCacheFactory.cs b/src/Tableau.Migration/Content/Search/IContentCacheFactory.cs new file mode 100644 index 00000000..cca45cb5 --- /dev/null +++ b/src/Tableau.Migration/Content/Search/IContentCacheFactory.cs @@ -0,0 +1,37 @@ +// +// 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.Diagnostics.CodeAnalysis; + +namespace Tableau.Migration.Content.Search +{ + /// + /// Interface for an object that can create caches for a given content type. + /// + /// Implementations should be thread safe due to parallel migration processing. + public interface IContentCacheFactory + { + /// + /// Gets or creates a content cache for a given content type. + /// + /// The content type. + /// True to throw if the cache is not available/registered, false otherwise. + /// The content cache. + IContentCache? ForContentType([DoesNotReturnIf(true)] bool throwIfNotAvailable) + where TContent : class, IContentReference; + } +} diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CloudIncrementalRefreshTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CloudIncrementalRefreshTransformer.cs new file mode 100644 index 00000000..bd559750 --- /dev/null +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CloudIncrementalRefreshTransformer.cs @@ -0,0 +1,52 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules.Cloud; + + +using Tableau.Migration.Resources; + +namespace Tableau.Migration.Engine.Hooks.Transformers.Default +{ + /// + /// Transformer that changes extract refresh tasks to cloud supported ones. + /// + public class CloudIncrementalRefreshTransformer( + ISharedResourcesLocalizer localizer, + ILogger logger) + : ContentTransformerBase(localizer, logger) + { + + /// + public override Task TransformAsync( + ICloudExtractRefreshTask itemToTransform, + CancellationToken cancel) + { + // Convert Server Incremental Refresh to Cloud Incremental Refresh + if (itemToTransform.Type == ExtractRefreshType.ServerIncrementalRefresh) + { + itemToTransform.Type = ExtractRefreshType.CloudIncrementalRefresh; + } + + return Task.FromResult(itemToTransform); + } + } +} diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CloudScheduleCompatibilityTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CloudScheduleCompatibilityTransformer.cs new file mode 100644 index 00000000..ad8a8adf --- /dev/null +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CloudScheduleCompatibilityTransformer.cs @@ -0,0 +1,74 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; + + +using Tableau.Migration.Resources; + +namespace Tableau.Migration.Engine.Hooks.Transformers.Default +{ + /// + /// Transformer that changes extract refresh tasks to cloud supported ones. + /// + public class CloudScheduleCompatibilityTransformer + : ContentTransformerBase + where TWithSchedule : IWithSchedule + { + /// + /// Creates a new object. + /// + /// A string localizer. + /// The logger used to log messages. + public CloudScheduleCompatibilityTransformer( + ISharedResourcesLocalizer localizer, + ILogger> logger) + : base(localizer, logger) + { } + + /// + public override Task TransformAsync( + TWithSchedule itemToTransform, + CancellationToken cancel) + { + var currentFrequency = itemToTransform.Schedule.Frequency; + var currentIntervals = itemToTransform.Schedule.FrequencyDetails.Intervals; + + if (currentFrequency.IsCloudCompatible(currentIntervals)) + { + return Task.FromResult(itemToTransform); + } + + var newIntervals = currentFrequency.ToCloudCompatible(currentIntervals); + + if (Logger.LogIntervalsChanges( + Localizer[SharedResourceKeys.IntervalsChangedWarning], + itemToTransform.Id, + currentIntervals, + newIntervals)) + { + itemToTransform.Schedule.FrequencyDetails.Intervals = newIntervals; + } + + return Task.FromResult(itemToTransform); + } + } +} diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedReferenceExtractRefreshTaskTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedReferenceExtractRefreshTaskTransformer.cs new file mode 100644 index 00000000..eea7198e --- /dev/null +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedReferenceExtractRefreshTaskTransformer.cs @@ -0,0 +1,101 @@ +// +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Resources; + +namespace Tableau.Migration.Engine.Hooks.Transformers.Default +{ + /// + /// Transformer that maps the workbook/data source from a given extract refresh task. + /// + public class MappedReferenceExtractRefreshTaskTransformer + : ContentTransformerBase + { + private readonly IDestinationContentReferenceFinder _dataSourceFinder; + private readonly IDestinationContentReferenceFinder _workbookFinder; + + /// + /// Creates a new object. + /// + /// Destination content finder factory object. + /// A string localizer. + /// The logger used to log messages. + public MappedReferenceExtractRefreshTaskTransformer( + IDestinationContentReferenceFinderFactory destinationFinderFactory, + ISharedResourcesLocalizer localizer, + ILogger logger) + : base(localizer, logger) + { + _dataSourceFinder = destinationFinderFactory.ForDestinationContentType(); + _workbookFinder = destinationFinderFactory.ForDestinationContentType(); + } + + /// + public override async Task TransformAsync( + ICloudExtractRefreshTask extractRefreshTask, + CancellationToken cancel) + { + var destinationReference = await FindDestinationReferenceAsync( + extractRefreshTask.ContentType, + extractRefreshTask.Content, + cancel) + .ConfigureAwait(false); + + if (destinationReference is null) + { + Logger.LogWarning( + Localizer[SharedResourceKeys.MappedReferenceExtractRefreshTaskTransformerCannotFindReferenceWarning], + extractRefreshTask.Id, + extractRefreshTask.ContentType, + extractRefreshTask.Content); + } + else + { + extractRefreshTask.Content = destinationReference; + } + + return extractRefreshTask; + } + + private async Task FindDestinationReferenceAsync( + ExtractRefreshContentType extractRefreshContentType, + IContentReference sourceContentReference, + CancellationToken cancel) + { + switch (extractRefreshContentType) + { + case ExtractRefreshContentType.Workbook: + return await _workbookFinder + .FindBySourceIdAsync(sourceContentReference.Id, cancel) + .ConfigureAwait(false); + case ExtractRefreshContentType.DataSource: + return await _dataSourceFinder + .FindBySourceIdAsync(sourceContentReference.Id, cancel) + .ConfigureAwait(false); + } + + return null; + } + } +} diff --git a/src/Tableau.Migration/Engine/Preparation/ExtractRefreshTaskServerToCloudPreparer.cs b/src/Tableau.Migration/Engine/Preparation/ExtractRefreshTaskServerToCloudPreparer.cs new file mode 100644 index 00000000..e7b0c9c7 --- /dev/null +++ b/src/Tableau.Migration/Engine/Preparation/ExtractRefreshTaskServerToCloudPreparer.cs @@ -0,0 +1,151 @@ +// +// 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.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Config; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Engine.Endpoints; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Engine.Hooks.Transformers; + +namespace Tableau.Migration.Engine.Preparation +{ + /// + /// implementation that pulls + /// the publish item from the source endpoint. + /// + public class ExtractRefreshTaskServerToCloudPreparer + : EndpointContentItemPreparer + { + private readonly IDestinationApiEndpoint? _destinationApi; + private readonly IConfigReader _configReader; + private readonly VolatileCache<(ExtractRefreshContentType, Guid), ImmutableList> _destinationExtractRefreshTasksCache; + + /// + /// Creates a new object. + /// + /// The source endpoint. + /// The destination endpoint. + /// + /// + /// A config reader. + public ExtractRefreshTaskServerToCloudPreparer( + ISourceEndpoint source, + IDestinationEndpoint destination, + IContentTransformerRunner transformerRunner, + IDestinationContentReferenceFinderFactory destinationFinderFactory, + IConfigReader configReader) + : base( + source, + transformerRunner, + destinationFinderFactory) + { + if (destination is IDestinationApiEndpoint destinationApi) + { + _destinationApi = destinationApi; + } + + _configReader = configReader; + _destinationExtractRefreshTasksCache = new( + async cancel => + { + var result = await _destinationApi! + .SiteApi + .CloudTasks + .GetAllExtractRefreshTasksAsync(cancel) + .ConfigureAwait(false); + + if (!result.Success) + { + return new(); + } + + return result + .Value + .GroupBy(item => (item.ContentType, item.Content.Id)) + .ToDictionary(group => group.Key, group => group.ToImmutableList()); + }); + } + + /// + protected override async Task> TransformAsync( + ICloudExtractRefreshTask publishItem, + CancellationToken cancel) + { + var result = await base.TransformAsync(publishItem, cancel).ConfigureAwait(false); + + if (result.Success) + { + await CleanExtractRefreshTasks( + publishItem.ContentType, + publishItem.Content.Id, + cancel) + .ConfigureAwait(false); + } + + return result; + } + + private async Task CleanExtractRefreshTasks( + ExtractRefreshContentType contentType, + Guid contentId, + CancellationToken cancel) + { + if (_destinationApi is null) + { + return; + } + + var items = await _destinationExtractRefreshTasksCache + .GetAndRelease( + (contentType, contentId), + cancel) + .ConfigureAwait(false); + + if (items is null) + { + return; + } + + await Parallel + .ForEachAsync( + items, + new ParallelOptions + { + CancellationToken = cancel, + MaxDegreeOfParallelism = _configReader.Get().MigrationParallelism + }, + async (item, itemCancel) => + { + await _destinationApi + .SiteApi + .CloudTasks + .DeleteExtractRefreshTaskAsync( + item.Id, + cancel) + .ConfigureAwait(false); + }) + .ConfigureAwait(false); + } + } +} diff --git a/src/Tableau.Migration/TableauInstanceType.cs b/src/Tableau.Migration/TableauInstanceType.cs new file mode 100644 index 00000000..8586ad34 --- /dev/null +++ b/src/Tableau.Migration/TableauInstanceType.cs @@ -0,0 +1,40 @@ +// +// 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. +// + +namespace Tableau.Migration +{ + /// + /// The type of Tableau instance connected to. + /// + public enum TableauInstanceType + { + /// + /// Constant for unknown Tableau server instance. + /// + Unknown = 0, + + /// + /// Constant for Tableau Server. + /// + Server = 1, + + /// + /// Constant for Tableau Cloud. + /// + Cloud = 2, + } +} diff --git a/src/Tableau.Migration/TableauInstanceTypeExtensions.cs b/src/Tableau.Migration/TableauInstanceTypeExtensions.cs new file mode 100644 index 00000000..febaf0ad --- /dev/null +++ b/src/Tableau.Migration/TableauInstanceTypeExtensions.cs @@ -0,0 +1,32 @@ +// +// 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. +// + +namespace Tableau.Migration +{ + internal static class TableauInstanceTypeExtensions + { + public static string GetFriendlyName(this TableauInstanceType instanceType) + { + return instanceType switch + { + TableauInstanceType.Server => "Tableau Server", + TableauInstanceType.Cloud => "Tableau Cloud", + _ => "Unknown", + }; + } + } +} diff --git a/src/Tableau.Migration/TableauInstanceTypeNotSupportedException.cs b/src/Tableau.Migration/TableauInstanceTypeNotSupportedException.cs new file mode 100644 index 00000000..87ea08be --- /dev/null +++ b/src/Tableau.Migration/TableauInstanceTypeNotSupportedException.cs @@ -0,0 +1,34 @@ +// +// 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 Tableau.Migration.Resources; + +namespace Tableau.Migration +{ + /// + /// The exception that is thrown when an operation is not supported for the current . + /// + public class TableauInstanceTypeNotSupportedException(TableauInstanceType unsupported, ISharedResourcesLocalizer localizer, string? message = null) + : NotSupportedException(message ?? localizer[SharedResourceKeys.TableauInstanceTypeNotSupportedMessage, unsupported.GetFriendlyName()]) + { + /// + /// Gets the unsupported . + /// + public TableauInstanceType UnsupportedInstanceType { get; } = unsupported; + } +} diff --git a/tests/Python.TestApplication/.gitignore b/tests/Python.TestApplication/.gitignore new file mode 100644 index 00000000..2ddf5f27 --- /dev/null +++ b/tests/Python.TestApplication/.gitignore @@ -0,0 +1 @@ +manifest.json diff --git a/tests/Tableau.Migration.Tests/Simulation/Tests/Api/SchedulesApiClientTests.cs b/tests/Tableau.Migration.Tests/Simulation/Tests/Api/SchedulesApiClientTests.cs new file mode 100644 index 00000000..224fa761 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Simulation/Tests/Api/SchedulesApiClientTests.cs @@ -0,0 +1,127 @@ +// +// 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.Threading.Tasks; +using Tableau.Migration.Api.Rest; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Xunit; + +namespace Tableau.Migration.Tests.Simulation.Tests.Api +{ + public class SchedulesApiClientTests + { + public abstract class SchedulesApiClientTest : ApiClientTestBase + { } + + public class GetByIdAsync : SchedulesApiClientTest + { + [Fact] + public async Task Returns_Schedule_on_success() + { + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var schedule = Create(); + + Api.Data.Schedules.Add(schedule); + + var result = await sitesClient.Schedules.GetByIdAsync(schedule.Id, Cancel); + + Assert.True(result.Success); + Assert.NotNull(result.Value); + } + + [Fact] + public async Task Returns_error_when_not_found() + { + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var result = await sitesClient.Schedules.GetByIdAsync(Create(), Cancel); + + Assert.False(result.Success); + Assert.Null(result.Value); + + var error = Assert.Single(result.Errors); + Assert.IsType(error); + } + } + + public class GetScheduleExtractRefreshTasksAsync : SchedulesApiClientTest + { + [Fact] + public async Task Returns_success_on_success() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var schedule = Create(); + + const int ADDITIONAL_EXTRACTS_COUNT = 7; + + // Add a few more extract refresh tasks for the schedules + for (int i = 0; i < ADDITIONAL_EXTRACTS_COUNT; i++) + { + Api.Data.CreateScheduleExtractRefreshTask( + AutoFixture); + } + + const int EXTRACTS_COUNT = 10; + + for (var i = 0; i != EXTRACTS_COUNT; i++) + { + Api.Data.CreateScheduleExtractRefreshTask( + AutoFixture, + schedule); + } + + // Act + var result = await sitesClient.Schedules.GetScheduleExtractRefreshTasksAsync(schedule.Id, 1, 100, Cancel); + + // Assert + Assert.True(result.Success); + Assert.NotNull(result.Value); + Assert.Equal(EXTRACTS_COUNT, result.Value.Count); + Assert.Equal(EXTRACTS_COUNT, result.TotalCount); + } + + [Fact] + public async Task Returns_success_when_no_schedules_exist() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var schedule = Create(); + + const int ADDITIONAL_EXTRACTS_COUNT = 7; + + // Add a few more extract refresh tasks for the schedules + for (int i = 0; i < ADDITIONAL_EXTRACTS_COUNT; i++) + { + Api.Data.CreateScheduleExtractRefreshTask( + AutoFixture); + } + + // Act + var result = await sitesClient.Schedules.GetScheduleExtractRefreshTasksAsync(schedule.Id, 1, 100, Cancel); + + // Assert + Assert.True(result.Success); + Assert.Empty(result.Value); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Simulation/Tests/Api/TasksApiClientTests.cs b/tests/Tableau.Migration.Tests/Simulation/Tests/Api/TasksApiClientTests.cs new file mode 100644 index 00000000..30494538 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Simulation/Tests/Api/TasksApiClientTests.cs @@ -0,0 +1,365 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Tableau.Migration.Api; +using Tableau.Migration.Api.Models.Cloud; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Xunit; + +namespace Tableau.Migration.Tests.Simulation.Tests.Api +{ + public class TasksApiClientTests + { + public abstract class TasksApiClientTest : ApiClientTestBase + { + public TasksApiClientTest(bool isCloud = false) + : base(isCloud) + { } + } + + #region - FromServer Tests - + + public class For_FromServer : TasksApiClientTest + { + public For_FromServer() + : base(false) + { } + + [Fact] + public async Task ForServer_Returns_ServerTasksApiClient() + { + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var serverTasks = sitesClient.ServerTasks; + + Assert.NotNull(serverTasks); + + var taskApi = Assert.IsAssignableFrom(serverTasks); + + Assert.IsAssignableFrom(taskApi.ForServer()); + } + + [Fact] + public async Task ForCloud_Throws_Exception() + { + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var cloudTasks = sitesClient.ServerTasks; + + Assert.NotNull(cloudTasks); + + var taskApi = Assert.IsAssignableFrom(cloudTasks); + + Assert.Throws(taskApi.ForCloud); + } + } + + public class GetAllExtractRefreshTasksAsync_FromServer : TasksApiClientTest + { + public GetAllExtractRefreshTasksAsync_FromServer() + : base(false) + { } + + [Fact] + public async Task Returns_success_on_success() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + + const int EXTRACT_REFRESH_COUNT = 10; + + var tasks = Api.Data.CreateServerExtractRefreshTasks(AutoFixture, EXTRACT_REFRESH_COUNT); + + // Act + var result = await sitesClient.ServerTasks.GetAllExtractRefreshTasksAsync(Cancel); + + // Assert + Assert.True(result.Success); + Assert.NotEmpty(result.Value); + Assert.Equal(EXTRACT_REFRESH_COUNT, result.Value.Count); + } + + [Fact] + public async Task Returns_success_with_empty() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + + // Act + var result = await sitesClient.ServerTasks.GetAllExtractRefreshTasksAsync(Cancel); + + // Assert + Assert.True(result.Success); + Assert.Empty(result.Value); + } + } + + #endregion - FromServer Tests - + + #region - FromCloud Tests - + + public class For_FromCloud : TasksApiClientTest + { + public For_FromCloud() + : base(true) + { } + + [Fact] + public async Task ForServer_Throws_Exception() + { + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var cloudTasks = sitesClient.CloudTasks; + + Assert.NotNull(cloudTasks); + + var taskApi = Assert.IsAssignableFrom(cloudTasks); + + Assert.Throws(taskApi.ForServer); + } + + [Fact] + public async Task ForCloud_Returns_CloudTasksApiClient() + { + await using var sitesClient = await GetSitesClientAsync(Cancel); + + var cloudTasks = sitesClient.CloudTasks; + + Assert.NotNull(cloudTasks); + + var taskApi = Assert.IsAssignableFrom(cloudTasks); + + Assert.IsAssignableFrom(taskApi.ForCloud()); + } + } + + public class GetAllExtractRefreshTasksAsync_FromCloud : TasksApiClientTest + { + public GetAllExtractRefreshTasksAsync_FromCloud() + : base(true) + { } + + [Fact] + public async Task Returns_success_on_success() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + + const int EXTRACT_REFRESH_COUNT = 10; + + var tasks = Api.Data.CreateCloudExtractRefreshTasks(AutoFixture, EXTRACT_REFRESH_COUNT); + + // Act + var result = await sitesClient.CloudTasks.GetAllExtractRefreshTasksAsync(Cancel); + + // Assert + Assert.True(result.Success); + Assert.NotEmpty(result.Value); + Assert.Equal(EXTRACT_REFRESH_COUNT, result.Value.Count); + } + + [Fact] + public async Task Returns_success_with_empty() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + + // Act + var result = await sitesClient.CloudTasks.GetAllExtractRefreshTasksAsync(Cancel); + + // Assert + Assert.True(result.Success); + Assert.Empty(result.Value); + } + } + + public class CreateExtractRefreshTaskAsync_FromCloud : TasksApiClientTest + { + public CreateExtractRefreshTaskAsync_FromCloud() + : base(true) + { } + + [Fact] + public async Task Returns_success_on_workbook_extract_refresh() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + var cloudTasks = sitesClient.CloudTasks; + + var workbook = Api.Data.CreateWorkbook(AutoFixture); + var frequency = ScheduleFrequencies.Weekly; + var start = new TimeOnly(20, 0); + var interval = Interval.WithWeekday(WeekDays.Monday); + var schedule = new CloudSchedule( + frequency, + new FrequencyDetails( + start, + null, + [interval])); + + var options = new CreateExtractRefreshTaskOptions( + ExtractRefreshType.FullRefresh, + ExtractRefreshContentType.Workbook, + workbook.Id, + schedule); + + Assert.DoesNotContain( + Api.Data.CloudExtractRefreshTasks, + cert => + cert.ExtractRefresh!.Workbook is not null && + cert.ExtractRefresh!.Workbook.Id == workbook.Id); + + // Act + var result = await cloudTasks.CreateExtractRefreshTaskAsync( + options, + Cancel); + + // Assert + Assert.True(result.Success); + var extractRefresh = Api.Data.CloudExtractRefreshTasks.FirstOrDefault(cert => cert.ExtractRefresh!.Id == result.Value.Id); + + Assert.NotNull(extractRefresh); + Assert.NotNull(extractRefresh.ExtractRefresh!.Workbook); + Assert.Equal(workbook.Id, extractRefresh.ExtractRefresh.Workbook.Id); + Assert.NotNull(extractRefresh.ExtractRefresh.Schedule); + Assert.Equal(frequency, extractRefresh.ExtractRefresh.Schedule.Frequency); + Assert.NotNull(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails); + Assert.Equal(start.ToString(Constants.FrequencyTimeFormat), extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Start); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.End); + Assert.Single(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals); + Assert.Equal(interval.WeekDay, extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].WeekDay); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].Hours); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].Minutes); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].MonthDay); + } + + [Fact] + public async Task Returns_success_on_datasource_extract_refresh() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + var cloudTasks = sitesClient.CloudTasks; + + var datasource = Api.Data.CreateDataSource(AutoFixture); + var frequency = ScheduleFrequencies.Monthly; + var start = new TimeOnly(13, 45); + var interval = Interval.WithMonthDay("10"); + var schedule = new CloudSchedule( + frequency, + new FrequencyDetails( + start, + null, + [interval])); + + var options = new CreateExtractRefreshTaskOptions( + ExtractRefreshType.ServerIncrementalRefresh, + ExtractRefreshContentType.DataSource, + datasource.Id, + schedule); + + Assert.DoesNotContain( + Api.Data.CloudExtractRefreshTasks, + cert => + cert.ExtractRefresh!.DataSource is not null && + cert.ExtractRefresh!.DataSource.Id == datasource.Id); + + // Act + var result = await cloudTasks.CreateExtractRefreshTaskAsync( + options, + Cancel); + + // Assert + Assert.True(result.Success); + var extractRefresh = Api.Data.CloudExtractRefreshTasks.FirstOrDefault(cert => cert.ExtractRefresh!.Id == result.Value.Id); + + Assert.NotNull(extractRefresh); + Assert.NotNull(extractRefresh.ExtractRefresh!.DataSource); + Assert.Equal(datasource.Id, extractRefresh.ExtractRefresh!.DataSource.Id); + Assert.NotNull(extractRefresh.ExtractRefresh.Schedule); + Assert.Equal(frequency, extractRefresh.ExtractRefresh.Schedule.Frequency); + Assert.NotNull(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails); + Assert.Equal(start.ToString(Constants.FrequencyTimeFormat), extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Start); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.End); + Assert.Single(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals); + Assert.Equal(interval.MonthDay, extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].MonthDay); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].Hours); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].Minutes); + Assert.Null(extractRefresh.ExtractRefresh.Schedule.FrequencyDetails.Intervals[0].WeekDay); + } + } + + public class DeleteExtractRefreshTaskAsync_FromCloud : TasksApiClientTest + { + public DeleteExtractRefreshTaskAsync_FromCloud() + : base(true) + { } + + [Fact] + public async Task Returns_success_on_existing_workbook_extract_refresh() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + var cloudTasks = sitesClient.CloudTasks; + + var workbook = Api.Data.CreateWorkbook(AutoFixture); + var extractRefreshTask = Api.Data.CreateCloudExtractRefreshTask( + AutoFixture, + workbook: workbook); + Assert.Contains(Api.Data.CloudExtractRefreshTasks, cert => cert.ExtractRefresh!.Id == extractRefreshTask.ExtractRefresh!.Id); + + // Act + var result = await cloudTasks.DeleteExtractRefreshTaskAsync( + extractRefreshTask.ExtractRefresh!.Id, + Cancel); + + // Assert + Assert.True(result.Success); + Assert.DoesNotContain(Api.Data.CloudExtractRefreshTasks, cert => cert.ExtractRefresh!.Id == extractRefreshTask.ExtractRefresh!.Id); + } + + [Fact] + public async Task Returns_success_on_existing_datasource_extract_refresh() + { + // Arrange + await using var sitesClient = await GetSitesClientAsync(Cancel); + var cloudTasks = sitesClient.CloudTasks; + + var dataSource = Api.Data.CreateDataSource(AutoFixture); + var extractRefreshTask = Api.Data.CreateCloudExtractRefreshTask( + AutoFixture, + dataSource: dataSource); + Assert.Contains(Api.Data.CloudExtractRefreshTasks, cert => cert.ExtractRefresh!.Id == extractRefreshTask.ExtractRefresh!.Id); + + // Act + var result = await cloudTasks.DeleteExtractRefreshTaskAsync( + extractRefreshTask.ExtractRefresh!.Id, + Cancel); + + // Assert + Assert.True(result.Success); + Assert.DoesNotContain(Api.Data.CloudExtractRefreshTasks, cert => cert.ExtractRefresh!.Id == extractRefreshTask.ExtractRefresh!.Id); + } + } + + #endregion - FromCloud Tests - + } +} diff --git a/tests/Tableau.Migration.Tests/Simulation/Tests/ExtractRefreshTaskMigrationTests.cs b/tests/Tableau.Migration.Tests/Simulation/Tests/ExtractRefreshTaskMigrationTests.cs new file mode 100644 index 00000000..f523417f --- /dev/null +++ b/tests/Tableau.Migration.Tests/Simulation/Tests/ExtractRefreshTaskMigrationTests.cs @@ -0,0 +1,149 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +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.Engine.Manifest; +using Xunit; + +namespace Tableau.Migration.Tests.Simulation.Tests +{ + public class ExtractRefreshTaskMigrationTests + { + public class UsersBatch : ServerToCloud + { + } + + public class UsersIndividual : ServerToCloud + { + protected override bool UsersBatchImportEnabled => false; + } + + public abstract class ServerToCloud : ServerToCloudSimulationTestBase + { + [Fact] + public async Task MigratesAllExtractRefreshTasksToCloudAsync() + { + //Arrange - create source content to migrate. + var (NonSupportUsers, SupportUsers) = PrepareSourceUsersData(5); + var groups = PrepareSourceGroupsData(5); + var sourceProjects = PrepareSourceProjectsData(); + var sourceDataSources = PrepareSourceDataSourceData(); + var sourceWorkbooks = PrepareSourceWorkbooksData(); + var sourceExtractRefreshTasks = PrepareSourceExtractRefreshTasksData(); + + //Migrate + var plan = ServiceProvider.GetRequiredService() + .FromSource(SourceEndpointConfig) + .ToDestination(CloudDestinationEndpointConfig) + .ForServerToCloud() + .WithTableauIdAuthenticationType() + .WithTableauCloudUsernames("test.com") + .Build(); + + var migrator = ServiceProvider.GetRequiredService(); + var result = await migrator.ExecuteAsync(plan, Cancel); + + //Assert - all extract refresh tasks should be migrated. + + Assert.Empty(result.Manifest.Errors); + Assert.Equal(MigrationCompletionStatus.Completed, result.Status); + + Assert.Equal(CloudDestinationApi.Data.CloudExtractRefreshTasks.Count, + result.Manifest.Entries.ForContentType().Where(e => e.Status == MigrationManifestEntryStatus.Migrated).Count()); + + Assert.All(sourceExtractRefreshTasks, AssertExtractRefreshTasksMigrated); + + void AssertExtractRefreshTasksMigrated(ExtractRefreshTasksResponse.TaskType sourceExtractRefreshTask) + { + // Get source references + var sourceExtractRefresh = sourceExtractRefreshTask.ExtractRefresh!; + var sourceSchedule = SourceApi.Data.Schedules.First(sch => sch.Id == sourceExtractRefresh.Schedule!.Id); + var extractRefreshType = SourceApi.Data.ScheduleExtractRefreshTasks.First(t => t.Id == sourceExtractRefresh.Id); + var sourceDataSource = SourceApi.Data.DataSources.FirstOrDefault(ds => + sourceExtractRefresh.DataSource is not null && + ds.Id == sourceExtractRefresh.DataSource.Id); + var sourceWorkbook = SourceApi.Data.Workbooks.FirstOrDefault(wb => + sourceExtractRefresh.Workbook is not null && + wb.Id == sourceExtractRefresh.Workbook.Id); + // Get destination references + var destinationDataSource = CloudDestinationApi.Data.DataSources.FirstOrDefault(ds => + sourceDataSource is not null && + ds.Name == sourceDataSource.Name); + var destinationWorkbook = CloudDestinationApi.Data.Workbooks.FirstOrDefault(wb => + sourceWorkbook is not null && + wb.Name == sourceWorkbook.Name); + + // Get destination extract refresh task + var destinationExtractRefreshTask = Assert.Single( + CloudDestinationApi.Data.CloudExtractRefreshTasks.Where(cert => + ( + cert.ExtractRefresh!.DataSource is not null && + destinationDataSource is not null && + cert.ExtractRefresh.DataSource.Id == destinationDataSource.Id + ) || + ( + cert.ExtractRefresh!.Workbook is not null && + destinationWorkbook is not null && + cert.ExtractRefresh.Workbook.Id == destinationWorkbook.Id + ) + )); + var destinationExtractRefresh = destinationExtractRefreshTask.ExtractRefresh!; + + Assert.NotEqual(sourceExtractRefresh.Id, destinationExtractRefresh.Id); + if (extractRefreshType.Type == ExtractRefreshType.ServerIncrementalRefresh) + { + Assert.Equal(ExtractRefreshType.CloudIncrementalRefresh, destinationExtractRefresh.Type); + } + else + { + Assert.Equal(extractRefreshType.Type, destinationExtractRefresh.Type); + } + // Assert schedule information + Assert.Equal(sourceSchedule.Frequency, destinationExtractRefresh.Schedule.Frequency); + Assert.Equal(sourceSchedule.FrequencyDetails.Start, destinationExtractRefresh.Schedule.FrequencyDetails!.Start); + if (sourceSchedule.FrequencyDetails.End is null) + { + Assert.Null(destinationExtractRefresh.Schedule.FrequencyDetails.End); + } + else + { + Assert.Equal(sourceSchedule.FrequencyDetails.End, destinationExtractRefresh.Schedule.FrequencyDetails.End); + } + Assert.Equal(sourceSchedule.FrequencyDetails.Intervals.Length, destinationExtractRefresh.Schedule.FrequencyDetails.Intervals.Length); + Assert.All( + destinationExtractRefresh.Schedule.FrequencyDetails.Intervals, + destinationInterval => + { + Assert.Single(sourceSchedule.FrequencyDetails.Intervals + .Where(sourceInterval => + (sourceInterval.Hours ?? string.Empty) == (destinationInterval.Hours ?? string.Empty) && + (sourceInterval.Minutes ?? string.Empty) == (destinationInterval.Minutes ?? string.Empty) && + (sourceInterval.WeekDay ?? string.Empty) == (destinationInterval.WeekDay ?? string.Empty) && + (sourceInterval.MonthDay ?? string.Empty) == (destinationInterval.MonthDay ?? string.Empty))); + }); + } + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/StringEnumGenerator.cs b/tests/Tableau.Migration.Tests/StringEnumGenerator.cs new file mode 100644 index 00000000..e86a06fb --- /dev/null +++ b/tests/Tableau.Migration.Tests/StringEnumGenerator.cs @@ -0,0 +1,26 @@ +// +// 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. +// + +namespace Tableau.Migration.Tests +{ + public static class StringEnumGenerator + { + public static string GetRandomValue() + where T : StringEnum + => StringEnum.GetAll().PickRandom(); + } +} diff --git a/tests/Tableau.Migration.Tests/TestLogger.cs b/tests/Tableau.Migration.Tests/TestLogger.cs new file mode 100644 index 00000000..7845aa6a --- /dev/null +++ b/tests/Tableau.Migration.Tests/TestLogger.cs @@ -0,0 +1,65 @@ +// +// 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.Collections.Immutable; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Tableau.Migration.Tests +{ + public class TestLogger : TestLogger + { + } + + public class TestLogger : Mock>, ILogger + { + private readonly ImmutableArray.Builder _messages = ImmutableArray.CreateBuilder(); + + public IImmutableList Messages => _messages.ToImmutable(); + + public TestLogger() + { + Setup(l => l.Log( + It.IsAny(), + It.IsAny(), + It.IsAny>>>(), + It.IsAny(), + It.IsAny>>, Exception?, string>>())) + .Callback((logLevel, eventId, state, exception, formatter) => + { + var formatted = formatter.DynamicInvoke(state, exception)?.ToString(); + + var logMessage = new TestLoggerMessage(formatted, (IReadOnlyList>)state, logLevel, eventId, exception); + + _messages.Add(logMessage); + }); + } + + #region - ILogger - + + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + => Object.Log(logLevel, eventId, state, exception, formatter); + + bool ILogger.IsEnabled(LogLevel logLevel) => Object.IsEnabled(logLevel); + + IDisposable? ILogger.BeginScope(TState state) => Object.BeginScope(state); + + #endregion + } +} diff --git a/tests/Tableau.Migration.Tests/TestLoggerFactory.cs b/tests/Tableau.Migration.Tests/TestLoggerFactory.cs new file mode 100644 index 00000000..6dd725ea --- /dev/null +++ b/tests/Tableau.Migration.Tests/TestLoggerFactory.cs @@ -0,0 +1,49 @@ +// +// 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.Concurrent; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Tableau.Migration.Tests +{ + public class TestLoggerFactory : Mock, ILoggerFactory + { + private readonly ConcurrentDictionary _loggersByCategory = new(); + + public TestLoggerFactory() + { + Setup(f => f.CreateLogger(It.IsAny())) + .Returns((string category) => _loggersByCategory.GetOrAdd(category, _ => new TestLogger())); + } + + #region - ILoggerFactory - + + ILogger ILoggerFactory.CreateLogger(string categoryName) => Object.CreateLogger(categoryName); + + void ILoggerFactory.AddProvider(ILoggerProvider provider) => Object.AddProvider(provider); + + void IDisposable.Dispose() + { + Object.Dispose(); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/tests/Tableau.Migration.Tests/TestLoggerMessage.cs b/tests/Tableau.Migration.Tests/TestLoggerMessage.cs new file mode 100644 index 00000000..7e7a2160 --- /dev/null +++ b/tests/Tableau.Migration.Tests/TestLoggerMessage.cs @@ -0,0 +1,31 @@ +// +// 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 Microsoft.Extensions.Logging; + +namespace Tableau.Migration.Tests +{ + public readonly record struct TestLoggerMessage( + string? Message, + IReadOnlyList> State, + LogLevel LogLevel, + EventId EventId, + Exception? Exception) + { } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/IContentCacheFactoryExtensions.cs b/tests/Tableau.Migration.Tests/Unit/Api/IContentCacheFactoryExtensions.cs new file mode 100644 index 00000000..c42ccc80 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/IContentCacheFactoryExtensions.cs @@ -0,0 +1,37 @@ +// +// 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 AutoFixture; +using Moq; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Tests.Unit +{ + public static class IContentCacheFactoryExtensions + { + public static Mock> SetupMockCache( + this Mock mockCacheFactory, + IFixture autoFixture) + where TContent : class, IContentReference + { + var mockCache = autoFixture.Create>>(); + mockCacheFactory.Setup(x => x.ForContentType(It.IsAny())).Returns(mockCache.Object); + + return mockCache; + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Cloud/ExtractRefreshTasksResponseTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Cloud/ExtractRefreshTasksResponseTests.cs new file mode 100644 index 00000000..d1745983 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Cloud/ExtractRefreshTasksResponseTests.cs @@ -0,0 +1,107 @@ +// +// 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.Linq; +using Tableau.Migration.Api.Rest.Models.Responses.Cloud; +using Tableau.Migration.Content.Schedules; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Api.Rest.Models.Responses.Cloud +{ + public class ExtractRefreshTasksResponseTests + { + public class Serialization : SerializationTestBase + { + [Fact] + public void Serializes() + { + var expectedResponse = Create(); + var expectedResponseItem = expectedResponse.Items.First(); + + var expectedExtractRefresh = expectedResponseItem.ExtractRefresh; + Assert.NotNull(expectedExtractRefresh); + + expectedExtractRefresh.DataSource = null; + expectedExtractRefresh.Workbook = Create(); + + var expectedWorkBook = expectedExtractRefresh.Workbook; + Assert.NotNull(expectedWorkBook); + + var expectedSchedule = expectedExtractRefresh.Schedule; + Assert.NotNull(expectedSchedule); + + var expectedFrequencyDetails = expectedSchedule.FrequencyDetails; + Assert.NotNull(expectedFrequencyDetails); + + var interval = new ExtractRefreshTasksResponse.TaskType.ExtractRefreshType.ScheduleType.FrequencyDetailsType.IntervalType + { + WeekDay = IntervalValues.WeekDaysValues.ExceptNulls().PickRandom() + }; + + expectedFrequencyDetails.Intervals = [interval]; + + var xml = @$" + + + + + + + + + + + + + + + + +"; + var deserialized = Serializer.DeserializeFromXml(xml); + Assert.NotNull(deserialized); + Assert.Single(deserialized.Items); + + var actualExtractRefresh = deserialized.Items[0].ExtractRefresh; + Assert.NotNull(actualExtractRefresh); + Assert.Equal(expectedExtractRefresh.Id, actualExtractRefresh.Id); + Assert.Equal(expectedExtractRefresh.Priority, actualExtractRefresh.Priority); + Assert.Equal(expectedExtractRefresh.ConsecutiveFailedCount, actualExtractRefresh.ConsecutiveFailedCount); + + var actualSchedule = actualExtractRefresh.Schedule; + Assert.NotNull(actualSchedule); + Assert.Equal(expectedSchedule.Frequency, actualSchedule.Frequency); + Assert.Equal(expectedSchedule.NextRunAt, actualSchedule.NextRunAt); + + var actualFrequencyDetails= actualSchedule.FrequencyDetails; + Assert.NotNull(actualFrequencyDetails); + Assert.Equal(expectedFrequencyDetails.Start, actualFrequencyDetails.Start); + Assert.Equal(expectedFrequencyDetails.End, actualFrequencyDetails.End); + Assert.Single(actualFrequencyDetails.Intervals); + Assert.Equal(expectedFrequencyDetails.Intervals[0].WeekDay, actualFrequencyDetails.Intervals[0].WeekDay); + + var actualWorkbook = actualExtractRefresh.Workbook; + Assert.NotNull(actualWorkbook); + Assert.Equal(expectedWorkBook.Id, actualWorkbook.Id); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Server/ExtractRefreshTasksResponseTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Server/ExtractRefreshTasksResponseTests.cs new file mode 100644 index 00000000..b368977e --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Server/ExtractRefreshTasksResponseTests.cs @@ -0,0 +1,94 @@ +// +// 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.Linq; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Api.Rest.Models.Responses.Server +{ + public class ExtractRefreshTasksResponseTests + { + public class Serialization : SerializationTestBase + { + [Fact] + public void Serializes() + { + var expectedResponse = Create(); + var expectedResponseItem = expectedResponse.Items.First(); + + var expectedExtractRefresh = expectedResponseItem.ExtractRefresh; + Assert.NotNull(expectedExtractRefresh); + + var expectedSchedule = expectedExtractRefresh.Schedule; + Assert.NotNull(expectedSchedule); + + var expectedDataSource = expectedExtractRefresh.DataSource; + Assert.NotNull(expectedDataSource); + + var xml = @$" + + + + + + + + + + +"; + var deserialized = Serializer.DeserializeFromXml(xml); + Assert.NotNull(deserialized); + Assert.Single(deserialized.Items); + + var actualExtractRefresh = deserialized.Items[0].ExtractRefresh; + Assert.NotNull(actualExtractRefresh); + Assert.Equal(expectedExtractRefresh.Id, actualExtractRefresh.Id); + Assert.Equal(expectedExtractRefresh.Priority, actualExtractRefresh.Priority); + Assert.Equal(expectedExtractRefresh.ConsecutiveFailedCount, actualExtractRefresh.ConsecutiveFailedCount); + + var actualSchedule = actualExtractRefresh.Schedule; + Assert.NotNull(actualSchedule); + Assert.Equal(expectedSchedule.Id, actualSchedule.Id); + Assert.Equal(expectedSchedule.Name, actualSchedule.Name); + Assert.Equal(expectedSchedule.State, actualSchedule.State); + Assert.Equal(expectedSchedule.Priority, actualSchedule.Priority); + Assert.Equal(expectedSchedule.CreatedAt, actualSchedule.CreatedAt); + Assert.Equal(expectedSchedule.UpdatedAt, actualSchedule.UpdatedAt); + Assert.Equal(expectedSchedule.Frequency, actualSchedule.Frequency); + Assert.Equal(expectedSchedule.NextRunAt, actualSchedule.NextRunAt); + + var actualDataSource = actualExtractRefresh.DataSource; + Assert.NotNull(actualDataSource); + Assert.Equal(expectedDataSource.Id, actualDataSource.Id); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Server/ScheduleExtractRefreshTasksResponseTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Server/ScheduleExtractRefreshTasksResponseTests.cs new file mode 100644 index 00000000..b19c492b --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/Server/ScheduleExtractRefreshTasksResponseTests.cs @@ -0,0 +1,108 @@ +// +// 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.Linq; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Api.Rest.Models.Responses.Server +{ + public class ScheduleExtractRefreshTasksResponseTests + { + public class Serialization : SerializationTestBase + { + [Fact] + public void Serializes_workbook_extracts() + { + var expectedResponse = Create(); + var expectedExtract = expectedResponse.Items.First(); + + + var xml = @$" + + + + + + + + +"; + var deserialized = Serializer.DeserializeFromXml(xml); + Assert.NotNull(deserialized); + Assert.Single(deserialized.Items); + + var actualExtract = deserialized.Items[0]; + Assert.NotNull(actualExtract); + + Assert.Equal(expectedExtract.Id, actualExtract.Id); + Assert.Equal(expectedExtract.Priority, actualExtract.Priority); + Assert.Equal(expectedExtract.Type, actualExtract.Type); + + var actualWorkbook = actualExtract.Workbook; + var expectedWorkbook = expectedExtract.Workbook; + Assert.NotNull(actualWorkbook); + Assert.NotNull(expectedWorkbook); + Assert.Equal(expectedWorkbook.Id, actualWorkbook.Id); + } + + [Fact] + public void Serializes_DataSource_extracts() + { + var expectedResponse = Create(); + var expectedExtract = expectedResponse.Items.First(); + + + var xml = @$" + + + + + + + + +"; + var deserialized = Serializer.DeserializeFromXml(xml); + Assert.NotNull(deserialized); + Assert.Single(deserialized.Items); + + var actualExtract = deserialized.Items[0]; + + Assert.NotNull(actualExtract); + Assert.Equal(expectedExtract.Id, actualExtract.Id); + Assert.Equal(expectedExtract.Priority, actualExtract.Priority); + Assert.Equal(expectedExtract.Type, actualExtract.Type); + + var expectedDataSource = expectedExtract.DataSource; + Assert.NotNull(expectedDataSource); + + var actualDataSource = actualExtract.DataSource; + Assert.NotNull(actualDataSource); + Assert.Equal(expectedDataSource.Id, actualDataSource.Id); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/SchedulesApiClientTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/SchedulesApiClientTests.cs new file mode 100644 index 00000000..91a1894d --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/SchedulesApiClientTests.cs @@ -0,0 +1,168 @@ +// +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Moq; +using Tableau.Migration.Api; +using Tableau.Migration.Api.Rest.Models.Responses.Server; +using Tableau.Migration.Config; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Server; +using Tableau.Migration.Net.Rest; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Api +{ + public class SchedulesApiClientTests + { + public abstract class SchedulesApiClientTest : ApiClientTestBase + { + internal SchedulesApiClient SchedulesApiClient => GetApiClient(); + private readonly string _baseApiUri; + protected SchedulesApiClientTest() + { + _baseApiUri = $"/api/{TableauServerVersion.RestApiVersion}"; + } + + protected internal void AssertScheduleRelativeUri(HttpRequestMessage request, Guid scheduleId) + { + request.AssertRelativeUri($"{_baseApiUri}/schedules/{scheduleId.ToUrlSegment()}"); + } + protected internal void AssertScheduleExtractsRelativeUri(HttpRequestMessage request, Guid scheduleId) + { + request.AssertRelativeUri($"{_baseApiUri}/sites/{SiteId}/schedules/{scheduleId.ToUrlSegment()}/extracts"); + } + } + + public class GetByIdAsync : SchedulesApiClientTest + { + [Fact] + public async Task Returns_success() + { + var scheduleResponse = AutoFixture.CreateResponse(); + + var mockResponse = new MockHttpResponseMessage(scheduleResponse); + + MockHttpClient.SetupResponse(mockResponse); + + var scheduleExtractRefreshTasksResponse = AutoFixture.CreateResponse(); + var mockScheduleExtractRefreshTasksResponse = new MockHttpResponseMessage( + scheduleExtractRefreshTasksResponse); + + MockHttpClient.SetupResponse(mockScheduleExtractRefreshTasksResponse); + + var scheduleId = Guid.NewGuid(); + + MockConfigReader + .Setup(x => x.Get()) + .Returns(new ContentTypesOptions() + { + Type = "ExtractRefresh_ServerSchedule", + BatchSize = ContentTypesOptions.Defaults.BATCH_SIZE + }); + + var result = await SchedulesApiClient.GetByIdAsync(scheduleId, Cancel); + + Assert.True(result.Success); + + var requests = MockHttpClient.AssertRequestCount(2); + + Assert.Collection(requests, + getScheduleRequest => AssertScheduleRelativeUri(getScheduleRequest, scheduleId), + getScheduleExtractRefTasks => AssertScheduleExtractsRelativeUri(getScheduleExtractRefTasks, scheduleId)); + + MockScheduleCache.Verify(c => c.AddOrUpdate(result.Value), Times.Once); + + var serverSchedule = result.Value; + var expectedTasks = scheduleExtractRefreshTasksResponse.Items; + Assert.NotNull(expectedTasks); + Assert.Equal(expectedTasks.Length, serverSchedule.ExtractRefreshTasks.Count); + } + + [Fact] + public async Task Returns_failure() + { + var exception = new Exception(); + + var mockResponse = + new MockHttpResponseMessage(HttpStatusCode.InternalServerError, null); + mockResponse.Setup(r => r.EnsureSuccessStatusCode()).Throws(exception); + + MockHttpClient.SetupResponse(mockResponse); + + var scheduleId = Guid.NewGuid(); + + var result = await SchedulesApiClient.GetByIdAsync(scheduleId, Cancel); + + Assert.False(result.Success); + + var request = MockHttpClient.AssertSingleRequest(); + + AssertScheduleRelativeUri(request, scheduleId); + + MockScheduleCache.Verify(c => c.AddOrUpdate(It.IsAny()), Times.Never); + } + } + + public class GetScheduleExtractRefreshTasksAsync : SchedulesApiClientTest + { + [Fact] + public async Task Returns_success() + { + var response = AutoFixture.CreateResponse(); + + var mockResponse = new MockHttpResponseMessage(response); + + MockHttpClient.SetupResponse(mockResponse); + + var scheduleId = Guid.NewGuid(); + + var result = await SchedulesApiClient.GetScheduleExtractRefreshTasksAsync(scheduleId, 1, 1, Cancel); + + Assert.True(result.Success); + + var request = MockHttpClient.AssertSingleRequest(); + AssertScheduleExtractsRelativeUri(request, scheduleId); + } + + [Fact] + public async Task Returns_failure() + { + var exception = new Exception(); + + var mockResponse = + new MockHttpResponseMessage(HttpStatusCode.InternalServerError, null); + mockResponse.Setup(r => r.EnsureSuccessStatusCode()).Throws(exception); + + MockHttpClient.SetupResponse(mockResponse); + + var scheduleId = Guid.NewGuid(); + + var result = await SchedulesApiClient.GetScheduleExtractRefreshTasksAsync(scheduleId, 1, 1, Cancel); + + Assert.False(result.Success); + + var request = MockHttpClient.AssertSingleRequest(); + + AssertScheduleExtractsRelativeUri(request, scheduleId); + } + } + } +} \ No newline at end of file diff --git a/tests/Tableau.Migration.Tests/Unit/Api/Search/ApiContentCacheTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/Search/ApiContentCacheTests.cs new file mode 100644 index 00000000..7789800e --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/Search/ApiContentCacheTests.cs @@ -0,0 +1,141 @@ +// +// 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.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Tableau.Migration.Api.Search; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Search; +using Tableau.Migration.Tests.Unit.Engine.Endpoints.Search; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Api.Search +{ + public class ApiContentCacheTests + { + public abstract class ApiContentCacheTest : BulkCacheTest, TestContentType> + { + protected readonly ConcurrentDictionary InnerCache = new(); + + protected readonly Mock> MockCache; + + protected Mock MockReferenceCache => MockCache.As(); + + public ApiContentCacheTest() + { + MockCache = Mock.Get(Cache); + } + + protected override ApiContentCache CreateCache() + => new Mock>( + MockSitesApiClient.Object, + MockConfigReader.Object, + InnerCache) + { + CallBase = true + } + .Object; + } + + public class ForLocationAsync : ApiContentCacheTest + { + [Fact] + public async Task Returns_cached_content_when_found() + { + var reference = EndpointContent[0].ToStub(); + var content = new TestContentType(reference); + + MockReferenceCache.Setup(c => c.ForLocationAsync(reference.Location, Cancel)).ReturnsAsync(reference); + + InnerCache.GetOrAdd(reference, content); + + var result = await Cache.ForLocationAsync(content.Location, Cancel); + + Assert.Equal(content, result); + } + + [Fact] + public async Task Returns_null_when_not_found() + { + MockReferenceCache.Setup(c => c.ForLocationAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((IContentReference?)null); + + var result = await Cache.ForLocationAsync(Create(), Cancel); + + Assert.Null(result); + } + } + + public class ForIdAsync : ApiContentCacheTest + { + [Fact] + public async Task Returns_cached_content_when_found() + { + var reference = EndpointContent[0].ToStub(); + var content = new TestContentType(reference); + + MockReferenceCache.Setup(c => c.ForIdAsync(reference.Id, Cancel)).ReturnsAsync(reference); + + InnerCache.GetOrAdd(reference, content); + + var result = await Cache.ForIdAsync(content.Id, Cancel); + + Assert.Equal(content, result); + } + + [Fact] + public async Task Returns_null_when_not_found() + { + MockReferenceCache.Setup(c => c.ForIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((IContentReference?)null); + + var result = await Cache.ForIdAsync(Create(), Cancel); + + Assert.Null(result); + } + } + + public class ForReferenceAsync : ApiContentCacheTest + { + [Fact] + public async Task Returns_cached_content_when_found() + { + var reference = EndpointContent[0].ToStub(); + var content = new TestContentType(reference); + + InnerCache.GetOrAdd(reference, content); + + var result = await Cache.ForReferenceAsync(reference, Cancel); + + Assert.Equal(content, result); + } + + [Fact] + public async Task Returns_null_when_not_found() + { + InnerCache.Clear(); + + var result = await Cache.ForReferenceAsync(Create(), Cancel); + + Assert.Null(result); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs new file mode 100644 index 00000000..fd30095b --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Api/TasksApiClientTests.cs @@ -0,0 +1,423 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using AutoFixture; +using Moq; +using Tableau.Migration.Api; +using Tableau.Migration.Api.Models.Cloud; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Content.Schedules.Server; +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 abstract class TasksApiClientTest : ApiClientTestBase + { + internal virtual TasksApiClient TasksApiClient => GetApiClient(); + + protected TableauInstanceType CurrentInstanceType { get; set; } + + public TasksApiClientTest() + { + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(() => CurrentInstanceType); + } + + protected static List AssertSuccess(IResult> result) + where TExtractRefreshTask: IExtractRefreshTask + where TSchedule : ISchedule + { + Assert.NotNull(result); + Assert.Empty(result.Errors); + + var actualExtractRefreshes = result.Value?.ToList(); + Assert.NotNull(actualExtractRefreshes); + return actualExtractRefreshes; + } + } + + public class ForServer : TasksApiClientTest + { + [Theory] + [EnumData(TableauInstanceType.Server)] + public void Fails_when_current_instance_is_not_server(TableauInstanceType instanceType) + { + CurrentInstanceType = instanceType; + + var exception = Assert.Throws(TasksApiClient.ForServer); + + Assert.Equal(instanceType, exception.UnsupportedInstanceType); + + MockSessionProvider.VerifyGet(p => p.InstanceType, Times.Once); + } + + [Fact] + public void Returns_client_when_current_instance_is_server() + { + CurrentInstanceType = TableauInstanceType.Server; + + var client = TasksApiClient.ForServer(); + + MockSessionProvider.VerifyGet(p => p.InstanceType, Times.Once); + } + } + + public class ForCloud : TasksApiClientTest + { + [Theory] + [EnumData(TableauInstanceType.Cloud)] + public void Fails_when_current_instance_is_not_cloud(TableauInstanceType instanceType) + { + CurrentInstanceType = instanceType; + + var exception = Assert.Throws(TasksApiClient.ForCloud); + + Assert.Equal(instanceType, exception.UnsupportedInstanceType); + + MockSessionProvider.VerifyGet(p => p.InstanceType, Times.Once); + } + + [Fact] + public void Returns_client_when_current_instance_is_cloud() + { + CurrentInstanceType = TableauInstanceType.Cloud; + + var client = TasksApiClient.ForCloud(); + + MockSessionProvider.VerifyGet(p => p.InstanceType, Times.Once); + } + } + + #region - DeleteExtractRefreshTaskAsync - + + public class DeleteExtractRefreshTaskAsync : TasksApiClientTest + { + [Fact] + public async Task Success() + { + //Setup + var extractRefreshTaskId = Guid.NewGuid(); + + MockHttpClient.SetupResponse(new MockHttpResponseMessage(HttpStatusCode.NoContent)); + + //Act + var result = await TasksApiClient.DeleteExtractRefreshTaskAsync(extractRefreshTaskId, Cancel); + + //Test + result.AssertSuccess(); + + MockHttpClient.AssertSingleRequest(r => + { + r.AssertHttpMethod(HttpMethod.Delete); + r.AssertRelativeUri($"/api/{TableauServerVersion.RestApiVersion}/sites/{SiteId}/tasks/extractRefreshes/{extractRefreshTaskId}"); + }); + } + + [Fact] + public async Task Failure() + { + //Setup + var extractRefreshTaskId = Guid.NewGuid(); + + MockHttpClient.SetupResponse(new MockHttpResponseMessage(HttpStatusCode.InternalServerError)); + + //Act + var result = await TasksApiClient.DeleteExtractRefreshTaskAsync(extractRefreshTaskId, Cancel); + + //Test + result.AssertFailure(); + + MockHttpClient.AssertSingleRequest(r => + { + r.AssertHttpMethod(HttpMethod.Delete); + r.AssertRelativeUri($"/api/{TableauServerVersion.RestApiVersion}/sites/{SiteId}/tasks/extractRefreshes/{extractRefreshTaskId}"); + }); + } + } + + #endregion + + #region - Cloud - + + public class Cloud + { + public abstract class CloudTasksApiClientTest : TasksApiClientTest + { + internal ICloudTasksApiClient CloudTasksApiClient => TasksApiClient; + + public CloudTasksApiClientTest() + { + CurrentInstanceType = TableauInstanceType.Cloud; + } + + protected CloudResponses.ExtractRefreshTasksResponse CreateCloudResponse(ExtractRefreshContentType contentType) + { + if (contentType == ExtractRefreshContentType.DataSource) + { + AutoFixture.Customize( + composer => composer.With(j => j.Workbook, () => null)); + } + else + { + AutoFixture.Customize( + composer => composer.With(j => j.DataSource, () => null)); + } + + return AutoFixture.CreateResponse(); + } + } + + #region - GetAllExtractRefreshTasksAsync - + + public class GetAllExtractRefreshTasksAsync : CloudTasksApiClientTest + { + [Fact] + public async Task Gets_datasource_extract_refreshes() + { + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Cloud); + + var response = CreateCloudResponse(ExtractRefreshContentType.DataSource); + + SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + + SetupSuccessResponse(response); + + var result = await CloudTasksApiClient.GetAllExtractRefreshTasksAsync(Cancel); + + var actualExtractRefreshes = AssertSuccess(result); + var expectedExtractRefreshes = response.Items.ToList(); + + Assert.Equal(expectedExtractRefreshes.Count, actualExtractRefreshes.Count); + Assert.DoesNotContain(actualExtractRefreshes, item => item.ContentType == ExtractRefreshContentType.Workbook); + } + + [Fact] + public async Task Gets_workbook_extract_refreshes() + { + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Cloud); + + var response = CreateCloudResponse(ExtractRefreshContentType.Workbook); + + SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + + SetupSuccessResponse(response); + + var result = await CloudTasksApiClient.GetAllExtractRefreshTasksAsync(Cancel); + + var actualExtractRefreshes = AssertSuccess(result); + var expectedExtractRefreshes = response.Items.ToList(); + + Assert.Equal(expectedExtractRefreshes.Count, actualExtractRefreshes.Count); + Assert.DoesNotContain(actualExtractRefreshes, item => item.ContentType == ExtractRefreshContentType.DataSource); + } + } + + #endregion + + #region - CreateExtractRefreshTaskAsync - + + public class CreateExtractRefreshTaskAsync : CloudTasksApiClientTest + { + [Fact] + public async Task Creates_extract_refresh_for_workbook_successfully() + { + // 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); + SetupExtractRefreshContentFinder(response.Item); + + // Act + var result = await CloudTasksApiClient.CreateExtractRefreshTaskAsync( + createTaskOptions, + Cancel); + + // Assert + Assert.True(result.Success); + Assert.Equal(cloudSchedule.Frequency, result.Value.Schedule.Frequency); + Assert.Equal(ExtractRefreshContentType.Workbook, result.Value.ContentType); + Assert.Equal(contentReference.Id, result.Value.Content.Id); + } + + [Fact] + public async Task Creates_extract_refresh_for_datasource_successfully() + { + // Arrange + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Cloud); + var contentReference = AutoFixture.Create(); + var cloudSchedule = AutoFixture.Create(); + var createTaskOptions = new CreateExtractRefreshTaskOptions( + ExtractRefreshType.ServerIncrementalRefresh, + ExtractRefreshContentType.DataSource, + contentReference.Id, + cloudSchedule); + + var response = AutoFixture.CreateResponse(); + response.Item!.Workbook = null; + response.Item.DataSource!.Id = contentReference.Id; + response.Schedule!.Frequency = cloudSchedule.Frequency; + + SetupSuccessResponse(response); + SetupExtractRefreshContentFinder(response.Item); + + // Act + var result = await CloudTasksApiClient.CreateExtractRefreshTaskAsync( + createTaskOptions, + Cancel); + + // Assert + Assert.True(result.Success); + Assert.Equal(cloudSchedule.Frequency, result.Value.Schedule.Frequency); + Assert.Equal(ExtractRefreshContentType.DataSource, result.Value.ContentType); + Assert.Equal(contentReference.Id, result.Value.Content.Id); + } + + [Fact] + public async Task Fails_to_create_extract_refresh() + { + // Arrange + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Cloud); + var contentReference = AutoFixture.Create(); + var cloudSchedule = AutoFixture.Create(); + var createTaskOptions = new CreateExtractRefreshTaskOptions( + ExtractRefreshType.FullRefresh, + ExtractRefreshContentType.DataSource, + contentReference.Id, + cloudSchedule); + + SetupErrorResponse(error => error.Code = "400000"); + + // Act + var result = await CloudTasksApiClient.CreateExtractRefreshTaskAsync( + createTaskOptions, + Cancel); + + // Assert + Assert.False(result.Success); + Assert.Null(result.Value); + } + } + + #endregion + } + + #endregion + + #region - Server - + + public class Server + { + public abstract class ServerTasksApiClientTest : TasksApiClientTest + { + internal IServerTasksApiClient ServerTasksApiClient => TasksApiClient; + + public ServerTasksApiClientTest() + { + CurrentInstanceType = TableauInstanceType.Server; + } + + protected ServerResponses.ExtractRefreshTasksResponse CreateServerResponse(ExtractRefreshContentType contentType) + { + if (contentType == ExtractRefreshContentType.DataSource) + { + AutoFixture.Customize( + composer => composer.With(j => j.Workbook, () => null)); + } + else + { + AutoFixture.Customize( + composer => composer.With(j => j.DataSource, () => null)); + } + + return AutoFixture.CreateResponse(); + } + } + + #region - GetAllExtractRefreshTasksAsync - + + public class GetAllExtractRefreshTasksAsync : ServerTasksApiClientTest + { + [Fact] + 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()); + + SetupSuccessResponse(response); + + var result = await ServerTasksApiClient.GetAllExtractRefreshTasksAsync(Cancel); + + var actualExtractRefreshes = AssertSuccess(result); + var expectedExtractRefreshes = response.Items.ToList(); + + Assert.Equal(expectedExtractRefreshes.Count, actualExtractRefreshes.Count); + Assert.DoesNotContain(actualExtractRefreshes, item => item.ContentType == ExtractRefreshContentType.DataSource); + } + + [Fact] + public async Task Gets_datasource_extract_refreshes() + { + MockSessionProvider.SetupGet(p => p.InstanceType).Returns(TableauInstanceType.Server); + + var response = CreateServerResponse(ExtractRefreshContentType.DataSource); + + SetupExtractRefreshContentFinder(response.Items.Select(i => i.ExtractRefresh).ExceptNulls()); + + SetupSuccessResponse(response); + + var result = await ServerTasksApiClient.GetAllExtractRefreshTasksAsync(Cancel); + + var actualExtractRefreshes = AssertSuccess(result); + var expectedExtractRefreshes = response.Items.ToList(); + + Assert.Equal(expectedExtractRefreshes.Count, actualExtractRefreshes.Count); + Assert.DoesNotContain(actualExtractRefreshes, item => item.ContentType == ExtractRefreshContentType.Workbook); + } + } + + #endregion + } + + #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 new file mode 100644 index 00000000..1186a5d6 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Cloud/CloudExtractRefreshTaskTests.cs @@ -0,0 +1,77 @@ +// +// 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 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; + +using ExtractRefreshType = Tableau.Migration.Api.Rest.Models.Responses.Cloud.ExtractRefreshTasksResponse.TaskType.ExtractRefreshType; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules.Cloud +{ + public class CloudExtractRefreshTaskTests + { + public abstract class CloudExtractRefreshTaskTest : ExtractRefreshTaskTestBase + { + protected ExtractRefreshType CreateExtractRefreshResponse( + string? type = null, + ExtractRefreshContentType? contentType = null, + Guid? contentId = null, + Action? configure = null) + => CreateExtractRefreshResponse(type, contentType, contentId, configure); + + internal CloudExtractRefreshTask CreateExtractRefreshTask( + ICloudExtractRefreshType? response = null, + string? type = null, + IContentReference? content = null, + ICloudSchedule? schedule = null) + { + response ??= CreateExtractRefreshResponse(); + + return new( + response.Id, + type ?? GetRandomType(), + response.GetContentType(), + content ?? CreateContentReference(), + schedule ?? CreateCloudSchedule()); + } + } + + public class Ctor : CloudExtractRefreshTaskTest + { + [Theory, ExtractRefreshContentTypeData] + public void Initializes(ExtractRefreshContentType contentType) + { + var type = GetRandomType(); + var response = CreateExtractRefreshResponse(type, contentType); + var content = CreateContentReference(); + var schedule = CreateCloudSchedule(); + + var task = new CloudExtractRefreshTask(response.Id, type, response.GetContentType(), content, schedule); + + Assert.Same(content, task.Content); + Assert.Equal(type, task.Type); + Assert.Equal(contentType, task.ContentType); + Assert.Same(schedule, task.Schedule); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs new file mode 100644 index 00000000..e0916fc2 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ExtractRefreshTaskTestBase.cs @@ -0,0 +1,27 @@ +// +// 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 Moq; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules +{ + public abstract class ExtractRefreshTaskTestBase : ScheduleTestBase + { + protected readonly Mock MockFinderFactory = new(); + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/IFrequencyExtensionsTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/IFrequencyExtensionsTests.cs new file mode 100644 index 00000000..aaa69b67 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/IFrequencyExtensionsTests.cs @@ -0,0 +1,99 @@ +// +// 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.Collections.Immutable; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules +{ + public class IFrequencyExtensionsTests + { + public class IsCloudCompatible + { + [Theory] + [InlineData(ScheduleFrequencies.Hourly, 15, null, null, null, false)] + [InlineData(ScheduleFrequencies.Hourly, 30, null, null, null, false)] + [InlineData(ScheduleFrequencies.Hourly, 60, null, null, null, true)] + [InlineData(ScheduleFrequencies.Hourly, null, 1, null, null, true)] + [InlineData(ScheduleFrequencies.Daily, null, 2, null, null, true)] + [InlineData(ScheduleFrequencies.Hourly, null, null, WeekDays.Monday, null, true)] + [InlineData(ScheduleFrequencies.Hourly, null, null, null, "26", true)] + public void Parses( + string frequency, + int? minutes, + int? hours, + string? weekday, + string? monthDay, + bool expectedResult) + { + // Arrange + var testIntervals = ImmutableList.Create(new Interval(hours, minutes, weekday, monthDay) as IInterval); + + // Act/Assert + Assert.Equal(expectedResult, frequency.IsCloudCompatible(testIntervals)); + } + } + + public class ToCloudCompatible + { + [Theory] + [InlineData(ScheduleFrequencies.Hourly, 15)] + [InlineData(ScheduleFrequencies.Hourly, 30)] + public void Converts_unsupported( + string frequency, + int? minutes) + { + // Arrange + var testIntervals = ImmutableList.Create(new Interval(null, minutes, null, null) as IInterval); + + // Act + var cloudInterval = frequency.ToCloudCompatible(testIntervals); + + // Assert + Assert.True(frequency.IsCloudCompatible(cloudInterval)); + Assert.Single(cloudInterval); + Assert.Equal(Interval.WithHours(1), cloudInterval[0]); + } + + [Theory] + [InlineData(ScheduleFrequencies.Daily, null, 2, null, null)] + [InlineData(ScheduleFrequencies.Daily, null, 4, null, null)] + [InlineData(ScheduleFrequencies.Weekly, null, null, "Monday", null)] + [InlineData(ScheduleFrequencies.Monthly, null, null, null, "26")] + public void Does_not_convert_supported( + string frequency, + int? minutes, + int? hours, + string? weekday, + string? monthDay) + { + // Arrange + var testInterval = ImmutableList.Create(new Interval(hours, minutes, weekday, monthDay) as IInterval); + + // Act + var cloudInterval = frequency.ToCloudCompatible(testInterval); + + // Assert + Assert.True(frequency.IsCloudCompatible(cloudInterval)); + Assert.Single(cloudInterval); + Assert.Equal(testInterval[0], cloudInterval[0]); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ILoggerExtensionsTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ILoggerExtensionsTests.cs new file mode 100644 index 00000000..42859020 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ILoggerExtensionsTests.cs @@ -0,0 +1,95 @@ +// +// 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.Collections.Immutable; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules +{ + public class ILoggerExtensionsTests + { + private static readonly string LogMessage = $"Guid: {{0}}{Environment.NewLine}Original:{Environment.NewLine}{{1}}{Environment.NewLine}Updated:{Environment.NewLine}{{2}}"; + + public abstract class ILoggerExtensionsTest : AutoFixtureTestBase + { + protected readonly TestLogger Logger = new(); + } + + public class LogIntervalsChanges : ILoggerExtensionsTest + { + [Fact] + public void Logs_changes_when_intervals_differ() + { + // Arrange + var originalIntervals = new List + { + Interval.WithHours(1), + Interval.WithMinutes(1), + Interval.WithWeekday(WeekDays.Monday), + Interval.WithWeekday(WeekDays.Tuesday) + }.ToImmutableList(); + var newIntervals = new List + { + Interval.WithHours(2), + Interval.WithMinutes(15), + Interval.WithMonthDay("24") + }.ToImmutableList(); + + // Act + var result = Logger.LogIntervalsChanges( + LogMessage, + Guid.NewGuid(), + originalIntervals, + newIntervals); + + // Assert + Assert.True(result); + + var message = Assert.Single(Logger.Messages); + Assert.Equal(LogLevel.Warning, message.LogLevel); + } + + [Fact] + public void Does_not_log_changes_when_intervals_same() + { + // Arrange + var intervals = new List() + { + Interval.WithHours(1), + Interval.WithMinutes(1), + Interval.WithWeekday(WeekDays.Monday) + }; + + // Act + var result = Logger.LogIntervalsChanges( + LogMessage, + Guid.NewGuid(), + intervals.ToImmutableList(), + intervals.ToImmutableList()); + + // Asserts + Assert.False(result); + Assert.Empty(Logger.Messages); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ScheduleTestBase.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ScheduleTestBase.cs new file mode 100644 index 00000000..23a2c706 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/ScheduleTestBase.cs @@ -0,0 +1,115 @@ +// +// 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 AutoFixture; +using Moq; +using Tableau.Migration.Api.Rest; +using Tableau.Migration.Api.Rest.Models.Responses; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Content.Schedules.Server; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules +{ + public abstract class ScheduleTestBase : AutoFixtureTestBase + { + protected static IInterval CreateInterval(int? hours = null, int? minutes = null, string? weekDay = null, string? monthDay = null) + => new Interval(hours, minutes, weekDay, monthDay); + + protected static IFrequencyDetails CreateFrequencyDetails(params IInterval[] intervals) + => CreateFrequencyDetails(null, null, intervals); + + protected static IFrequencyDetails CreateFrequencyDetails(TimeOnly? startAt, TimeOnly? endAt, params IInterval[] intervals) + => new FrequencyDetails(startAt, endAt, intervals); + + protected static IFrequencyDetails CreateFrequencyDetails(DateTime startAt, TimeSpan duration, params IInterval[] intervals) + => CreateFrequencyDetails(TimeOnly.FromDateTime(startAt), TimeOnly.FromDateTime(startAt.Add(duration)), intervals); + + protected TSchedule CreateSchedule(string? frequency = null, IFrequencyDetails? frequencyDetails = null) + where TSchedule : class, ISchedule + { + var mockSchedule = Create>(m => + { + if (frequency is not null) + m.SetupGet(s => s.Frequency).Returns(frequency); + + if (frequencyDetails is not null) + m.SetupGet(s => s.FrequencyDetails).Returns(frequencyDetails); + }); + + return mockSchedule.Object; + } + + protected ICloudSchedule CreateCloudSchedule(string? frequency = null, IFrequencyDetails? frequencyDetails = null) + => CreateSchedule(frequency, frequencyDetails); + + protected IServerSchedule CreateServerSchedule(string? frequency = null, IFrequencyDetails? frequencyDetails = null) + => CreateSchedule(frequency, frequencyDetails); + + protected TExtractRefreshType CreateExtractRefreshResponse( + string? type = null, + ExtractRefreshContentType? contentType = null, + Guid? contentId = null, + Action? configure = null) + where TExtractRefreshType : IExtractRefreshType + where TWorkbook : class, IRestIdentifiable + where TDataSource : class, IRestIdentifiable + { + var response = AutoFixture.Build() + .Without(r => r.DataSource) + .Without(r => r.Workbook) + .Create(); + type ??= GetRandomType(); + contentType ??= GetRandomContentType(); + + response.Type = type; + + if (contentType is ExtractRefreshContentType.DataSource) + response.DataSource = CreateRestIdentifiable(contentId); + + if (contentType is ExtractRefreshContentType.Workbook) + response.Workbook = CreateRestIdentifiable(contentId); + + configure?.Invoke(response); + + return response; + } + + protected static string GetRandomType() + => new[] { ExtractRefreshType.FullRefresh, ExtractRefreshType.ServerIncrementalRefresh }.PickRandom(); + + protected static ExtractRefreshContentType GetRandomContentType() + => new[] { ExtractRefreshContentType.DataSource, ExtractRefreshContentType.Workbook }.PickRandom(); + + protected TIdentifiable CreateRestIdentifiable(Guid? id = null) + where TIdentifiable : class, IRestIdentifiable + => Create>(m => + { + m.CallBase = true; + m.SetupGet(w => w.Id).Returns(id ?? Create()); + }) + .Object; + + protected IContentReference CreateContentReference() => Create(); + + public class ExtractRefreshContentTypeDataAttribute() + : EnumDataAttribute(ExtractRefreshContentType.Unknown) + { } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ScheduleExtractRefreshTaskTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ScheduleExtractRefreshTaskTests.cs new file mode 100644 index 00000000..1165febd --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ScheduleExtractRefreshTaskTests.cs @@ -0,0 +1,66 @@ +// +// 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 Tableau.Migration.Api.Rest.Models.Responses.Server; +using Tableau.Migration.Content.Schedules.Server; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules.Server +{ + public class ScheduleExtractRefreshTaskTests : AutoFixtureTestBase + { + [Fact] + public void Constructor_GivenScheduleExtractsResponse_ExtractType_ShouldSetProperties() + { + // Arrange + var response = Create(); + + // Act + var scheduleExtract = new ScheduleExtractRefreshTask(response); + + // Assert + Assert.Equal(response.Id, scheduleExtract.Id); + Assert.Equal(response.Priority, scheduleExtract.Priority); + Assert.Equal(response.Type, scheduleExtract.Type); + Assert.Equal(response.Workbook?.Id, scheduleExtract.WorkbookId); + Assert.Equal(response.DataSource?.Id, scheduleExtract.DatasourceId); + } + + [Fact] + public void Test_ScheduleExtract_With_Null_Id() + { + // Arrange + var response = Create(); + response.Id = default; + + // Act & Assert + Assert.Throws(() => new ScheduleExtractRefreshTask(response)); + } + + [Fact] + public void Test_ScheduleExtract_With_Null_Type() + { + // Arrange + var response = Create(); + response.Type = null; + + // Act & Assert + Assert.Throws(() => new ScheduleExtractRefreshTask(response)); + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ScheduleExtractRefreshTasksTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ScheduleExtractRefreshTasksTests.cs new file mode 100644 index 00000000..250745dc --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ScheduleExtractRefreshTasksTests.cs @@ -0,0 +1,45 @@ +// +// 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.Linq; +using Tableau.Migration.Content.Schedules.Server; +using Xunit; +using ScheduleExtractRefreshTasksResponse = Tableau.Migration.Api.Rest.Models.Responses.Server.ScheduleExtractRefreshTasksResponse; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules.Server +{ + public class ScheduleExtractRefreshTasksTests : AutoFixtureTestBase + { + [Fact] + public void Ctor() + { + // Arrange + var scheduleId = Guid.NewGuid(); + var response = new ScheduleExtractRefreshTasksResponse + { + Items = CreateMany(3).ToArray() + }; + + // Act + var scheduleExtracts = new ScheduleExtractRefreshTasks(scheduleId, response); + + // Assert + Assert.Equal(3, scheduleExtracts.ExtractRefreshTasks.Count); + } + } +} \ No newline at end of file diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs new file mode 100644 index 00000000..f9a65cd1 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/Server/ServerExtractRefreshTaskTests.cs @@ -0,0 +1,89 @@ +// +// 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 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; +using Xunit; + +using ExtractRefreshType = Tableau.Migration.Api.Rest.Models.Responses.Server.ExtractRefreshTasksResponse.TaskType.ExtractRefreshType; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules.Server +{ + public class ServerExtractRefreshTaskTests + { + public abstract class ServerExtractRefreshTaskTest : ExtractRefreshTaskTestBase + { + protected readonly Mock MockContentCacheFactory = new(); + protected readonly Mock> MockScheduleCache = new(); + + public ServerExtractRefreshTaskTest() + { + MockContentCacheFactory + .Setup(f => f.ForContentType(true)) + .Returns(MockScheduleCache.Object); + } + + protected ExtractRefreshType CreateExtractRefreshResponse( + string? type = null, + ExtractRefreshContentType? contentType = null, + Guid? contentId = null, + Action? configure = null) + => CreateExtractRefreshResponse(type, contentType, contentId, configure); + + internal ServerExtractRefreshTask CreateExtractRefreshTask( + IServerExtractRefreshType? response = null, + string? type = null, + IContentReference? content = null, + IServerSchedule? schedule = null) + { + response ??= CreateExtractRefreshResponse(); + + return new( + response.Id, + type ?? GetRandomType(), + response.GetContentType(), + content ?? CreateContentReference(), + schedule ?? CreateServerSchedule()); + } + } + + public class Ctor : ServerExtractRefreshTaskTest + { + [Theory, ExtractRefreshContentTypeData] + public void Initializes(ExtractRefreshContentType contentType) + { + var type = GetRandomType(); + var response = CreateExtractRefreshResponse(type, contentType); + var content = CreateContentReference(); + var schedule = CreateServerSchedule(); + + var task = new ServerExtractRefreshTask(response.Id, type, response.GetContentType(), content, schedule); + + Assert.Same(content, task.Content); + Assert.Equal(type, task.Type); + Assert.Equal(contentType, task.ContentType); + Assert.Same(schedule, task.Schedule); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Schedules/VolatileCacheTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/VolatileCacheTests.cs new file mode 100644 index 00000000..6d66ba35 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Content/Schedules/VolatileCacheTests.cs @@ -0,0 +1,246 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Content.Schedules +{ + public class VolatileCacheTests + { + public abstract class VolatileCacheTest : AutoFixtureTestBase + where TKey : struct + where TContent : class + { + internal VolatileCache? Cache { get; set; } + } + + public class SingleThread : VolatileCacheTest<(ExtractRefreshContentType, Guid), ImmutableList> + { + [Fact] + public async Task EmptyList_LoadsOnce() + { + var loaded = 0; + Cache = new( + cancel => + { + loaded++; + return Task.FromResult( + new Dictionary<(ExtractRefreshContentType, Guid), ImmutableList>()); + }); + + var result1 = await Cache.GetAndRelease((ExtractRefreshContentType.DataSource,Guid.NewGuid()), Cancel); + + var result2 = await Cache.GetAndRelease((ExtractRefreshContentType.Workbook,Guid.NewGuid()), Cancel); + + Assert.Null(result1); + Assert.Null(result2); + Assert.Equal(1, loaded); + } + + [Fact] + public async Task SingleItem_LoadsAndReturnsOnce() + { + var loaded = 0; + var id = Guid.NewGuid(); + var list = CreateMany().ToImmutableList(); + Cache = new( + cancel => + { + loaded++; + return Task.FromResult( + new Dictionary<(ExtractRefreshContentType, Guid), ImmutableList> + { + [(ExtractRefreshContentType.DataSource, id)] = list + }); + }); + + + var result1 = await Cache.GetAndRelease((ExtractRefreshContentType.DataSource, id), Cancel); + + var result2 = await Cache.GetAndRelease((ExtractRefreshContentType.DataSource, id), Cancel); + + Assert.NotNull(result1); + Assert.Equal(list, result1); + Assert.Null(result2); + Assert.Equal(1, loaded); + } + + [Fact] + public async Task MultipleItems_LoadsAndReturnsOnce() + { + var loaded = 0; + var id1 = Guid.NewGuid(); + var list1 = CreateMany().ToImmutableList(); + var id2 = Guid.NewGuid(); + var list2 = CreateMany().ToImmutableList(); + var id3 = Guid.NewGuid(); + var list3 = CreateMany().ToImmutableList(); + Cache = new( + cancel => + { + loaded++; + return Task.FromResult( + new Dictionary<(ExtractRefreshContentType, Guid), ImmutableList> + { + [(ExtractRefreshContentType.DataSource, id1)] = list1, + [(ExtractRefreshContentType.Workbook, id2)] = list2, + [(ExtractRefreshContentType.DataSource, id3)] = list3, + }); + }); + + var result1 = await Cache.GetAndRelease((ExtractRefreshContentType.DataSource, id1), Cancel); + + var result2 = await Cache.GetAndRelease((ExtractRefreshContentType.Workbook, id2), Cancel); + + var emptyResult = await Cache.GetAndRelease((ExtractRefreshContentType.DataSource, Guid.NewGuid()), Cancel); + + Assert.NotNull(result1); + Assert.Equal(list1, result1); + Assert.NotNull(result2); + Assert.Equal(list2, result2); + Assert.Null(emptyResult); + Assert.Equal(1, loaded); + } + } + + public class MultiThread : VolatileCacheTest + { + [Fact] + public async Task EmptyList_LoadsOnce() + { + var loaded = 0; + Cache = new( + cancel => + { + loaded++; + return Task.FromResult( + new Dictionary()); + }); + + var totalThreads = 127; + var tasks = Enumerable + .Range(1, totalThreads) + .Select(x => Cache + .GetAndRelease( + Guid.NewGuid(), + Cancel)) + .ToList(); + var results = await Task.WhenAll(tasks); + + var notNullList = results.Where(result => result is not null).ToList(); + var nullList = results.Where(result => result is null).ToList(); + + Assert.Empty(notNullList); + Assert.Equal(totalThreads, nullList.Count); + Assert.Equal(1, loaded); + } + + [Fact] + public async Task SingleItem_LoadsAndReturnsOnce() + { + var loaded = 0; + var item = Create(); + var id = item.Id; + Cache = new( + cancel => + { + loaded++; + return Task.FromResult( + new Dictionary + { + [id] = item + }); + }); + var totalThreads = 143; + var tasks = Enumerable + .Range(1, totalThreads) + .Select(x => Cache + .GetAndRelease( + id, + Cancel)) + .ToList(); + var results = await Task.WhenAll(tasks); + + var notNullList = results.Where(result => result is not null).ToList(); + var nullList = results.Where(result => result is null).ToList(); + + Assert.Single(notNullList); + Assert.Equal(totalThreads - 1, nullList.Count); + Assert.Same(item, notNullList.First()); + Assert.Equal(1, loaded); + } + + [Fact] + public async Task MultipleItems_LoadsAndReturnsOnce() + { + var loaded = 0; + var item1 = Create(); + var id1 = item1.Id; + + var item2 = Create(); + var id2 = item2.Id; + + var item3 = Create(); + var id3 = item3.Id; + Cache = new( + cancel => + { + loaded++; + return Task.FromResult( + new Dictionary + { + [id1] = item1, + [id2] = item2, + [id3] = item3 + }); + }); + var totalThreads = 159; + var tasks = Enumerable + .Range(1, totalThreads) + .Select(x => Cache + .GetAndRelease( + x % 3 != 0 + ? x % 3 != 1 + ? Guid.NewGuid() + : id2 + : id1, + Cancel)) + .ToList(); + + var results = await Task.WhenAll(tasks); + var id1Result = results.First(result => result is not null && result.Id == id1); + var id2Result = results.First(result => result is not null && result.Id == id2); + var emptyResults = results.Where(result => result is null).ToList(); + + Assert.NotNull(id1Result); + Assert.Equal(item1, id1Result); + Assert.NotNull(id2Result); + Assert.Equal(item2, id2Result); + Assert.Equal(totalThreads - 2, emptyResults.Count); + Assert.Equal(1, loaded); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/BulkCacheTest.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/BulkCacheTest.cs new file mode 100644 index 00000000..c0e8ec33 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/BulkCacheTest.cs @@ -0,0 +1,68 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using System.Threading; +using Moq; +using Tableau.Migration.Api; +using Tableau.Migration.Api.Search; +using Tableau.Migration.Config; + +namespace Tableau.Migration.Tests.Unit.Engine.Endpoints.Search +{ + public abstract class BulkCacheTest : AutoFixtureTestBase + where TCache : BulkApiContentReferenceCache + where TContent : class, IContentReference + { + private readonly Lazy _cache; + + protected readonly Mock MockConfigReader; + protected readonly Mock MockSitesApiClient; + protected readonly Mock> MockListApiClient; + + protected TCache Cache => _cache.Value; + + protected ContentTypesOptions ContentTypesOptions { get; set; } = new ContentTypesOptions(); + + protected List EndpointContent { get; set; } + + public BulkCacheTest() + { + _cache = new(CreateCache); + + EndpointContent = CreateMany().ToList(); + ContentTypesOptions.BatchSize = EndpointContent.Count / 2; + + MockListApiClient = Freeze>>(); + MockListApiClient.Setup(x => x.GetAllAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(() => Result>.Succeeded(EndpointContent.ToImmutableList())); + + MockSitesApiClient = Freeze>(); + MockSitesApiClient.Setup(x => x.GetListApiClient()) + .Returns(MockListApiClient.Object); + + MockConfigReader = Freeze>(); + MockConfigReader.Setup(x => x.Get()) + .Returns(() => ContentTypesOptions); + } + + protected virtual TCache CreateCache() => Create(); + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CloudIncrementalRefreshTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CloudIncrementalRefreshTransformerTests.cs new file mode 100644 index 00000000..7a47e6ef --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CloudIncrementalRefreshTransformerTests.cs @@ -0,0 +1,74 @@ +// +// 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.Collections.Immutable; +using System.Threading.Tasks; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Engine.Hooks.Transformers.Default; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Engine.Hooks.Transformers.Default +{ + public class CloudIncrementalRefreshTransformerTests : AutoFixtureTestBase + { + protected readonly TestLogger Logger = new(); + protected readonly MockSharedResourcesLocalizer MockSharedResourcesLocalizer = new(); + protected readonly CloudIncrementalRefreshTransformer Transformer; + + public CloudIncrementalRefreshTransformerTests() + { + Transformer = new(MockSharedResourcesLocalizer.Object, Logger); + } + + [Fact] + public async Task Transforms_server_incremental_refresh() + { + // Arrange + var input = Create(); + input.Type = ExtractRefreshType.ServerIncrementalRefresh; + input.Schedule.FrequencyDetails.Intervals = ImmutableList.Create(Interval.WithMonthDay("10")); + input.Schedule.Frequency = ScheduleFrequencies.Monthly; + + // Act + var result = await Transformer.ExecuteAsync(input, Cancel); + + // Assert + Assert.NotNull(result); + Assert.Equal(ExtractRefreshType.CloudIncrementalRefresh, result.Type); + } + + [Fact] + public async Task Noop_full_refresh() + { + // Arrange + var input = Create(); + input.Type = ExtractRefreshType.FullRefresh; + input.Schedule.FrequencyDetails.Intervals = ImmutableList.Create(Interval.WithMonthDay("10")); + input.Schedule.Frequency = ScheduleFrequencies.Monthly; + + // Act + var result = await Transformer.ExecuteAsync(input, Cancel); + + // Assert + Assert.NotNull(result); + Assert.Equal(ExtractRefreshType.FullRefresh, result.Type); + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CloudScheduleCompatibilityTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CloudScheduleCompatibilityTransformerTests.cs new file mode 100644 index 00000000..630db6e6 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CloudScheduleCompatibilityTransformerTests.cs @@ -0,0 +1,101 @@ +// +// 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.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Engine.Hooks.Transformers.Default; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Engine.Hooks.Transformers.Default +{ + public class CloudScheduleCompatibilityTransformerTests : AutoFixtureTestBase + { + protected readonly TestLogger> Logger = new(); + protected readonly MockSharedResourcesLocalizer MockSharedResourcesLocalizer = new(); + protected readonly CloudScheduleCompatibilityTransformer Transformer; + + public CloudScheduleCompatibilityTransformerTests() + { + Transformer = new(MockSharedResourcesLocalizer.Object, Logger); + } + + [Theory] + [InlineData(ScheduleFrequencies.Monthly)] + [InlineData(ScheduleFrequencies.Daily)] + public async Task Skips_intervals_longer_than_1_hour(string frequency) + { + // Arrange + var input = Create(); + input.Type = ExtractRefreshType.FullRefresh; + input.Schedule.Frequency = frequency; + + // Act + var result = await Transformer.ExecuteAsync(input, Cancel); + + // Assert + Assert.NotNull(result); + Assert.Equal(input.Schedule.FrequencyDetails.Intervals.Count, result.Schedule.FrequencyDetails.Intervals.Count); + Assert.Empty(Logger.Messages.Where(m => m.LogLevel == LogLevel.Warning)); + } + + [Fact] + public async Task Transforms_intervals_shorter_than_1_hour() + { + // Arrange + var input = Create(); + input.Type = ExtractRefreshType.FullRefresh; + input.Schedule.FrequencyDetails.Intervals = ImmutableList.Create(Interval.WithMinutes(15)); + input.Schedule.Frequency = ScheduleFrequencies.Hourly; + + // Act + var result = await Transformer.ExecuteAsync(input, Cancel); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Schedule.FrequencyDetails.Intervals); + Assert.Equal(Interval.WithHours(1), result.Schedule.FrequencyDetails.Intervals[0]); + Assert.Single(Logger.Messages.Where(m => m.LogLevel == LogLevel.Warning)); + } + + [Fact] + public async Task Transforms_weekly_intervals_with_multiple_weekdays() + { + // Arrange + var input = Create(); + input.Type = ExtractRefreshType.FullRefresh; + input.Schedule.FrequencyDetails.Intervals = ImmutableList.Create( + Interval.WithWeekday(WeekDays.Sunday), + Interval.WithWeekday(WeekDays.Monday)); + input.Schedule.Frequency = ScheduleFrequencies.Weekly; + + // Act + var result = await Transformer.ExecuteAsync(input, Cancel); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Schedule.FrequencyDetails.Intervals); + Assert.Equal(Interval.WithWeekday(WeekDays.Sunday), result.Schedule.FrequencyDetails.Intervals[0]); + Assert.Single(Logger.Messages.Where(m => m.LogLevel == LogLevel.Warning)); + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/GroupUsersTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/GroupUsersTransformerTests.cs new file mode 100644 index 00000000..cedac514 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/GroupUsersTransformerTests.cs @@ -0,0 +1,81 @@ +// +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Tableau.Migration.Content; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Engine.Hooks.Transformers.Default; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Engine.Hooks.Transformers.Default +{ + public class GroupUsersTransformerTests + { + public abstract class GroupUsersTransformerTest : AutoFixtureTestBase + { + protected readonly Mock MockDestinationContentReferenceFinderFactory = new(); + protected readonly Mock> MockLogger = new(); + protected readonly MockSharedResourcesLocalizer MockSharedResourcesLocalizer = new(); + protected readonly Mock> MockUserContentFinder = new(); + + protected readonly GroupUsersTransformer Transformer; + + public GroupUsersTransformerTest() + { + MockDestinationContentReferenceFinderFactory.Setup(p => p.ForDestinationContentType()).Returns(MockUserContentFinder.Object); + + Transformer = new(MockDestinationContentReferenceFinderFactory.Object, MockSharedResourcesLocalizer.Object, MockLogger.Object); + } + } + + public class ExecuteAsync : GroupUsersTransformerTest + { + [Fact] + public async Task Returns_the_same_object() + { + var group = Create(); + + var result = await Transformer.TransformAsync(group, Cancel); + + Assert.NotNull(result); + Assert.Same(group, result); + MockLogger.VerifyWarnings(Times.Exactly(group.Users.Count)); + } + + [Fact] + public async Task Returns_destination_user_when_found() + { + var group = Create(); + var sourceUser = Create(); + var destinationUser = Create(); + group.Users.Add(new GroupUser(sourceUser)); + + MockUserContentFinder.Setup(f => f.FindBySourceLocationAsync(sourceUser.Location, Cancel)).ReturnsAsync(destinationUser); + + var result = await Transformer.TransformAsync(group, Cancel); + + Assert.NotNull(result); + Assert.Same(group, result); + MockLogger.VerifyWarnings(Times.Exactly(group.Users.Count - 1)); + Assert.Same(destinationUser, result.Users.Last().User); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedReferenceExtractRefreshTaskTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedReferenceExtractRefreshTaskTransformerTests.cs new file mode 100644 index 00000000..ebde0544 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedReferenceExtractRefreshTaskTransformerTests.cs @@ -0,0 +1,105 @@ +// +// 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.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Schedules; +using Tableau.Migration.Content.Schedules.Cloud; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Engine.Hooks.Transformers.Default; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Engine.Hooks.Transformers.Default +{ + public class MappedReferenceExtractRefreshTaskTransformerTests + { + public abstract class MappedReferenceExtractRefreshTaskTransformerTest : AutoFixtureTestBase + { + protected readonly Mock MockDestinationContentReferenceFinderFactory = new(); + protected readonly Mock> MockLogger = new(); + protected readonly MockSharedResourcesLocalizer MockSharedResourcesLocalizer = new(); + protected readonly Mock> MockWorkbookContentFinder = new(); + protected readonly Mock> MockDatasourceContentFinder = new(); + + protected readonly MappedReferenceExtractRefreshTaskTransformer Transformer; + + public MappedReferenceExtractRefreshTaskTransformerTest() + { + MockDestinationContentReferenceFinderFactory.Setup(p => p.ForDestinationContentType()).Returns(MockWorkbookContentFinder.Object); + MockDestinationContentReferenceFinderFactory.Setup(p => p.ForDestinationContentType()).Returns(MockDatasourceContentFinder.Object); + + Transformer = new(MockDestinationContentReferenceFinderFactory.Object, MockSharedResourcesLocalizer.Object, MockLogger.Object); + } + } + + public class ExecuteAsync : MappedReferenceExtractRefreshTaskTransformerTest + { + [Fact] + public async Task Returns_the_same_object() + { + var extractRefreshTask = Create(); + + var result = await Transformer.TransformAsync(extractRefreshTask, Cancel); + + Assert.NotNull(result); + Assert.Same(extractRefreshTask, result); + MockLogger.VerifyWarnings(Times.Once); + } + + [Fact] + public async Task Returns_destination_workbook_when_found() + { + var extractRefreshTask = Create(); + var sourceWorkbook = Create(); + var destinationWorkbook = Create(); + extractRefreshTask.ContentType = ExtractRefreshContentType.Workbook; + extractRefreshTask.Content = sourceWorkbook; + + MockWorkbookContentFinder.Setup(f => f.FindBySourceIdAsync(sourceWorkbook.Id, Cancel)).ReturnsAsync(destinationWorkbook); + + var result = await Transformer.TransformAsync(extractRefreshTask, Cancel); + + Assert.NotNull(result); + Assert.Same(extractRefreshTask, result); + MockLogger.VerifyWarnings(Times.Never); + Assert.Same(destinationWorkbook, result.Content); + } + + [Fact] + public async Task Returns_destination_datasource_when_found() + { + var extractRefreshTask = Create(); + var sourceDataSource = Create(); + var destinationDataSource = Create(); + extractRefreshTask.ContentType = ExtractRefreshContentType.DataSource; + extractRefreshTask.Content = sourceDataSource; + + MockDatasourceContentFinder.Setup(f => f.FindBySourceIdAsync(sourceDataSource.Id, Cancel)).ReturnsAsync(destinationDataSource); + + var result = await Transformer.TransformAsync(extractRefreshTask, Cancel); + + Assert.NotNull(result); + Assert.Same(extractRefreshTask, result); + MockLogger.VerifyWarnings(Times.Never); + Assert.Same(destinationDataSource, result.Content); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/ISyncMigrationActionCompletedHookTests.cs b/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/ISyncMigrationActionCompletedHookTests.cs new file mode 100644 index 00000000..ad2981c2 --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/ISyncMigrationActionCompletedHookTests.cs @@ -0,0 +1,54 @@ +// +// 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.Threading.Tasks; +using Moq; +using Tableau.Migration.Engine.Actions; +using Tableau.Migration.Engine.Hooks; +using Tableau.Migration.Interop.Hooks; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Interop.Hooks.Transformers +{ + public class ISyncMigrationActionCompletedHookTests + { + public class ExecuteAsync : AutoFixtureTestBase + { + public class TestImplementation : ISyncMigrationActionCompletedHook + { + public virtual IMigrationActionResult? Execute(IMigrationActionResult ctx) => ctx; + } + + [Fact] + public async Task CallsExecuteAsync() + { + var mockTransformer = new Mock() + { + CallBase = true + }; + + var ctx = Create(); + + var result = await ((IMigrationHook)mockTransformer.Object).ExecuteAsync(ctx, Cancel); + + Assert.Same(ctx, result); + + mockTransformer.Verify(x => x.Execute(ctx), Times.Once); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/ISyncMigrationHookTests.cs b/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/ISyncMigrationHookTests.cs new file mode 100644 index 00000000..3d0fbf5f --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/ISyncMigrationHookTests.cs @@ -0,0 +1,53 @@ +// +// 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.Threading.Tasks; +using Moq; +using Tableau.Migration.Engine.Hooks; +using Tableau.Migration.Interop.Hooks; +using Xunit; + +namespace Tableau.Migration.Tests.Unit.Interop.Hooks +{ + public class ISyncMigrationHookTests + { + public class ExecuteAsync : AutoFixtureTestBase + { + public class TestImplementation : ISyncMigrationHook + { + public virtual string? Execute(string ctx) => ctx; + } + + [Fact] + public async Task CallsExecuteAsync() + { + var mockTransformer = new Mock() + { + CallBase = true + }; + + var ctx = Create(); + + var result = await ((IMigrationHook)mockTransformer.Object).ExecuteAsync(ctx, Cancel); + + Assert.Same(ctx, result); + + mockTransformer.Verify(x => x.Execute(ctx), Times.Once); + } + } + } +}