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