diff --git a/Mailtrap.sln b/Mailtrap.sln index 0631c09c..496723da 100644 --- a/Mailtrap.sln +++ b/Mailtrap.sln @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mailtrap.Example.Factory", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.Contact", "examples\Mailtrap.Example.Contact\Mailtrap.Example.Contact.csproj", "{3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.ContactImports", "examples\Mailtrap.Example.ContactImports\Mailtrap.Example.ContactImports.csproj", "{E7B8C1F2-9A3D-4C2E-8B7A-6D2F3A1E4B5C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -167,6 +169,10 @@ Global {3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64}.Release|Any CPU.ActiveCfg = Release|Any CPU {3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64}.Release|Any CPU.Build.0 = Release|Any CPU + {E7B8C1F2-9A3D-4C2E-8B7A-6D2F3A1E4B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7B8C1F2-9A3D-4C2E-8B7A-6D2F3A1E4B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7B8C1F2-9A3D-4C2E-8B7A-6D2F3A1E4B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7B8C1F2-9A3D-4C2E-8B7A-6D2F3A1E4B5C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -190,6 +196,7 @@ Global {F6357CAB-06C6-4603-99E7-1EDB79ACA8E8} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB} {AB1237F4-D074-4D3C-9AE4-6794BD30EA71} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB} {3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB} + {E7B8C1F2-9A3D-4C2E-8B7A-6D2F3A1E4B5C} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0FF614CC-FEBC-4C66-B3FC-FCB73EE511D7} diff --git a/examples/Mailtrap.Example.ContactImports/Mailtrap.Example.ContactImports.csproj b/examples/Mailtrap.Example.ContactImports/Mailtrap.Example.ContactImports.csproj new file mode 100644 index 00000000..6f9ac7de --- /dev/null +++ b/examples/Mailtrap.Example.ContactImports/Mailtrap.Example.ContactImports.csproj @@ -0,0 +1,11 @@ + + + + + + + + PreserveNewest + + + diff --git a/examples/Mailtrap.Example.ContactImports/Program.cs b/examples/Mailtrap.Example.ContactImports/Program.cs new file mode 100644 index 00000000..78e0b905 --- /dev/null +++ b/examples/Mailtrap.Example.ContactImports/Program.cs @@ -0,0 +1,69 @@ +using Mailtrap; +using Mailtrap.Accounts; +using Mailtrap.Contacts; +using Mailtrap.Contacts.Requests; +using Mailtrap.ContactImports; +using Mailtrap.ContactImports.Models; +using Mailtrap.ContactImports.Requests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + + + +HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(args); + +IConfigurationSection config = hostBuilder.Configuration.GetSection("Mailtrap"); + +hostBuilder.Services.AddMailtrapClient(config); + +using IHost host = hostBuilder.Build(); + +ILogger logger = host.Services.GetRequiredService>(); +IMailtrapClient mailtrapClient = host.Services.GetRequiredService(); + +try +{ + var accountId = 12345; + IAccountResource accountResource = mailtrapClient.Account(accountId); + + // Get resource for contacts collection + IContactCollectionResource contactsResource = accountResource.Contacts(); + + //Get resource for contact imports collection + IContactsImportCollectionResource contactsImportsResource = contactsResource.Imports(); + + // Prepare list of contacts to import + var contactImportList = new List + { + new("alice@mailtrap.io"), + new("bob@mailtrap.io"), + new("charlie@mailtrap.io"), + }; + + // Create contacts import request + var importRequest = new ContactsImportRequest(contactImportList); + + // Import contacts in bulk + ContactsImport importResponse = await contactsImportsResource.Create(importRequest); + logger.LogInformation("Created contact import: {Import}", importResponse); + + // Get resource for specific contact import + IContactsImportResource contactsImportResource = contactsResource.Import(importResponse.Id); + + // Get details of specific contact import + ContactsImport contactsImportDetails = await contactsImportResource.GetDetails(); + logger.LogInformation("Contacts Import Details: {Details}", contactsImportDetails); + + if (contactsImportDetails.Status == ContactsImportStatus.Failed) + { + logger.LogWarning("Import failed!"); + } +} +catch (Exception ex) +{ + logger.LogError(ex, "An error occurred during API call."); + Environment.FailFast(ex.Message); + throw; +} diff --git a/examples/Mailtrap.Example.ContactImports/Properties/launchSettings.json b/examples/Mailtrap.Example.ContactImports/Properties/launchSettings.json new file mode 100644 index 00000000..b8daf491 --- /dev/null +++ b/examples/Mailtrap.Example.ContactImports/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Project": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Mailtrap.Example.ContactImports/appsettings.json b/examples/Mailtrap.Example.ContactImports/appsettings.json new file mode 100644 index 00000000..7cf4089d --- /dev/null +++ b/examples/Mailtrap.Example.ContactImports/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Warning", + "Microsoft": "Warning" + }, + "Debug": { + "LogLevel": { + "Default": "Debug" + } + } + }, + "Mailtrap": { + "ApiToken": "" + } +} diff --git a/src/Mailtrap.Abstractions/ContactImports/IContactsImportCollectionResource.cs b/src/Mailtrap.Abstractions/ContactImports/IContactsImportCollectionResource.cs new file mode 100644 index 00000000..636e22e9 --- /dev/null +++ b/src/Mailtrap.Abstractions/ContactImports/IContactsImportCollectionResource.cs @@ -0,0 +1,29 @@ +namespace Mailtrap.ContactImports; + +/// +/// Represents Contact imports collection resource. +/// +public interface IContactsImportCollectionResource : IRestResource +{ + /// + /// Import contacts in bulk with support for custom fields and list management. + /// Existing contacts with matching email addresses will be updated automatically. + /// You can import up to 50,000 contacts per request. + /// The import process runs asynchronously - use the returned + /// import ID to check the status and results. + /// List all contacts with details into . + /// + /// + /// + /// Request containing contact list for import. + /// + /// + /// + /// Token to control operation cancellation. + /// + /// + /// + /// Contact import id and status. + /// + public Task Create(ContactsImportRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Mailtrap.Abstractions/ContactImports/IContactsImportResource.cs b/src/Mailtrap.Abstractions/ContactImports/IContactsImportResource.cs new file mode 100644 index 00000000..0d1b4305 --- /dev/null +++ b/src/Mailtrap.Abstractions/ContactImports/IContactsImportResource.cs @@ -0,0 +1,20 @@ +namespace Mailtrap.ContactImports; + +/// +/// Represents Contact import resource. +/// +public interface IContactsImportResource : IRestResource +{ + /// + /// Gets details of the contact import, represented by the current resource instance. + /// + /// + /// + /// Token to control operation cancellation. + /// + /// + /// + /// Requested contact import details. + /// + public Task GetDetails(CancellationToken cancellationToken = default); +} diff --git a/src/Mailtrap.Abstractions/ContactImports/Models/ContactsImport.cs b/src/Mailtrap.Abstractions/ContactImports/Models/ContactsImport.cs new file mode 100644 index 00000000..76d04832 --- /dev/null +++ b/src/Mailtrap.Abstractions/ContactImports/Models/ContactsImport.cs @@ -0,0 +1,63 @@ +namespace Mailtrap.ContactImports.Models; + +/// +/// Generic response object for contact imports operations. +/// +public record ContactsImport +{ + /// + /// Gets created contact imports identifier. + /// + /// + /// + /// Contact imports identifier. + /// + [JsonPropertyName("id")] + [JsonPropertyOrder(1)] + [JsonRequired] + public long Id { get; set; } + + /// + /// Gets contact imports status. + /// + /// + /// + /// Contact imports status. + /// + [JsonPropertyName("status")] + [JsonPropertyOrder(2)] + public ContactsImportStatus Status { get; set; } = ContactsImportStatus.Unknown; + + /// + /// Gets count of created contacts. + /// + /// + /// + /// Count of created contacts. + /// + [JsonPropertyName("created_contacts_count")] + [JsonPropertyOrder(3)] + public long? CreatedContactsCount { get; set; } + + /// + /// Gets count of updated contacts. + /// + /// + /// + /// Count of updated contacts. + /// + [JsonPropertyName("updated_contacts_count")] + [JsonPropertyOrder(4)] + public long? UpdatedContactsCount { get; set; } + + /// + /// Gets count of contacts over limit. + /// + /// + /// + /// Count of contacts over limit. + /// + [JsonPropertyName("contacts_over_limit_count")] + [JsonPropertyOrder(5)] + public long? ContactsOverLimitCount { get; set; } +} diff --git a/src/Mailtrap.Abstractions/ContactImports/Models/ContactsImportStatus.cs b/src/Mailtrap.Abstractions/ContactImports/Models/ContactsImportStatus.cs new file mode 100644 index 00000000..7f8c590e --- /dev/null +++ b/src/Mailtrap.Abstractions/ContactImports/Models/ContactsImportStatus.cs @@ -0,0 +1,44 @@ +namespace Mailtrap.ContactImports.Models; + + +/// +/// Represents status of the contact imports. +/// +public sealed record ContactsImportStatus : StringEnum +{ + /// + /// Gets the value representing "created" status. + /// + /// + /// + /// Represents "created" status. + /// + public static ContactsImportStatus Created { get; } = Define("created"); + + /// + /// Gets the value representing "started" status. + /// + /// + /// + /// Represents "started" status. + /// + public static ContactsImportStatus Started { get; } = Define("started"); + + /// + /// Gets the value representing "finished" status. + /// + /// + /// + /// Represents "finished" status. + /// + public static ContactsImportStatus Finished { get; } = Define("finished"); + + /// + /// Gets the value representing "failed" status. + /// + /// + /// + /// Represents "failed" status. + /// + public static ContactsImportStatus Failed { get; } = Define("failed"); +} diff --git a/src/Mailtrap.Abstractions/ContactImports/Requests/ContactsImportRequest.cs b/src/Mailtrap.Abstractions/ContactImports/Requests/ContactsImportRequest.cs new file mode 100644 index 00000000..8e8952dc --- /dev/null +++ b/src/Mailtrap.Abstractions/ContactImports/Requests/ContactsImportRequest.cs @@ -0,0 +1,61 @@ +namespace Mailtrap.ContactImports.Requests; + +/// +/// Generic request object for contact CRUD operations. +/// +public record ContactsImportRequest : IValidatable +{ + /// + /// Gets contact collection for import. + /// + /// + /// + /// Contact collection for import. + /// + [JsonPropertyName("contacts")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public IList Contacts { get; } = []; + + /// + /// Primary instance constructor. + /// + /// + /// + /// Collection of contacts to import. + /// + /// + /// /// + /// Each contact in the must include a valid email. + /// Size and item-level constraints are validated by . + /// + /// + /// + /// When is or empty. + /// + /// + /// Use to ensure the count is within the allowed range + /// (currently 1–50,000) and each contact satisfies per-item rules. + /// + public ContactsImportRequest(IEnumerable contacts) + { + Ensure.NotNullOrEmpty(contacts, nameof(contacts)); + + // Defensive copy to prevent post-ctor mutation. + Contacts = contacts is List list + ? new List(list) // defensive copy when already a List + : new List(contacts); // otherwise enumerate once + } + + /// + /// Parameterless instance constructor for serializers. + /// + public ContactsImportRequest() { } + + /// + public ValidationResult Validate() + { + return ContactsImportRequestValidator.Instance + .Validate(this) + .ToMailtrapValidationResult(); + } +} diff --git a/src/Mailtrap.Abstractions/ContactImports/Validators/ContactsImportRequestValidator.cs b/src/Mailtrap.Abstractions/ContactImports/Validators/ContactsImportRequestValidator.cs new file mode 100644 index 00000000..5a4d622f --- /dev/null +++ b/src/Mailtrap.Abstractions/ContactImports/Validators/ContactsImportRequestValidator.cs @@ -0,0 +1,31 @@ +namespace Mailtrap.ContactImports.Validators; + + +/// +/// Validator for Create/Update contact requests.
+/// Ensures contact's email is not empty and length is within the allowed range. +///
+internal sealed class ContactsImportRequestValidator : AbstractValidator +{ + public const int MaxContactsPerRequest = 50_000; + public const int MinContactsPerRequest = 1; + /// + /// Static validator instance for reuse. + /// + public static ContactsImportRequestValidator Instance { get; } = new(); + + /// + /// Primary constructor. + /// + public ContactsImportRequestValidator() + { + RuleFor(r => r.Contacts) + .Cascade(CascadeMode.Stop) + .NotEmpty() + .Must(list => list != null && list.Count is >= MinContactsPerRequest and <= MaxContactsPerRequest); + + RuleForEach(r => r.Contacts) + .NotNull() + .SetValidator(ContactRequestValidator.Instance); + } +} diff --git a/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs b/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs index 44ff8ea5..3a796b06 100644 --- a/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs +++ b/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs @@ -5,6 +5,32 @@ namespace Mailtrap.Contacts; /// public interface IContactCollectionResource : IRestResource { + /// + /// Gets contacts import collection resource for the account, represented by this resource instance. + /// + /// + /// + /// for the account, represented by this resource instance. + /// + public IContactsImportCollectionResource Imports(); + + /// + /// Gets resource for a specific contacts import identified by . + /// + /// + /// + /// Unique Contact Import ID to get resource for. + /// + /// + /// + /// for the contacts import with the specified ID. + /// + /// + /// + /// When is less than or equal to zero. + /// + public IContactsImportResource Import(long importId); + /// /// Gets contacts. /// diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/ContactImportRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/ContactImportRequest.cs new file mode 100644 index 00000000..29ce577a --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Requests/ContactImportRequest.cs @@ -0,0 +1,37 @@ +namespace Mailtrap.Contacts.Requests; + +/// +/// Request object for importing a contact. +/// +public record ContactImportRequest : ContactRequest +{ + /// + /// Gets contact list IDs to include. + /// + /// + /// + /// Contact list IDs to include. + /// + [JsonPropertyName("list_ids_included")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public IList ListIdsIncluded { get; } = []; + + /// + /// Gets contact list IDs to exclude. + /// + /// + /// + /// Contact list IDs to exclude. + /// + [JsonPropertyName("list_ids_excluded")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public IList ListIdsExcluded { get; } = []; + + /// + public ContactImportRequest(string email) : base(email) { } + + /// + /// Parameterless instance constructor for serializers. + /// + public ContactImportRequest() { } +} diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs index 97f64c68..c272909f 100644 --- a/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs +++ b/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs @@ -24,6 +24,7 @@ public record ContactRequest : IValidatable /// Contact fields. /// [JsonPropertyName("fields")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] public IDictionary Fields { get; } = new Dictionary(); /// @@ -48,6 +49,11 @@ public ContactRequest(string email) Email = email; } + /// + /// Parameterless instance constructor for serializers. + /// + public ContactRequest() { Email = string.Empty; } + /// public ValidationResult Validate() diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs index 4744aa91..1ebc2ef5 100644 --- a/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs +++ b/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs @@ -17,4 +17,9 @@ public sealed record CreateContactRequest : ContactRequest /// public CreateContactRequest(string email) : base(email) { } + + /// + /// Parameterless instance constructor for serializers. + /// + public CreateContactRequest() { } } diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs index 06cfc4bb..695c1379 100644 --- a/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs +++ b/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs @@ -3,28 +3,8 @@ namespace Mailtrap.Contacts.Requests; /// /// Request object for updating a contact. /// -public sealed record UpdateContactRequest : ContactRequest +public sealed record UpdateContactRequest : ContactImportRequest { - /// - /// Gets contact list IDs to include. - /// - /// - /// - /// Contact list IDs to include. - /// - [JsonPropertyName("list_ids_included")] - public IList ListIdsIncluded { get; } = []; - - /// - /// Gets contact list IDs to exclude. - /// - /// - /// - /// Contact list IDs to exclude. - /// - [JsonPropertyName("list_ids_excluded")] - public IList ListIdsExcluded { get; } = []; - /// /// Gets contact "unsubscribed" status. /// @@ -37,4 +17,9 @@ public sealed record UpdateContactRequest : ContactRequest /// public UpdateContactRequest(string email) : base(email) { } + + /// + /// Parameterless instance constructor for serializers. + /// + public UpdateContactRequest() { } } diff --git a/src/Mailtrap.Abstractions/Core/Extensions/Ensure.cs b/src/Mailtrap.Abstractions/Core/Extensions/Ensure.cs index 5648d06a..b987cd2b 100644 --- a/src/Mailtrap.Abstractions/Core/Extensions/Ensure.cs +++ b/src/Mailtrap.Abstractions/Core/Extensions/Ensure.cs @@ -2,7 +2,7 @@ /// -/// +/// /// /// A set of helper methods for input validation. /// @@ -11,7 +11,7 @@ public static class Ensure /// /// Ensures provided is not null. /// - /// + /// /// /// When is . /// @@ -35,7 +35,7 @@ public static void NotNull(T? paramValue, string paramName, string? message = /// /// Ensures provided string is not null or empty string. /// - /// + /// /// /// When is or . /// @@ -56,10 +56,34 @@ public static void NotNullOrEmpty(string? paramValue, string paramName, string? } } + /// + /// Ensures provided collection is not null or empty. + /// + /// + /// + /// When is or empty. + /// + public static void NotNullOrEmpty(IEnumerable? paramValue, string paramName, string? message = default) + { + if (paramValue is not null && paramValue.Any()) + { + return; + } + + if (message is null) + { + throw new ArgumentNullException(paramName); + } + else + { + throw new ArgumentNullException(paramName, message); + } + } + /// /// Ensures provided is greater than zero. /// - /// + /// /// /// When is equal or less than zero. /// diff --git a/src/Mailtrap.Abstractions/GlobalSuppressions.cs b/src/Mailtrap.Abstractions/GlobalSuppressions.cs new file mode 100644 index 00000000..97cc60fe --- /dev/null +++ b/src/Mailtrap.Abstractions/GlobalSuppressions.cs @@ -0,0 +1,7 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + + +[assembly: SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Contacts Imports collection nested to Contacts should be named Imports", Scope = "member", Target = "~M:Mailtrap.Contacts.IContactCollectionResource.Imports~Mailtrap.ContactImports.IContactsImportCollectionResource")] diff --git a/src/Mailtrap.Abstractions/GlobalUsings.cs b/src/Mailtrap.Abstractions/GlobalUsings.cs index 28d0de4e..50aaff48 100644 --- a/src/Mailtrap.Abstractions/GlobalUsings.cs +++ b/src/Mailtrap.Abstractions/GlobalUsings.cs @@ -27,6 +27,10 @@ global using Mailtrap.Contacts.Responses; global using Mailtrap.Contacts.Converters; global using Mailtrap.Contacts.Validators; +global using Mailtrap.ContactImports; +global using Mailtrap.ContactImports.Models; +global using Mailtrap.ContactImports.Requests; +global using Mailtrap.ContactImports.Validators; global using Mailtrap.Emails; global using Mailtrap.Emails.Models; global using Mailtrap.Emails.Requests; diff --git a/src/Mailtrap/ContactImports/ContactsImportCollectionResource.cs b/src/Mailtrap/ContactImports/ContactsImportCollectionResource.cs new file mode 100644 index 00000000..39faa889 --- /dev/null +++ b/src/Mailtrap/ContactImports/ContactsImportCollectionResource.cs @@ -0,0 +1,13 @@ +namespace Mailtrap.ContactImports; + +/// +/// Implementation of Contact Imports Collection resource. +/// +internal sealed class ContactsImportCollectionResource : RestResource, IContactsImportCollectionResource +{ + public ContactsImportCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri) + : base(restResourceCommandFactory, resourceUri) { } + + public async Task Create(ContactsImportRequest request, CancellationToken cancellationToken = default) + => await Create(request, cancellationToken).ConfigureAwait(false); +} diff --git a/src/Mailtrap/ContactImports/ContactsImportResource.cs b/src/Mailtrap/ContactImports/ContactsImportResource.cs new file mode 100644 index 00000000..6b36ed47 --- /dev/null +++ b/src/Mailtrap/ContactImports/ContactsImportResource.cs @@ -0,0 +1,13 @@ +namespace Mailtrap.ContactImports; + +/// +/// Implementation of Contact Imports API operations. +/// +internal sealed class ContactsImportResource : RestResource, IContactsImportResource +{ + public ContactsImportResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri) + : base(restResourceCommandFactory, resourceUri) { } + + public async Task GetDetails(CancellationToken cancellationToken = default) + => await Get(cancellationToken).ConfigureAwait(false); +} diff --git a/src/Mailtrap/Contacts/ContactCollectionResource.cs b/src/Mailtrap/Contacts/ContactCollectionResource.cs index 3e28a867..3c217d23 100644 --- a/src/Mailtrap/Contacts/ContactCollectionResource.cs +++ b/src/Mailtrap/Contacts/ContactCollectionResource.cs @@ -7,9 +7,16 @@ namespace Mailtrap.Contacts; /// internal sealed class ContactCollectionResource : RestResource, IContactCollectionResource { + private const string ImportsSegment = "imports"; + public ContactCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri) : base(restResourceCommandFactory, resourceUri) { } + public IContactsImportCollectionResource Imports() + => new ContactsImportCollectionResource(RestResourceCommandFactory, ResourceUri.Append(ImportsSegment)); + + public IContactsImportResource Import(long importId) + => new ContactsImportResource(RestResourceCommandFactory, ResourceUri.Append(ImportsSegment).Append(importId)); public async Task> GetAll(CancellationToken cancellationToken = default) => await GetList(cancellationToken).ConfigureAwait(false); diff --git a/src/Mailtrap/GlobalUsings.cs b/src/Mailtrap/GlobalUsings.cs index a1cc70e3..322317ec 100644 --- a/src/Mailtrap/GlobalUsings.cs +++ b/src/Mailtrap/GlobalUsings.cs @@ -32,6 +32,9 @@ global using Mailtrap.Contacts.Models; global using Mailtrap.Contacts.Requests; global using Mailtrap.Contacts.Validators; +global using Mailtrap.ContactImports; +global using Mailtrap.ContactImports.Models; +global using Mailtrap.ContactImports.Requests; global using Mailtrap.Emails; global using Mailtrap.Emails.Models; global using Mailtrap.Emails.Requests; diff --git a/tests/Mailtrap.IntegrationTests/ContactImports/ContactImportsIntegrationTests.cs b/tests/Mailtrap.IntegrationTests/ContactImports/ContactImportsIntegrationTests.cs new file mode 100644 index 00000000..21e4522d --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/ContactImports/ContactImportsIntegrationTests.cs @@ -0,0 +1,262 @@ +namespace Mailtrap.IntegrationTests.ContactImports; + + +[TestFixture] +internal sealed class ContactImportsIntegrationTests +{ + private const string Feature = "ContactImports"; + + private readonly long _accountId; + private readonly Uri _resourceUri = null!; + private readonly MailtrapClientOptions _clientConfig = null!; + private readonly JsonSerializerOptions _jsonSerializerOptions = null!; + + + public ContactImportsIntegrationTests() + { + var random = TestContext.CurrentContext.Random; + + _accountId = random.NextLong(); + _resourceUri = EndpointsTestConstants.ApiDefaultUrl + .Append( + UrlSegmentsTestConstants.ApiRootSegment, + UrlSegmentsTestConstants.AccountsSegment) + .Append(_accountId) + .Append(UrlSegmentsTestConstants.ContactsSegment) + .Append(UrlSegmentsTestConstants.ImportsSegment); + + var token = random.GetString(); + _clientConfig = new MailtrapClientOptions(token); + _jsonSerializerOptions = _clientConfig.ToJsonSerializerOptions(); + } + + [Test] + public async Task Create_Success() + { + // Arrange + var httpMethod = HttpMethod.Post; + var requestUri = _resourceUri.AbsoluteUri; + + var fileName = TestContext.CurrentContext.Test.MethodName; + + var requestContent = await Feature.LoadFileToString(fileName + "_Request"); + var request = JsonSerializer.Deserialize(requestContent, _jsonSerializerOptions); + request.Should().NotBeNull(); + + using var responseContent = await Feature.LoadFileToStringContent(fileName + "_Response"); + var expectedResponse = await DeserializeStringContentAsync(responseContent); + + using var mockHttp = new MockHttpMessageHandler(); + mockHttp + .Expect(httpMethod, requestUri) + .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}") + .WithHeaders("Accept", MimeTypes.Application.Json) + .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString()) + .WithJsonContent(request, _jsonSerializerOptions) + .Respond(HttpStatusCode.Created, responseContent); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddMailtrapClient(_clientConfig) + .ConfigurePrimaryHttpMessageHandler(() => mockHttp); + + using var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // Act + var result = await client + .Account(_accountId) + .Contacts() + .Imports() + .Create(request) + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + result.Should().BeEquivalentTo(expectedResponse); + } + + [Test] + public async Task Create_ShouldFailValidation_WhenProvidedCollectionSizeIsInvalid([Values(0, 50001)] int length) + { + // Arrange + var httpMethod = HttpMethod.Post; + var requestUri = _resourceUri.AbsoluteUri; + + var contacts = new List(length); + for (var i = 0; i < length; i++) + { + contacts.Add(new ContactImportRequest(TestContext.CurrentContext.Random.NextEmail())); + } + var request = length == 0 ? new ContactsImportRequest() : new ContactsImportRequest(contacts); + using var responseContent = await Feature.LoadFileToStringContent(); + + using var mockHttp = new MockHttpMessageHandler(); + var mockedRequest = mockHttp + .Expect(httpMethod, requestUri) + .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}") + .WithHeaders("Accept", MimeTypes.Application.Json) + .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString()) + .WithJsonContent(request, _jsonSerializerOptions) + .Respond(HttpStatusCode.UnprocessableContent, responseContent); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddMailtrapClient(_clientConfig) + .ConfigurePrimaryHttpMessageHandler(() => mockHttp); + + using var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // Act + var act = () => client + .Account(_accountId) + .Contacts() + .Imports() + .Create(request); + + // Assert + await act.Should().ThrowAsync(); + + mockHttp.GetMatchCount(mockedRequest).Should().Be(0); + } + + [Test] + public async Task Create_ShouldFailValidation_WhenProvidedCollectionContainsNull() + { + // Arrange + var httpMethod = HttpMethod.Post; + var requestUri = _resourceUri.AbsoluteUri; + + var contacts = new List() + { + new(TestContext.CurrentContext.Random.NextEmail()), + null! + }; + + var request = new ContactsImportRequest(contacts); + + using var mockHttp = new MockHttpMessageHandler(); + var mockedRequest = mockHttp + .Expect(httpMethod, requestUri) + .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}") + .WithHeaders("Accept", MimeTypes.Application.Json) + .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString()) + .WithJsonContent(request, _jsonSerializerOptions) + .Respond(HttpStatusCode.UnprocessableContent); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddMailtrapClient(_clientConfig) + .ConfigurePrimaryHttpMessageHandler(() => mockHttp); + + using var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // Act + var act = () => client + .Account(_accountId) + .Contacts() + .Imports() + .Create(request); + + // Assert + await act.Should().ThrowAsync(); + + mockHttp.GetMatchCount(mockedRequest).Should().Be(0); + } + + [Test] + public async Task Create_ShouldFailValidation_WhenProvidedCollectionContainsInvalidRecord([Values(1, 101)] int emailLength) + { + // Arrange + var httpMethod = HttpMethod.Post; + var requestUri = _resourceUri.AbsoluteUri; + + var contacts = new List() + { + new(TestContext.CurrentContext.Random.NextEmail()), + new(TestContext.CurrentContext.Random.NextEmail(emailLength)), + }; + + var request = new ContactsImportRequest(contacts); + + using var mockHttp = new MockHttpMessageHandler(); + var mockedRequest = mockHttp + .Expect(httpMethod, requestUri) + .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}") + .WithHeaders("Accept", MimeTypes.Application.Json) + .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString()) + .WithJsonContent(request, _jsonSerializerOptions) + .Respond(HttpStatusCode.UnprocessableContent); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddMailtrapClient(_clientConfig) + .ConfigurePrimaryHttpMessageHandler(() => mockHttp); + + using var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // Act + var act = () => client + .Account(_accountId) + .Contacts() + .Imports() + .Create(request); + + // Assert + await act.Should().ThrowAsync(); + + mockHttp.GetMatchCount(mockedRequest).Should().Be(0); + } + + [Test] + public async Task GetDetails_Success() + { + // Arrange + var httpMethod = HttpMethod.Get; + var importId = TestContext.CurrentContext.Random.NextLong(); + var requestUri = _resourceUri.Append(importId).AbsoluteUri; + + using var responseContent = await Feature.LoadFileToStringContent(); + var expectedResponse = await DeserializeStringContentAsync(responseContent); + + using var mockHttp = new MockHttpMessageHandler(); + mockHttp + .Expect(httpMethod, requestUri) + .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}") + .WithHeaders("Accept", MimeTypes.Application.Json) + .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString()) + .Respond(HttpStatusCode.OK, responseContent); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddMailtrapClient(_clientConfig) + .ConfigurePrimaryHttpMessageHandler(() => mockHttp); + + using var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // Act + var result = await client + .Account(_accountId) + .Contacts() + .Import(importId) + .GetDetails() + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + + result.Should().BeEquivalentTo(expectedResponse); + } + + private async Task DeserializeStringContentAsync(StringContent responseContent) + { + var responseStream = await responseContent.ReadAsStreamAsync(); + var expectedResponse = await JsonSerializer.DeserializeAsync(responseStream, _jsonSerializerOptions); + responseStream.Position = 0; // Reset stream position + return expectedResponse; + } +} diff --git a/tests/Mailtrap.IntegrationTests/ContactImports/Create_ShouldFailValidation_WhenProvidedCollectionSizeIsInvalid.json b/tests/Mailtrap.IntegrationTests/ContactImports/Create_ShouldFailValidation_WhenProvidedCollectionSizeIsInvalid.json new file mode 100644 index 00000000..646b7141 --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/ContactImports/Create_ShouldFailValidation_WhenProvidedCollectionSizeIsInvalid.json @@ -0,0 +1,23 @@ +{ + "errors": [ + { + "email": "test@example.com", + "errors": { + "base": [ + "contacts limit reached", + "cannot import more than 50000 contacts at once" + ], + "email": [ + "is invalid", + "top level domain is too short" + ], + "fields": [ + "contains invalid values: invalid_field" + ], + "field_merge_tag": [ + "is invalid" + ] + } + } + ] +} diff --git a/tests/Mailtrap.IntegrationTests/ContactImports/Create_Success_Request.json b/tests/Mailtrap.IntegrationTests/ContactImports/Create_Success_Request.json new file mode 100644 index 00000000..92a6bfff --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/ContactImports/Create_Success_Request.json @@ -0,0 +1,36 @@ +{ + "contacts": [ + { + "email": "customer1@example.com", + "fields": { + "first_name": "John", + "last_name": "Smith", + "zip_code": 11111 + }, + "list_ids_included": [ + 1, + 2, + 3 + ], + "list_ids_excluded": [ + 4, + 5, + 6 + ] + }, + { + "email": "customer2@example.com", + "fields": { + "first_name": "Joe", + "last_name": "Doe", + "zip_code": 22222 + }, + "list_ids_included": [ + 1 + ], + "list_ids_excluded": [ + 4 + ] + } + ] +} diff --git a/tests/Mailtrap.IntegrationTests/ContactImports/Create_Success_Response.json b/tests/Mailtrap.IntegrationTests/ContactImports/Create_Success_Response.json new file mode 100644 index 00000000..49708d56 --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/ContactImports/Create_Success_Response.json @@ -0,0 +1,7 @@ +{ + "id": 1234, + "status": "finished", + "created_contacts_count": 1, + "updated_contacts_count": 1, + "contacts_over_limit_count": 0 +} diff --git a/tests/Mailtrap.IntegrationTests/ContactImports/GetDetails_Success.json b/tests/Mailtrap.IntegrationTests/ContactImports/GetDetails_Success.json new file mode 100644 index 00000000..3bbdd489 --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/ContactImports/GetDetails_Success.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "status": "started" +} diff --git a/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs b/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs index 198fd89e..c0d2d121 100644 --- a/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs +++ b/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs @@ -77,7 +77,7 @@ public async Task Create_Success() var httpMethod = HttpMethod.Post; var requestUri = _resourceUri.AbsoluteUri; - var contactEmail = $"{TestContext.CurrentContext.Random.GetString(10)}@mailtrap.io"; + var contactEmail = TestContext.CurrentContext.Random.NextEmail(); var request = new CreateContactRequest(contactEmail); using var responseContent = await Feature.LoadFileToStringContent(); @@ -198,7 +198,7 @@ public async Task Update_Success() var contactId = TestContext.CurrentContext.Random.NextGuid().ToString(); var requestUri = _resourceUri.Append(contactId).AbsoluteUri; - var updatedEmail = $"{TestContext.CurrentContext.Random.GetString(10)}@mailtrap.io"; + var updatedEmail = TestContext.CurrentContext.Random.NextEmail(10); var request = new UpdateContactRequest(updatedEmail); using var responseContent = await Feature.LoadFileToStringContent(); diff --git a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs index da82392b..f96e21a3 100644 --- a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs +++ b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs @@ -14,6 +14,8 @@ global using Mailtrap.Core.Extensions; global using Mailtrap.Core.Models; global using Mailtrap.Contacts.Requests; +global using Mailtrap.ContactImports.Models; +global using Mailtrap.ContactImports.Requests; global using Mailtrap.Emails; global using Mailtrap.Emails.Requests; global using Mailtrap.Emails.Responses; diff --git a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs index 34d15d5e..0241f1a7 100644 --- a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs +++ b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs @@ -18,4 +18,5 @@ internal static class UrlSegmentsTestConstants internal static string AttachmentsSegment { get; } = "attachments"; internal static string SendEmailSegment { get; } = "send"; internal static string ContactsSegment { get; } = "contacts"; + internal static string ImportsSegment { get; } = "imports"; } diff --git a/tests/Mailtrap.IntegrationTests/TestExtensions/EmailAddressHelpers.cs b/tests/Mailtrap.IntegrationTests/TestExtensions/EmailAddressHelpers.cs new file mode 100644 index 00000000..3aaff473 --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/TestExtensions/EmailAddressHelpers.cs @@ -0,0 +1,52 @@ +namespace Mailtrap.IntegrationTests.TestExtensions; + + +internal static class EmailAddressHelpers +{ + + + /// + /// Generates a random email address with the specified domain. + /// + /// Randomizer instance + /// Optional domain name + /// Total output length including domain. + /// Minimum length depends on the domain name length. + /// Random email address + public static string NextEmail(this NUnit.Framework.Internal.Randomizer random, int? outputLength = null, string? domain = null) + { + // domain name: at least 3 symbols (ex, "abc.org") + // TLD: minimum 2 symbols (ex, ".io") + const int minDomainNameLen = 3; + const int minTldLen = 2; + const int defaultUsernameLength = 8; + var atSymbol = "@"; + string domainPart; + + if (string.IsNullOrWhiteSpace(domain)) + { + var domainName = random.GetString(minDomainNameLen); + var tld = random.GetString(minTldLen); + domainPart = $"{domainName}.{tld}"; + } + else + { + domainPart = domain.TrimStart('@'); + } + + // Minimum email length: username (1) + '@' (1) + minimum domain (3) + '.' (1) + TLD (2) + var minEmailLength = 1 + atSymbol.Length + domainPart.Length; + + // Explicit check for valid outputLength + if (outputLength.HasValue && outputLength < minEmailLength) + { + return random.GetString(outputLength.Value); + } + + // Generation of username with optimal length + var usernameLength = outputLength - atSymbol.Length - domainPart.Length; + var username = random.GetString(usernameLength ?? defaultUsernameLength); + + return $"{username}{atSymbol}{domainPart}"; + } +} diff --git a/tests/Mailtrap.IntegrationTests/TestExtensions/FileHelpers.cs b/tests/Mailtrap.IntegrationTests/TestExtensions/FileHelpers.cs index 81c992bc..411ca0c5 100644 --- a/tests/Mailtrap.IntegrationTests/TestExtensions/FileHelpers.cs +++ b/tests/Mailtrap.IntegrationTests/TestExtensions/FileHelpers.cs @@ -7,13 +7,23 @@ internal static async Task LoadFileToStringContent( this string featureFolderName, string? fileName = null, string filexExt = "json") + { + var fileString = await LoadFileToString(featureFolderName, fileName, filexExt); + + return new StringContent(fileString); + } + + internal static async Task LoadFileToString( + this string featureFolderName, + string? fileName, + string filexExt = "json") { Ensure.NotNullOrEmpty(featureFolderName, nameof(featureFolderName)); var name = $"{fileName ?? TestContext.CurrentContext.Test.MethodName ?? "Test"}.{filexExt}"; var path = Path.Combine(featureFolderName, name); - var responseString = await File.ReadAllTextAsync(path); + var fileString = await File.ReadAllTextAsync(path); - return new StringContent(responseString); + return fileString; } } diff --git a/tests/Mailtrap.UnitTests/ContactImports/ContactsImportCollectionResourceTests.cs b/tests/Mailtrap.UnitTests/ContactImports/ContactsImportCollectionResourceTests.cs new file mode 100644 index 00000000..94d89bd5 --- /dev/null +++ b/tests/Mailtrap.UnitTests/ContactImports/ContactsImportCollectionResourceTests.cs @@ -0,0 +1,52 @@ +namespace Mailtrap.UnitTests.ContactImports; + + +[TestFixture] +internal sealed class ContactsImportCollectionResourceTests +{ + private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of(); + private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl + .Append( + UrlSegmentsTestConstants.ApiRootSegment, + UrlSegmentsTestConstants.AccountsSegment) + .Append(TestContext.CurrentContext.Random.NextLong()) + .Append(UrlSegmentsTestConstants.ContactsSegment) + .Append(UrlSegmentsTestConstants.ImportsSegment); + + + #region Constructor + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull() + { + // Act + var act = () => new ContactsImportCollectionResource(null!, _resourceUri); + + // Assert + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull() + { + // Act + var act = () => new ContactsImportCollectionResource(_commandFactoryMock, null!); + + // Assert + act.Should().Throw(); + } + + [Test] + public void ResourceUri_ShouldBeInitializedProperly() + { + // Arrange + var client = CreateResource(); + + // Assert + client.ResourceUri.Should().Be(_resourceUri); + } + + #endregion + + private ContactsImportCollectionResource CreateResource() => new(_commandFactoryMock, _resourceUri); +} diff --git a/tests/Mailtrap.UnitTests/ContactImports/ContactsImportResourceTests.cs b/tests/Mailtrap.UnitTests/ContactImports/ContactsImportResourceTests.cs new file mode 100644 index 00000000..073b1a49 --- /dev/null +++ b/tests/Mailtrap.UnitTests/ContactImports/ContactsImportResourceTests.cs @@ -0,0 +1,52 @@ +namespace Mailtrap.UnitTests.ContactImports; + + +[TestFixture] +internal sealed class ContactsImportResourceTests +{ + private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of(); + private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl + .Append( + UrlSegmentsTestConstants.ApiRootSegment, + UrlSegmentsTestConstants.AccountsSegment) + .Append(TestContext.CurrentContext.Random.NextLong()) + .Append(UrlSegmentsTestConstants.ContactsSegment) + .Append(UrlSegmentsTestConstants.ImportsSegment) + .Append(TestContext.CurrentContext.Random.NextLong()); + + #region Constructor + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull() + { + // Act + var act = () => new ContactsImportResource(null!, _resourceUri); + + // Assert + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull() + { + // Act + var act = () => new ContactsImportResource(_commandFactoryMock, null!); + + // Assert + act.Should().Throw(); + } + + [Test] + public void ResourceUri_ShouldBeInitializedProperly() + { + // Arrange + var client = CreateResource(); + + // Assert + client.ResourceUri.Should().Be(_resourceUri); + } + + #endregion + + private ContactsImportResource CreateResource() => new(_commandFactoryMock, _resourceUri); +} diff --git a/tests/Mailtrap.UnitTests/ContactImports/Requests/ContactsImportRequestTests.cs b/tests/Mailtrap.UnitTests/ContactImports/Requests/ContactsImportRequestTests.cs new file mode 100644 index 00000000..4f249621 --- /dev/null +++ b/tests/Mailtrap.UnitTests/ContactImports/Requests/ContactsImportRequestTests.cs @@ -0,0 +1,103 @@ +namespace Mailtrap.UnitTests.ContactImports.Requests; + + +[TestFixture] +internal sealed class ContactsImportRequestTests +{ + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenProvidedCollectionIsNull() + { + var act = () => new ContactsImportRequest(null!); + + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenProvidedCollectionIsEmpty() + { + var act = () => new ContactsImportRequest(Array.Empty()); + + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldInitializeFieldsCorrectly() + { + // Arrange + var contacts = new List { RandomContactImportRequest() }; + + // Act + var request = new ContactsImportRequest(contacts); + + // Assert + request.Contacts.Should().BeEquivalentTo(contacts); + } + + + [Test] + public void Validate_ShouldFail_WhenProvidedCollectionSizeIsInvalid([Values(0, 50001)] int size) + { + // Arrange + var contacts = new List(size); + for (var i = 0; i < size; i++) + { + contacts.Add(RandomContactImportRequest()); + } + var request = size == 0 ? new ContactsImportRequest() : new ContactsImportRequest(contacts); + + // Act + var result = request.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + } + + [Test] + public void Validate_ShouldFail_WhenProvidedCollectionContainsNull() + { + // Arrange + var contacts = new List() + { + RandomContactImportRequest(), + null!, + RandomContactImportRequest() + }; + + var request = new ContactsImportRequest(contacts); + + // Act + var result = request.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + } + + [Test] + public void Validate_ShouldFail_WhenProvidedCollectionContainsInvalidRecord([Values(1, 101)] int emailLength) + { + // Arrange + var contacts = new List() + { + RandomContactImportRequest(), + new(TestContext.CurrentContext.Random.GetString(emailLength)), // Invalid email length + RandomContactImportRequest() + }; + + var request = new ContactsImportRequest(contacts); + + // Act + var result = request.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + } + + private static ContactImportRequest RandomContactImportRequest() + { + var email = TestContext.CurrentContext.Random.GetString(5) + + "@" + + TestContext.CurrentContext.Random.GetString(5) + + ".com"; + return new ContactImportRequest(email); + } +} diff --git a/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs b/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs index f8ef8e6f..6e3b1df0 100644 --- a/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs +++ b/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs @@ -2,7 +2,7 @@ [TestFixture] -internal sealed class ContactctResourceTests +internal sealed class ContactResourceTests { private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of(); private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl diff --git a/tests/Mailtrap.UnitTests/GlobalUsings.cs b/tests/Mailtrap.UnitTests/GlobalUsings.cs index 8523ffb9..549ca6a5 100644 --- a/tests/Mailtrap.UnitTests/GlobalUsings.cs +++ b/tests/Mailtrap.UnitTests/GlobalUsings.cs @@ -25,6 +25,8 @@ global using Mailtrap.Core.Rest.Commands; global using Mailtrap.Contacts; global using Mailtrap.Contacts.Requests; +global using Mailtrap.ContactImports; +global using Mailtrap.ContactImports.Requests; global using Mailtrap.Emails; global using Mailtrap.Emails.Models; global using Mailtrap.Emails.Requests; diff --git a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs index 78a531c0..c3052bd5 100644 --- a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs +++ b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs @@ -15,4 +15,5 @@ internal static class UrlSegmentsTestConstants internal static string AttachmentsSegment { get; } = "attachments"; internal static string SendEmailSegment { get; } = "send"; internal static string ContactsSegment { get; } = "contacts"; + internal static string ImportsSegment { get; } = "imports"; }