diff --git a/Mailtrap.sln b/Mailtrap.sln index 8becfbb5..0631c09c 100644 --- a/Mailtrap.sln +++ b/Mailtrap.sln @@ -87,6 +87,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mailtrap.Example.TestingMes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mailtrap.Example.Factory", "examples\Mailtrap.Example.Factory\Mailtrap.Example.Factory.csproj", "{AB1237F4-D074-4D3C-9AE4-6794BD30EA71}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.Contact", "examples\Mailtrap.Example.Contact\Mailtrap.Example.Contact.csproj", "{3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,6 +163,10 @@ Global {AB1237F4-D074-4D3C-9AE4-6794BD30EA71}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB1237F4-D074-4D3C-9AE4-6794BD30EA71}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB1237F4-D074-4D3C-9AE4-6794BD30EA71}.Release|Any CPU.Build.0 = Release|Any CPU + {3F8D2B21-5C6E-4A9A-9C3B-9F1D2A7B8C64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -183,6 +189,7 @@ Global {AB9CF980-EAC5-4BC4-AC85-FFA0770FF7DA} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0FF614CC-FEBC-4C66-B3FC-FCB73EE511D7} diff --git a/examples/Mailtrap.Example.Contact/Mailtrap.Example.Contact.csproj b/examples/Mailtrap.Example.Contact/Mailtrap.Example.Contact.csproj new file mode 100644 index 00000000..6f9ac7de --- /dev/null +++ b/examples/Mailtrap.Example.Contact/Mailtrap.Example.Contact.csproj @@ -0,0 +1,11 @@ + + + + + + + + PreserveNewest + + + diff --git a/examples/Mailtrap.Example.Contact/Program.cs b/examples/Mailtrap.Example.Contact/Program.cs new file mode 100644 index 00000000..6cec63f5 --- /dev/null +++ b/examples/Mailtrap.Example.Contact/Program.cs @@ -0,0 +1,81 @@ +using Mailtrap; +using Mailtrap.Accounts; +using Mailtrap.Contacts; +using Mailtrap.Contacts.Models; +using Mailtrap.Contacts.Requests; +using Mailtrap.Contacts.Responses; +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); + + var contactEmail = "example@mailtrap.io"; + + // Get resource for contacts collection + IContactCollectionResource contactsResource = accountResource.Contacts(); + + // Get all contacts for account + IList contacts = await contactsResource.GetAll(); + + Contact? contact = contacts + .FirstOrDefault(p => string.Equals(p.Email, contactEmail, StringComparison.OrdinalIgnoreCase)); + + + if (contact is null) + { + logger.LogWarning("No contact found. Creating."); + + // Create contact + var createContactRequest = new CreateContactRequest(contactEmail); + createContactRequest.Fields.Add("first_name", "John"); + createContactRequest.Fields.Add("last_name", "Doe"); + CreateContactResponse createContactResponse = await contactsResource.Create(createContactRequest); + contact = createContactResponse.Contact; + } + else + { + logger.LogInformation("Contact found."); + } + + // Get resource for specific contact + IContactResource contactResource = accountResource.Contact(contact.Id); + + // Get details + ContactResponse contactResponse = await contactResource.GetDetails(); + Contact contactDetails = contactResponse.Contact; + logger.LogInformation("Contact: {Contact}", contactDetails); + + // Update contact details + var updateContactRequest = new UpdateContactRequest("test@mailtrap.io"); + UpdateContactResponse updateContactResponse = await contactResource.Update(updateContactRequest); + Contact updatedContact = updateContactResponse.Contact; + logger.LogInformation("Updated Contact: {Contact}", updatedContact); + + // Delete contact + // Beware that contact resource becomes invalid after deletion and should not be used anymore + await contactResource.Delete(); + logger.LogInformation("Contact Deleted."); +} +catch (Exception ex) +{ + logger.LogError(ex, "An error occurred during API call."); + Environment.FailFast(ex.Message); + throw; +} diff --git a/examples/Mailtrap.Example.Contact/Properties/launchSettings.json b/examples/Mailtrap.Example.Contact/Properties/launchSettings.json new file mode 100644 index 00000000..b8daf491 --- /dev/null +++ b/examples/Mailtrap.Example.Contact/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Project": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Mailtrap.Example.Contact/appsettings.json b/examples/Mailtrap.Example.Contact/appsettings.json new file mode 100644 index 00000000..7cf4089d --- /dev/null +++ b/examples/Mailtrap.Example.Contact/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/Accounts/IAccountResource.cs b/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs index 0f04458e..f9596393 100644 --- a/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs +++ b/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs @@ -9,7 +9,7 @@ public interface IAccountResource : IRestResource /// /// Gets account access collection resource for the account, represented by this resource instance. /// - /// + /// /// /// Account access collection resource for the account, represented by this resource instance. /// @@ -22,7 +22,7 @@ public interface IAccountResource : IRestResource /// /// ID of account access to get resource for. /// - /// + /// /// /// Resource for the account access with specified ID. /// @@ -36,7 +36,7 @@ public interface IAccountResource : IRestResource /// /// Gets permissions resource for the account, represented by this resource instance. /// - /// + /// /// /// Permissions resource for the account, represented by this resource instance. /// @@ -46,7 +46,7 @@ public interface IAccountResource : IRestResource /// /// Gets billing resource for the account, represented by this resource instance. /// - /// + /// /// /// Billing resource for the account, represented by this resource instance. /// @@ -56,7 +56,7 @@ public interface IAccountResource : IRestResource /// /// Gets sending domain collection resource for the account, represented by this resource instance. /// - /// + /// /// /// Sending domain collection resource for the account, represented by this resource instance. /// @@ -69,7 +69,7 @@ public interface IAccountResource : IRestResource /// /// ID of sending domain to get resource for. /// - /// + /// /// /// Resource for the sending domain with specified ID. /// @@ -83,7 +83,7 @@ public interface IAccountResource : IRestResource /// /// Gets project collection resource for the account, represented by this resource instance. /// - /// + /// /// /// Project collection resource for the account, represented by this resource instance. /// @@ -96,7 +96,7 @@ public interface IAccountResource : IRestResource /// /// ID of project to get resource for. /// - /// + /// /// /// Resource for the project with specified ID. /// @@ -110,7 +110,7 @@ public interface IAccountResource : IRestResource /// /// Gets inbox collection resource for the account, represented by this resource instance. /// - /// + /// /// /// Inbox collection resource for the account, represented by this resource instance. /// @@ -123,7 +123,7 @@ public interface IAccountResource : IRestResource /// /// ID of inbox to get resource for. /// - /// + /// /// /// Resource for the inbox with specified ID. /// @@ -132,4 +132,30 @@ public interface IAccountResource : IRestResource /// When is less than or equal to zero. /// public IInboxResource Inbox(long inboxId); + + /// + /// Gets contact collection resource for the account, represented by this resource instance. + /// + /// + /// + /// Contact collection resource for the account, represented by this resource instance. + /// + public IContactCollectionResource Contacts(); + + /// + /// Gets resource for specific contact, identified by . + /// + /// + /// + /// ID or email of contact to get resource for. + /// + /// + /// + /// Resource for the contact with specified ID or email. + /// + /// + /// + /// When is null or empty. + /// + public IContactResource Contact(string idOrEmail); } diff --git a/src/Mailtrap.Abstractions/Contacts/Converters/DateTimeToUnixMsJsonConverter.cs b/src/Mailtrap.Abstractions/Contacts/Converters/DateTimeToUnixMsJsonConverter.cs new file mode 100644 index 00000000..560a6246 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Converters/DateTimeToUnixMsJsonConverter.cs @@ -0,0 +1,34 @@ +namespace Mailtrap.Contacts.Converters; + +/// +/// Converts DateTimeOffset to Unix time milliseconds for JSON serialization. +/// +internal sealed class DateTimeToUnixMsNullableJsonConverter : JsonConverter +{ + public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType != JsonTokenType.Number) + { + throw new JsonException($"Expected number for Unix time milliseconds but got {reader.TokenType}."); + } + + var ms = reader.GetInt64(); + return DateTimeOffset.FromUnixTimeMilliseconds(ms); + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteNumberValue(value.Value.ToUnixTimeMilliseconds()); + } +} diff --git a/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs b/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs new file mode 100644 index 00000000..44ff8ea5 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/IContactCollectionResource.cs @@ -0,0 +1,37 @@ +namespace Mailtrap.Contacts; + +/// +/// Represents Contacts collection resource.. +/// +public interface IContactCollectionResource : IRestResource +{ + /// + /// Gets contacts. + /// + /// + /// + /// Token to control operation cancellation. + /// + /// + /// + /// Collection of contact details. + /// + public Task> GetAll(CancellationToken cancellationToken = default); + + /// + /// Creates a new contact with details specified by . + /// + /// + /// + /// Request containing contact details for creation. + /// + /// + /// + /// + /// + /// + /// + /// Created contact details. + /// + public Task Create(CreateContactRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Mailtrap.Abstractions/Contacts/IContactResource.cs b/src/Mailtrap.Abstractions/Contacts/IContactResource.cs new file mode 100644 index 00000000..1fe6fa39 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/IContactResource.cs @@ -0,0 +1,57 @@ +namespace Mailtrap.Contacts; + +/// +/// Represents Contacts resource. +/// +public interface IContactResource : IRestResource +{ + /// + /// Gets details of the contact, represented by the current resource instance. + /// + /// + /// + /// Token to control operation cancellation. + /// + /// + /// + /// Requested contact details. + /// + public Task GetDetails(CancellationToken cancellationToken = default); + + /// + /// Updates the contact, represented by the current resource instance, with details specified by . + /// + /// + /// + /// Contact details for update. + /// + /// + /// + /// + /// + /// + /// + /// Updated contact details. + /// + public Task Update(UpdateContactRequest request, CancellationToken cancellationToken = default); + + /// + /// Deletes a contact, represented by the current resource instance. + /// + /// + /// + /// + /// + /// + /// + /// Nothing is returned upon successful deletion. + /// + /// + /// + /// + /// After deletion of the contact, represented by the current resource instance, it will be no longer available.
+ /// Thus any further operations on it will result in an error. + ///
+ ///
+ public Task Delete(CancellationToken cancellationToken = default); +} diff --git a/src/Mailtrap.Abstractions/Contacts/Models/Contact.cs b/src/Mailtrap.Abstractions/Contacts/Models/Contact.cs new file mode 100644 index 00000000..d3989abb --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Models/Contact.cs @@ -0,0 +1,81 @@ +namespace Mailtrap.Contacts.Models; + +/// +/// Represents Contact details. +/// +public sealed record Contact +{ + /// + /// Gets Contact identifier. + /// + /// + /// + /// Contact identifier. + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// Gets Contact email. + /// + /// + /// + /// Contact email. + /// + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + + /// + /// Gets Contact fields. + /// + /// + /// + /// Contact fields. + /// + [JsonPropertyName("fields")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public IDictionary Fields { get; } = new Dictionary(); + + /// + /// Gets Contact's list ids. + /// + /// + /// + /// Contact's list ids. + /// + [JsonPropertyName("list_ids")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public IList ListIds { get; } = []; + + /// + /// Gets status of the contact. + /// + /// + /// + /// Contact's status. + /// + [JsonPropertyName("status")] + public ContactStatus Status { get; set; } = ContactStatus.Unknown; + + /// + /// Gets Contact creation date and time. + /// + /// + /// + /// Contact creation date and time. + /// + [JsonPropertyName("created_at")] + [JsonConverter(typeof(DateTimeToUnixMsNullableJsonConverter))] + public DateTimeOffset? CreatedAt { get; set; } + + /// + /// Gets Contact's update date and time. + /// + /// + /// + /// Contact's update date and time. + /// + [JsonPropertyName("updated_at")] + [JsonConverter(typeof(DateTimeToUnixMsNullableJsonConverter))] + public DateTimeOffset? UpdatedAt { get; set; } +} diff --git a/src/Mailtrap.Abstractions/Contacts/Models/ContactAction.cs b/src/Mailtrap.Abstractions/Contacts/Models/ContactAction.cs new file mode 100644 index 00000000..e7636321 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Models/ContactAction.cs @@ -0,0 +1,25 @@ +namespace Mailtrap.Contacts.Models; + +/// +/// Contact update actions. +/// +public sealed record ContactAction : StringEnum +{ + /// + /// Gets the value representing "Updated" contact action. + /// + /// + /// + /// Represents "Updated" contact action. + /// + public static readonly ContactAction Updated = Define("updated"); + + /// + /// Gets the value representing "Created" contact action. + /// + /// + /// + /// Represents "Created" contact action. + /// + public static readonly ContactAction Created = Define("created"); +} diff --git a/src/Mailtrap.Abstractions/Contacts/Models/ContactStatus.cs b/src/Mailtrap.Abstractions/Contacts/Models/ContactStatus.cs new file mode 100644 index 00000000..16a3d316 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Models/ContactStatus.cs @@ -0,0 +1,25 @@ +namespace Mailtrap.Contacts.Models; + +/// +/// Contact subscription status. +/// +public sealed record ContactStatus : StringEnum +{ + /// + /// Gets the value representing "Subscribed" contact status. + /// + /// + /// + /// Represents "Subscribed" contact status. + /// + public static readonly ContactStatus Subscribed = Define("subscribed"); + + /// + /// Gets the value representing "Unsubscribed" contact status. + /// + /// + /// + /// Represents "Unsubscribed" contact status. + /// + public static readonly ContactStatus Unsubscribed = Define("unsubscribed"); +} diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs new file mode 100644 index 00000000..97f64c68 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Requests/ContactRequest.cs @@ -0,0 +1,59 @@ +namespace Mailtrap.Contacts.Requests; + +/// +/// Generic request object for contact CRUD operations. +/// +public record ContactRequest : IValidatable +{ + /// + /// Gets contact email. + /// + /// + /// + /// Contact email. + /// + [JsonPropertyName("email")] + [JsonRequired] + public string Email { get; set; } + + /// + /// Gets contact fields. + /// + /// + /// + /// Contact fields. + /// + [JsonPropertyName("fields")] + public IDictionary Fields { get; } = new Dictionary(); + + /// + /// Primary instance constructor. + /// + /// + /// + /// Email of the contact. + /// + /// + /// + /// Contact's must be min 2 characters and max 100 characters long. + /// + /// + /// + /// When is or . + /// + public ContactRequest(string email) + { + Ensure.NotNullOrEmpty(email, nameof(email)); + + Email = email; + } + + + /// + public ValidationResult Validate() + { + return ContactRequestValidator.Instance + .Validate(this) + .ToMailtrapValidationResult(); + } +} diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs new file mode 100644 index 00000000..4744aa91 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Requests/CreateContactRequest.cs @@ -0,0 +1,20 @@ +namespace Mailtrap.Contacts.Requests; + +/// +/// Request object for creating a contact. +/// +public sealed record CreateContactRequest : ContactRequest +{ + /// + /// Gets contact list IDs. + /// + /// + /// + /// Contact list IDs. + /// + [JsonPropertyName("list_ids")] + public IList ListIds { get; } = []; + + /// + public CreateContactRequest(string email) : base(email) { } +} diff --git a/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs b/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs new file mode 100644 index 00000000..06cfc4bb --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Requests/UpdateContactRequest.cs @@ -0,0 +1,40 @@ +namespace Mailtrap.Contacts.Requests; + +/// +/// Request object for updating a contact. +/// +public sealed record UpdateContactRequest : ContactRequest +{ + /// + /// 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. + /// + /// + /// + /// Contact "unsubscribed" status. + /// + [JsonPropertyName("unsubscribed")] + public bool? Unsubscribed { get; set; } + + /// + public UpdateContactRequest(string email) : base(email) { } +} diff --git a/src/Mailtrap.Abstractions/Contacts/Responses/ContactResponse.cs b/src/Mailtrap.Abstractions/Contacts/Responses/ContactResponse.cs new file mode 100644 index 00000000..317ead04 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Responses/ContactResponse.cs @@ -0,0 +1,18 @@ +namespace Mailtrap.Contacts.Responses; + +/// +/// Generic response object for contact operations. +/// +public record ContactResponse +{ + /// + /// Gets created contact data. + /// + /// + /// + /// Contact data. + /// + [JsonPropertyName("data")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public Contact Contact { get; set; } = new(); +} diff --git a/src/Mailtrap.Abstractions/Contacts/Responses/CreateContactResponse.cs b/src/Mailtrap.Abstractions/Contacts/Responses/CreateContactResponse.cs new file mode 100644 index 00000000..e912e072 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Responses/CreateContactResponse.cs @@ -0,0 +1,8 @@ +namespace Mailtrap.Contacts.Responses; + +/// +/// Response object for contact creation. +/// +public sealed record CreateContactResponse : ContactResponse +{ +} diff --git a/src/Mailtrap.Abstractions/Contacts/Responses/UpdateContactResponse.cs b/src/Mailtrap.Abstractions/Contacts/Responses/UpdateContactResponse.cs new file mode 100644 index 00000000..af8b6047 --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Responses/UpdateContactResponse.cs @@ -0,0 +1,18 @@ +namespace Mailtrap.Contacts.Responses; + +/// +/// Response object for contact update. +/// +public sealed record UpdateContactResponse : ContactResponse +{ + /// + /// Gets the action performed on the contact. + /// + /// + /// + /// "created" if contact does not exist, + /// "updated" if contact already exists. + /// + [JsonPropertyName("action")] + public ContactAction Action { get; set; } = ContactAction.Unknown; +} diff --git a/src/Mailtrap.Abstractions/Contacts/Validators/ContactRequestValidator.cs b/src/Mailtrap.Abstractions/Contacts/Validators/ContactRequestValidator.cs new file mode 100644 index 00000000..6d44a6cb --- /dev/null +++ b/src/Mailtrap.Abstractions/Contacts/Validators/ContactRequestValidator.cs @@ -0,0 +1,22 @@ +namespace Mailtrap.Contacts.Validators; + + +/// +/// Validator for Create/Update contact requests.
+/// Ensures contact's email is not empty and length is within the allowed range. +///
+public sealed class ContactRequestValidator : AbstractValidator +{ + /// + /// Static validator instance for reuse. + /// + public static ContactRequestValidator Instance { get; } = new(); + + /// + /// Primary constructor. + /// + public ContactRequestValidator() + { + RuleFor(r => r.Email).NotEmpty().Length(2, 100); + } +} diff --git a/src/Mailtrap.Abstractions/GlobalUsings.cs b/src/Mailtrap.Abstractions/GlobalUsings.cs index 09d1b175..28d0de4e 100644 --- a/src/Mailtrap.Abstractions/GlobalUsings.cs +++ b/src/Mailtrap.Abstractions/GlobalUsings.cs @@ -21,6 +21,12 @@ global using Mailtrap.Core.Models; global using Mailtrap.Core.Rest; global using Mailtrap.Core.Validation; +global using Mailtrap.Contacts; +global using Mailtrap.Contacts.Models; +global using Mailtrap.Contacts.Requests; +global using Mailtrap.Contacts.Responses; +global using Mailtrap.Contacts.Converters; +global using Mailtrap.Contacts.Validators; global using Mailtrap.Emails; global using Mailtrap.Emails.Models; global using Mailtrap.Emails.Requests; diff --git a/src/Mailtrap/Accounts/AccountResource.cs b/src/Mailtrap/Accounts/AccountResource.cs index 394f0c31..22c56e85 100644 --- a/src/Mailtrap/Accounts/AccountResource.cs +++ b/src/Mailtrap/Accounts/AccountResource.cs @@ -52,4 +52,19 @@ public IInboxCollectionResource Inboxes() public IInboxResource Inbox(long inboxId) => new InboxResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.InboxesSegment).Append(inboxId)); + + public IContactCollectionResource Contacts() + => new ContactCollectionResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.ContactsSegment)); + + public IContactResource Contact(string idOrEmail) + { + Ensure.NotNullOrEmpty(idOrEmail, nameof(idOrEmail)); + var encoded = Uri.EscapeDataString(idOrEmail); + + return new ContactResource( + RestResourceCommandFactory, + ResourceUri + .Append(UrlSegments.ContactsSegment) + .Append(encoded)); + } } diff --git a/src/Mailtrap/Contacts/ContactCollectionResource.cs b/src/Mailtrap/Contacts/ContactCollectionResource.cs new file mode 100644 index 00000000..3e28a867 --- /dev/null +++ b/src/Mailtrap/Contacts/ContactCollectionResource.cs @@ -0,0 +1,19 @@ +using Mailtrap.Contacts.Responses; + +namespace Mailtrap.Contacts; + +/// +/// Implementation of Contact Collection resource. +/// +internal sealed class ContactCollectionResource : RestResource, IContactCollectionResource +{ + public ContactCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri) + : base(restResourceCommandFactory, resourceUri) { } + + + public async Task> GetAll(CancellationToken cancellationToken = default) + => await GetList(cancellationToken).ConfigureAwait(false); + + public async Task Create(CreateContactRequest request, CancellationToken cancellationToken = default) + => await Create(request.ToDto(), cancellationToken).ConfigureAwait(false); +} diff --git a/src/Mailtrap/Contacts/ContactResource.cs b/src/Mailtrap/Contacts/ContactResource.cs new file mode 100644 index 00000000..6fd06a15 --- /dev/null +++ b/src/Mailtrap/Contacts/ContactResource.cs @@ -0,0 +1,18 @@ +using Mailtrap.Contacts.Responses; + +namespace Mailtrap.Contacts; + +/// +/// Implementation of Contacts API operations. +/// +internal sealed class ContactResource : RestResource, IContactResource +{ + public ContactResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri) + : base(restResourceCommandFactory, resourceUri) { } + public async Task GetDetails(CancellationToken cancellationToken = default) + => await Get(cancellationToken).ConfigureAwait(false); + public async Task Update(UpdateContactRequest request, CancellationToken cancellationToken = default) + => await Update(request.ToDto(), cancellationToken).ConfigureAwait(false); + public async Task Delete(CancellationToken cancellationToken = default) + => await DeleteWithStatusCodeResult(cancellationToken).ConfigureAwait(false); +} diff --git a/src/Mailtrap/Contacts/Requests/ContactRequestDto.cs b/src/Mailtrap/Contacts/Requests/ContactRequestDto.cs new file mode 100644 index 00000000..3141b778 --- /dev/null +++ b/src/Mailtrap/Contacts/Requests/ContactRequestDto.cs @@ -0,0 +1,37 @@ +namespace Mailtrap.Contacts.Requests; + + +/// +/// Generic request object for contact CRUD operations. +/// +internal record ContactRequestDto : IValidatable + where T : ContactRequest +{ + /// + /// Gets or sets contact request payload. + /// + /// + /// + /// Contact request payload. + /// + [JsonPropertyName("contact")] + [JsonPropertyOrder(1)] + public T Contact { get; } + + + public ContactRequestDto(T contact) + { + Ensure.NotNull(contact, nameof(contact)); + + Contact = contact; + } + + + /// + public ValidationResult Validate() + { + return ContactRequestValidator.Instance + .Validate(Contact) + .ToMailtrapValidationResult(); + } +} diff --git a/src/Mailtrap/Contacts/Requests/ContactRequestExtensions.cs b/src/Mailtrap/Contacts/Requests/ContactRequestExtensions.cs new file mode 100644 index 00000000..a076125d --- /dev/null +++ b/src/Mailtrap/Contacts/Requests/ContactRequestExtensions.cs @@ -0,0 +1,9 @@ +namespace Mailtrap.Contacts.Requests; + + +internal static class ContactRequestExtensions +{ + public static CreateContactRequestDto ToDto(this CreateContactRequest request) => new(request); + + public static UpdateContactRequestDto ToDto(this UpdateContactRequest request) => new(request); +} diff --git a/src/Mailtrap/Contacts/Requests/CreateContactRequestDto.cs b/src/Mailtrap/Contacts/Requests/CreateContactRequestDto.cs new file mode 100644 index 00000000..01c63c40 --- /dev/null +++ b/src/Mailtrap/Contacts/Requests/CreateContactRequestDto.cs @@ -0,0 +1,10 @@ +namespace Mailtrap.Contacts.Requests; + + +/// +/// Request object for creating contact. +/// +internal sealed record CreateContactRequestDto : ContactRequestDto +{ + public CreateContactRequestDto(CreateContactRequest request) : base(request) { } +} diff --git a/src/Mailtrap/Contacts/Requests/UpdateContactRequestDto.cs b/src/Mailtrap/Contacts/Requests/UpdateContactRequestDto.cs new file mode 100644 index 00000000..0dcf24c3 --- /dev/null +++ b/src/Mailtrap/Contacts/Requests/UpdateContactRequestDto.cs @@ -0,0 +1,10 @@ +namespace Mailtrap.Contacts.Requests; + + +/// +/// Request object for updating contact details. +/// +internal sealed record UpdateContactRequestDto : ContactRequestDto +{ + public UpdateContactRequestDto(UpdateContactRequest request) : base(request) { } +} diff --git a/src/Mailtrap/Core/Constants/UrlSegments.cs b/src/Mailtrap/Core/Constants/UrlSegments.cs index c0e2b3fd..1c4520ad 100644 --- a/src/Mailtrap/Core/Constants/UrlSegments.cs +++ b/src/Mailtrap/Core/Constants/UrlSegments.cs @@ -6,4 +6,5 @@ internal static class UrlSegments internal static string ApiRootSegment { get; } = "api"; internal static string ProjectsSegment { get; } = "projects"; internal static string InboxesSegment { get; } = "inboxes"; + internal static string ContactsSegment { get; } = "contacts"; } diff --git a/src/Mailtrap/Core/Rest/Commands/DeleteWithStatusCodeResultRestResourceCommand.cs b/src/Mailtrap/Core/Rest/Commands/DeleteWithStatusCodeResultRestResourceCommand.cs index 51cfa789..0c0e944b 100644 --- a/src/Mailtrap/Core/Rest/Commands/DeleteWithStatusCodeResultRestResourceCommand.cs +++ b/src/Mailtrap/Core/Rest/Commands/DeleteWithStatusCodeResultRestResourceCommand.cs @@ -1,6 +1,6 @@ namespace Mailtrap.Core.Rest.Commands; -internal sealed class DeleteWithStatusCodeResultRestResourceCommand : DeleteRestResourceCommand +internal sealed class DeleteWithStatusCodeResultRestResourceCommand : DeleteRestResourceCommand { public DeleteWithStatusCodeResultRestResourceCommand( IHttpClientProvider httpClientProvider, diff --git a/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs b/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs index 38609f9a..ec544c94 100644 --- a/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs +++ b/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs @@ -7,10 +7,8 @@ internal interface IRestResourceCommandFactory public IRestResourceCommand CreatePlainText(Uri resourceUri, params string[] additionalAcceptContentTypes); public IRestResourceCommand CreatePatch(Uri resourceUri); public IRestResourceCommand CreateDelete(Uri resourceUri); - public IRestResourceCommand CreateDeleteWithStatusCodeResult(Uri resourceUri); - + public IRestResourceCommand CreateDeleteWithStatusCodeResult(Uri resourceUri); public IRestResourceCommand CreatePostWithStatusCodeResult(Uri resourceUri, TRequest request) where TRequest : class; - public IRestResourceCommand CreatePost(Uri resourceUri, TRequest request) where TRequest : class; public IRestResourceCommand CreatePut(Uri resourceUri, TRequest request) where TRequest : class; public IRestResourceCommand CreatePatchWithContent(Uri resourceUri, TRequest request) where TRequest : class; diff --git a/src/Mailtrap/Core/Rest/RestResource.cs b/src/Mailtrap/Core/Rest/RestResource.cs index 4e7046f0..1d2a1f5b 100644 --- a/src/Mailtrap/Core/Rest/RestResource.cs +++ b/src/Mailtrap/Core/Rest/RestResource.cs @@ -61,6 +61,12 @@ protected async Task Delete(CancellationToken cancellationToke .Execute(cancellationToken) .ConfigureAwait(false); + protected async Task DeleteWithStatusCodeResult(CancellationToken cancellationToken = default) + => await RestResourceCommandFactory + .CreateDeleteWithStatusCodeResult(ResourceUri) + .Execute(cancellationToken) + .ConfigureAwait(false); + private Task Get(Uri uri, CancellationToken cancellationToken = default) => RestResourceCommandFactory diff --git a/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs b/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs index 9ce386ac..373c2a2b 100644 --- a/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs +++ b/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs @@ -53,11 +53,12 @@ public IRestResourceCommand CreateDelete(Uri resourceUri) _httpResponseHandlerFactory, resourceUri); - public IRestResourceCommand CreateDeleteWithStatusCodeResult(Uri resourceUri) => new DeleteWithStatusCodeResultRestResourceCommand( - _httpClientProvider, - _httpRequestMessageFactory, - _httpResponseHandlerFactory, - resourceUri); + public IRestResourceCommand CreateDeleteWithStatusCodeResult(Uri resourceUri) + => new DeleteWithStatusCodeResultRestResourceCommand( + _httpClientProvider, + _httpRequestMessageFactory, + _httpResponseHandlerFactory, + resourceUri); public IRestResourceCommand CreatePostWithStatusCodeResult(Uri resourceUri, TRequest request) where TRequest : class diff --git a/src/Mailtrap/GlobalUsings.cs b/src/Mailtrap/GlobalUsings.cs index adfc7a3e..a1cc70e3 100644 --- a/src/Mailtrap/GlobalUsings.cs +++ b/src/Mailtrap/GlobalUsings.cs @@ -28,6 +28,10 @@ global using Mailtrap.Core.Rest; global using Mailtrap.Core.Rest.Commands; global using Mailtrap.Core.Validation; +global using Mailtrap.Contacts; +global using Mailtrap.Contacts.Models; +global using Mailtrap.Contacts.Requests; +global using Mailtrap.Contacts.Validators; global using Mailtrap.Emails; global using Mailtrap.Emails.Models; global using Mailtrap.Emails.Requests; diff --git a/src/Mailtrap/SendingDomains/SendingDomainResource.cs b/src/Mailtrap/SendingDomains/SendingDomainResource.cs index 03d59bed..c644cce7 100644 --- a/src/Mailtrap/SendingDomains/SendingDomainResource.cs +++ b/src/Mailtrap/SendingDomains/SendingDomainResource.cs @@ -27,7 +27,7 @@ await RestResourceCommandFactory public async Task Delete(CancellationToken cancellationToken = default) => await RestResourceCommandFactory - .CreateDeleteWithStatusCodeResult(ResourceUri) + .CreateDeleteWithStatusCodeResult(ResourceUri) .Execute(cancellationToken) .ConfigureAwait(false); } diff --git a/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs b/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs new file mode 100644 index 00000000..198fd89e --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/Contacts/ContactsIntegrationTests.cs @@ -0,0 +1,310 @@ +namespace Mailtrap.IntegrationTests.Contacts; + + +[TestFixture] +internal sealed class ContactsIntegrationTests +{ + private const string Feature = "Contacts"; + + private readonly long _accountId; + private readonly Uri _resourceUri = null!; + private readonly MailtrapClientOptions _clientConfig = null!; + private readonly JsonSerializerOptions _jsonSerializerOptions = null!; + + + public ContactsIntegrationTests() + { + var random = TestContext.CurrentContext.Random; + + _accountId = random.NextLong(); + _resourceUri = EndpointsTestConstants.ApiDefaultUrl + .Append( + UrlSegmentsTestConstants.ApiRootSegment, + UrlSegmentsTestConstants.AccountsSegment) + .Append(_accountId) + .Append(UrlSegmentsTestConstants.ContactsSegment); + + var token = random.GetString(); + _clientConfig = new MailtrapClientOptions(token); + _jsonSerializerOptions = _clientConfig.ToJsonSerializerOptions(); + } + + + [Test] + public async Task GetAll_Success() + { + // Arrange + var httpMethod = HttpMethod.Get; + var requestUri = _resourceUri.AbsoluteUri; + + using var responseContent = await Feature.LoadFileToStringContent(); + + 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() + .GetAll() + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + + result.Should() + .NotBeNull().And + .HaveCount(3); + } + + [Test] + public async Task Create_Success() + { + // Arrange + var httpMethod = HttpMethod.Post; + var requestUri = _resourceUri.AbsoluteUri; + + var contactEmail = $"{TestContext.CurrentContext.Random.GetString(10)}@mailtrap.io"; + var request = new CreateContactRequest(contactEmail); + + using var responseContent = await Feature.LoadFileToStringContent(); + + 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.ToDto(), _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() + .Create(request) + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + + result.Should().NotBeNull(); + } + + [Test] + public async Task Create_ShouldFailValidation_WhenEmailIsNotValid([Values(1, 101)] int length) + { + // Arrange + var httpMethod = HttpMethod.Post; + var requestUri = _resourceUri.AbsoluteUri; + + var contactEmail = TestContext.CurrentContext.Random.GetString(length); + var request = new CreateContactRequest(contactEmail); + + 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.ToDto(), _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() + .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 contactId = TestContext.CurrentContext.Random.NextGuid().ToString(); + var requestUri = _resourceUri.Append(contactId).AbsoluteUri; + + using var responseContent = await Feature.LoadFileToStringContent(); + + 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) + .Contact(contactId) + .GetDetails() + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + + result.Should().NotBeNull(); + } + + [Test] + public async Task Update_Success() + { + // Arrange + var httpMethod = HttpMethodEx.Patch; + var contactId = TestContext.CurrentContext.Random.NextGuid().ToString(); + var requestUri = _resourceUri.Append(contactId).AbsoluteUri; + + var updatedEmail = $"{TestContext.CurrentContext.Random.GetString(10)}@mailtrap.io"; + var request = new UpdateContactRequest(updatedEmail); + + using var responseContent = await Feature.LoadFileToStringContent(); + + 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.ToDto(), _jsonSerializerOptions) + .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) + .Contact(contactId) + .Update(request) + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + + result.Should().NotBeNull(); + } + + [Test] + public async Task Update_ShouldFailValidation_WhenEmailIsNotValid([Values(1, 101)] int length) + { + // Arrange + var httpMethod = HttpMethodEx.Patch; + var contactId = TestContext.CurrentContext.Random.NextGuid().ToString(); + var requestUri = _resourceUri.Append(contactId).AbsoluteUri; + + var updatedEmail = TestContext.CurrentContext.Random.GetString(length); + var request = new UpdateContactRequest(updatedEmail); + + 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.ToDto(), _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) + .Contact(contactId) + .Update(request); + + // Assert + await act.Should().ThrowAsync(); + + mockHttp.GetMatchCount(mockedRequest).Should().Be(0); + } + + [Test] + public async Task Delete_Success() + { + // Arrange + var httpMethod = HttpMethod.Delete; + var contactId = TestContext.CurrentContext.Random.NextGuid().ToString(); + var requestUri = _resourceUri.Append(contactId).AbsoluteUri; + + 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.NoContent); + + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddMailtrapClient(_clientConfig) + .ConfigurePrimaryHttpMessageHandler(() => mockHttp); + + using var services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // Act + await client + .Account(_accountId) + .Contact(contactId) + .Delete() + .ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + } +} diff --git a/tests/Mailtrap.IntegrationTests/Contacts/Create_Success.json b/tests/Mailtrap.IntegrationTests/Contacts/Create_Success.json new file mode 100644 index 00000000..b6dd3d8f --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/Contacts/Create_Success.json @@ -0,0 +1,18 @@ +{ + "data": { + "id": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132", + "status": "subscribed", + "email": "john.smith@example.com", + "fields": { + "first_name": "John", + "last_name": "Smith" + }, + "list_ids": [ + 1, + 2, + 3 + ], + "created_at": 1742820600230, + "updated_at": 1742820600230 + } +} diff --git a/tests/Mailtrap.IntegrationTests/Contacts/GetAll_Success.json b/tests/Mailtrap.IntegrationTests/Contacts/GetAll_Success.json new file mode 100644 index 00000000..ac90ac8a --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/Contacts/GetAll_Success.json @@ -0,0 +1,49 @@ +[ + { + "id": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132", + "status": "subscribed", + "email": "john.smith@example.com", + "fields": { + "first_name": "John", + "last_name": "Smith" + }, + "list_ids": [ + 1, + 2, + 3 + ], + "created_at": 1742820600230, + "updated_at": 1742820600230 + }, + { + "id": "7c5a9d84-3f2b-4c1e-9a6b-2f7d3a8c5e10", + "status": "unsubscribed", + "email": "doe.j@example.com", + "fields": { + "first_name": "John", + "last_name": "Doe" + }, + "list_ids": [ + 2, + 3, + 4 + ], + "created_at": 1742820600231, + "updated_at": 1742820600231 + }, + { + "id": "7c1f2e5a-9d3b-4f2c-8a6b-2f1c9e7d4b90", + "email": "7c1f2e5a@example.com", + "fields": { + "first_name": "Alice", + "last_name": "Doe" + }, + "list_ids": [ + 12, + 13, + 14 + ], + "created_at": 1742820600233, + "updated_at": 1742820600233 + } +] diff --git a/tests/Mailtrap.IntegrationTests/Contacts/GetDetails_Success.json b/tests/Mailtrap.IntegrationTests/Contacts/GetDetails_Success.json new file mode 100644 index 00000000..b6dd3d8f --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/Contacts/GetDetails_Success.json @@ -0,0 +1,18 @@ +{ + "data": { + "id": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132", + "status": "subscribed", + "email": "john.smith@example.com", + "fields": { + "first_name": "John", + "last_name": "Smith" + }, + "list_ids": [ + 1, + 2, + 3 + ], + "created_at": 1742820600230, + "updated_at": 1742820600230 + } +} diff --git a/tests/Mailtrap.IntegrationTests/Contacts/Update_Success.json b/tests/Mailtrap.IntegrationTests/Contacts/Update_Success.json new file mode 100644 index 00000000..bbc47512 --- /dev/null +++ b/tests/Mailtrap.IntegrationTests/Contacts/Update_Success.json @@ -0,0 +1,19 @@ +{ + "action": "updated", + "data": { + "id": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132", + "status": "subscribed", + "email": "john.smith@example.com", + "fields": { + "first_name": "John", + "last_name": "Smith" + }, + "list_ids": [ + 1, + 2, + 3 + ], + "created_at": 1740659901189, + "updated_at": 1742903266889 + } +} diff --git a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs index a98cc05e..da82392b 100644 --- a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs +++ b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs @@ -13,6 +13,7 @@ global using Mailtrap.Core.Exceptions; global using Mailtrap.Core.Extensions; global using Mailtrap.Core.Models; +global using Mailtrap.Contacts.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 300d6e78..34d15d5e 100644 --- a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs +++ b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs @@ -17,4 +17,5 @@ internal static class UrlSegmentsTestConstants internal static string EmailsSegment { get; } = "messages"; internal static string AttachmentsSegment { get; } = "attachments"; internal static string SendEmailSegment { get; } = "send"; + internal static string ContactsSegment { get; } = "contacts"; } diff --git a/tests/Mailtrap.UnitTests/Contacts/ContactCollectionResourceTests.cs b/tests/Mailtrap.UnitTests/Contacts/ContactCollectionResourceTests.cs new file mode 100644 index 00000000..f39f7872 --- /dev/null +++ b/tests/Mailtrap.UnitTests/Contacts/ContactCollectionResourceTests.cs @@ -0,0 +1,51 @@ +namespace Mailtrap.UnitTests.Contacts; + + +[TestFixture] +internal sealed class ContactCollectionResourceTests +{ + 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); + + + #region Constructor + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull() + { + // Act + var act = () => new ContactCollectionResource(null!, _resourceUri); + + // Assert + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull() + { + // Act + var act = () => new ContactCollectionResource(_commandFactoryMock, null!); + + // Assert + act.Should().Throw(); + } + + [Test] + public void ResourceUri_ShouldBeInitializedProperly() + { + // Arrange + var client = CreateResource(); + + // Assert + client.ResourceUri.Should().Be(_resourceUri); + } + + #endregion + + private ContactCollectionResource CreateResource() => new(_commandFactoryMock, _resourceUri); +} diff --git a/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs b/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs new file mode 100644 index 00000000..f8ef8e6f --- /dev/null +++ b/tests/Mailtrap.UnitTests/Contacts/ContactResourceTests.cs @@ -0,0 +1,51 @@ +namespace Mailtrap.UnitTests.Contacts; + + +[TestFixture] +internal sealed class ContactctResourceTests +{ + 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(TestContext.CurrentContext.Random.NextGuid().ToString()); + + #region Constructor + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull() + { + // Act + var act = () => new ContactResource(null!, _resourceUri); + + // Assert + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull() + { + // Act + var act = () => new ContactResource(_commandFactoryMock, null!); + + // Assert + act.Should().Throw(); + } + + [Test] + public void ResourceUri_ShouldBeInitializedProperly() + { + // Arrange + var client = CreateResource(); + + // Assert + client.ResourceUri.Should().Be(_resourceUri); + } + + #endregion + + private ContactResource CreateResource() => new(_commandFactoryMock, _resourceUri); +} diff --git a/tests/Mailtrap.UnitTests/Contacts/Requests/ContactRequestTests.cs b/tests/Mailtrap.UnitTests/Contacts/Requests/ContactRequestTests.cs new file mode 100644 index 00000000..023cf612 --- /dev/null +++ b/tests/Mailtrap.UnitTests/Contacts/Requests/ContactRequestTests.cs @@ -0,0 +1,52 @@ +namespace Mailtrap.UnitTests.Contacts.Requests; + + +[TestFixture] +internal sealed class ContactRequestTests +{ + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenEmailIsNull() + { + var act = () => new ContactRequest(null!); + + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldThrowArgumentNullException_WhenEmailIsEmpty() + { + var act = () => new ContactRequest(string.Empty); + + act.Should().Throw(); + } + + [Test] + public void Constructor_ShouldInitializeFieldsCorrectly() + { + // Arrange + var email = TestContext.CurrentContext.Random.GetString(5) + + "@" + + TestContext.CurrentContext.Random.GetString(5) + + ".com"; + + // Act + var request = new ContactRequest(email); + + // Assert + request.Email.Should().Be(email); + } + + [Test] + public void Validate_ShouldFail_WhenProvidedEmailLengthIsInvalid([Values(1, 101)] int length) + { + // Arrange + var email = TestContext.CurrentContext.Random.GetString(length); + var request = new ContactRequest(email); + + // Act + var result = request.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + } +} diff --git a/tests/Mailtrap.UnitTests/GlobalUsings.cs b/tests/Mailtrap.UnitTests/GlobalUsings.cs index 63225f87..8523ffb9 100644 --- a/tests/Mailtrap.UnitTests/GlobalUsings.cs +++ b/tests/Mailtrap.UnitTests/GlobalUsings.cs @@ -23,6 +23,8 @@ global using Mailtrap.Core.Models; global using Mailtrap.Core.Rest; global using Mailtrap.Core.Rest.Commands; +global using Mailtrap.Contacts; +global using Mailtrap.Contacts.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 7bbe6829..78a531c0 100644 --- a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs +++ b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs @@ -14,4 +14,5 @@ internal static class UrlSegmentsTestConstants internal static string MessagesSegment { get; } = "messages"; internal static string AttachmentsSegment { get; } = "attachments"; internal static string SendEmailSegment { get; } = "send"; + internal static string ContactsSegment { get; } = "contacts"; }