diff --git a/Mailtrap.sln b/Mailtrap.sln
index 5bcf15c9..a4b8061b 100644
--- a/Mailtrap.sln
+++ b/Mailtrap.sln
@@ -102,6 +102,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.ContactEve
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.EmailTemplates", "examples\Mailtrap.Example.EmailTemplates\Mailtrap.Example.EmailTemplates.csproj", "{A52169E1-8653-4B7D-9F14-84837E237E43}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.Templates", "examples\Mailtrap.Example.Templates\Mailtrap.Example.Templates.csproj", "{9A59D8CF-688C-4DA6-9CF6-F9C892783910}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.Email.BatchSend", "examples\Mailtrap.Example.Email.BatchSend\Mailtrap.Example.Email.BatchSend.csproj", "{9922946A-03DA-4AC6-8AA6-ADEC5EF2E9EB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.ContactExports", "examples\Mailtrap.Example.ContactExports\Mailtrap.Example.ContactExports.csproj", "{8791C343-7B1E-4206-A954-A0CFD573E460}"
@@ -422,6 +424,18 @@ Global
{A52169E1-8653-4B7D-9F14-84837E237E43}.Release|x64.Build.0 = Release|Any CPU
{A52169E1-8653-4B7D-9F14-84837E237E43}.Release|x86.ActiveCfg = Release|Any CPU
{A52169E1-8653-4B7D-9F14-84837E237E43}.Release|x86.Build.0 = Release|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Debug|x64.Build.0 = Debug|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Debug|x86.Build.0 = Debug|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Release|x64.ActiveCfg = Release|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Release|x64.Build.0 = Release|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Release|x86.ActiveCfg = Release|Any CPU
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910}.Release|x86.Build.0 = Release|Any CPU
{9922946A-03DA-4AC6-8AA6-ADEC5EF2E9EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9922946A-03DA-4AC6-8AA6-ADEC5EF2E9EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9922946A-03DA-4AC6-8AA6-ADEC5EF2E9EB}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -583,6 +597,7 @@ Global
{5CEEEC08-7F1F-4F75-BC8D-6E80C54C21FD} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{AA91FC07-FE50-4DE1-A388-7AA0DB35C84A} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{A52169E1-8653-4B7D-9F14-84837E237E43} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
+ {9A59D8CF-688C-4DA6-9CF6-F9C892783910} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{9922946A-03DA-4AC6-8AA6-ADEC5EF2E9EB} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{8791C343-7B1E-4206-A954-A0CFD573E460} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{11894B20-F780-4FC9-8140-3601EBF54C10} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
diff --git a/README.md b/README.md
index b4768b0c..db5f02d1 100644
--- a/README.md
+++ b/README.md
@@ -308,6 +308,7 @@ private static SendEmailRequest TemplateBasedRequest()
### General
+- Templates management (experimental) – [`examples/Mailtrap.Example.Templates`](examples/Mailtrap.Example.Templates/)
- Email Templates management – [`examples/Mailtrap.Example.EmailTemplates`](examples/Mailtrap.Example.EmailTemplates/)
- Account access management – [`examples/Mailtrap.Example.AccountAccess`](examples/Mailtrap.Example.AccountAccess/)
- Permissions management – [`examples/Mailtrap.Example.Permissions`](examples/Mailtrap.Example.Permissions/)
diff --git a/examples/Mailtrap.Example.Templates/Mailtrap.Example.Templates.csproj b/examples/Mailtrap.Example.Templates/Mailtrap.Example.Templates.csproj
new file mode 100644
index 00000000..6b512ec9
--- /dev/null
+++ b/examples/Mailtrap.Example.Templates/Mailtrap.Example.Templates.csproj
@@ -0,0 +1 @@
+
diff --git a/examples/Mailtrap.Example.Templates/Program.cs b/examples/Mailtrap.Example.Templates/Program.cs
new file mode 100644
index 00000000..c4ea854e
--- /dev/null
+++ b/examples/Mailtrap.Example.Templates/Program.cs
@@ -0,0 +1,77 @@
+using Mailtrap;
+using Mailtrap.Accounts;
+using Mailtrap.Templates;
+using Mailtrap.Templates.Models;
+using Mailtrap.Templates.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 templates collection
+ ITemplateCollectionResource templatesResource = accountResource.Templates();
+
+ // Get the first page of templates, 50 per page
+ var filter = new TemplateListFilter { Token = 1, PerPage = 50 };
+ TemplateList page = await templatesResource.GetAll(filter);
+
+ logger.LogInformation("Found {Count} templates on the first page.", page.Data.Count);
+ logger.LogInformation("Next page token: {NextToken}", page.Pagination?.NextToken);
+
+ // Create a dedicated template so the update and delete below never touch an existing one
+ var createTemplateRequest = new CreateTemplateRequest
+ {
+ Name = "MyFirstTemplate",
+ Category = "TestCategory",
+ Subject = "TestSubject",
+ BodyHtml = "This is HTML body
",
+ BodyText = "This is text body"
+ };
+ Template template = await templatesResource.Create(createTemplateRequest);
+ logger.LogInformation("Created template {Name} with Id {Id}.", template.Name, template.Id);
+
+ // Get resource for specific template
+ ITemplateResource templateResource = accountResource.Template(template.Id);
+
+ // Get details
+ Template templateResponse = await templateResource.GetDetails();
+ logger.LogInformation("Template Name from resource: {Name}", templateResponse.Name);
+ logger.LogInformation("Template Id from resource: {Id}", templateResponse.Id);
+
+ // Update template details, only the properties that are set are changed
+ var updateTemplateRequest = new UpdateTemplateRequest
+ {
+ Name = "updatedTemplate",
+ BodyHtml = "This is updated HTML body
"
+ };
+ Template updateTemplateResponse = await templateResource.Update(updateTemplateRequest);
+ logger.LogInformation("Updated Template: Name={Name}, Id={Id}", updateTemplateResponse.Name, updateTemplateResponse.Id);
+
+ // Delete template
+ // Beware that template resource becomes invalid after deletion and should not be used anymore
+ await templateResource.Delete();
+ logger.LogInformation("Template Deleted.");
+}
+catch (Exception ex)
+{
+ logger.LogError(ex, "An error occurred during API call.");
+ Environment.ExitCode = 1;
+ return;
+}
diff --git a/examples/Mailtrap.Example.Templates/Properties/launchSettings.json b/examples/Mailtrap.Example.Templates/Properties/launchSettings.json
new file mode 100644
index 00000000..b8daf491
--- /dev/null
+++ b/examples/Mailtrap.Example.Templates/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Project": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/Mailtrap.Example.Templates/appsettings.json b/examples/Mailtrap.Example.Templates/appsettings.json
new file mode 100644
index 00000000..7cf4089d
--- /dev/null
+++ b/examples/Mailtrap.Example.Templates/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 6ddfad51..bf27a78c 100644
--- a/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs
+++ b/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs
@@ -162,6 +162,9 @@ public interface IAccountResource : IRestResource
///
/// Gets email template collection resource for the account, represented by this resource instance.
///
+ ///
+ /// For the paginated, experimental /api/templates endpoints, see .
+ ///
///
///
/// Email template collection resource for the account, represented by this resource instance.
@@ -185,6 +188,42 @@ public interface IAccountResource : IRestResource
///
public IEmailTemplateResource EmailTemplate(long emailTemplateId);
+ ///
+ /// Gets template collection resource for the account, represented by this resource instance.
+ ///
+ ///
+ ///
+ /// The /api/templates endpoints are experimental: their request and response shapes may change
+ /// before general availability.
+ ///
+ ///
+ ///
+ /// Template collection resource for the account, represented by this resource instance.
+ ///
+ public ITemplateCollectionResource Templates();
+
+ ///
+ /// Gets resource for specific template, identified by .
+ ///
+ ///
+ ///
+ /// The /api/templates endpoints are experimental: their request and response shapes may change
+ /// before general availability.
+ ///
+ ///
+ ///
+ /// ID of template to get resource for.
+ ///
+ ///
+ ///
+ /// Resource for the template with specified ID.
+ ///
+ ///
+ ///
+ /// When is less than or equal to zero.
+ ///
+ public ITemplateResource Template(long templateId);
+
///
/// Gets suppression collection resource for the account, represented by this resource instance.
///
diff --git a/src/Mailtrap.Abstractions/GlobalUsings.cs b/src/Mailtrap.Abstractions/GlobalUsings.cs
index 72c1c1ae..b32001a5 100644
--- a/src/Mailtrap.Abstractions/GlobalUsings.cs
+++ b/src/Mailtrap.Abstractions/GlobalUsings.cs
@@ -95,6 +95,10 @@
global using Mailtrap.Suppressions.Models;
global using Mailtrap.Suppressions.Requests;
global using Mailtrap.Suppressions.Validators;
+global using Mailtrap.Templates;
+global using Mailtrap.Templates.Models;
+global using Mailtrap.Templates.Requests;
+global using Mailtrap.Templates.Validators;
global using Mailtrap.TrackingOptOuts;
global using Mailtrap.TrackingOptOuts.Models;
global using Mailtrap.TrackingOptOuts.Requests;
diff --git a/src/Mailtrap.Abstractions/Templates/ITemplateCollectionResource.cs b/src/Mailtrap.Abstractions/Templates/ITemplateCollectionResource.cs
new file mode 100644
index 00000000..feb76eab
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/ITemplateCollectionResource.cs
@@ -0,0 +1,45 @@
+namespace Mailtrap.Templates;
+
+
+///
+/// Represents the templates collection resource, served by the paginated /api/templates endpoints.
+///
+///
+/// The endpoints are experimental: their request and response shapes may change before general availability.
+///
+public interface ITemplateCollectionResource : IRestResource
+{
+ ///
+ /// Gets a page of template details.
+ ///
+ ///
+ ///
+ /// Pagination parameters, or to get the first page with the default page size.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Page of template details with pagination metadata.
+ ///
+ public Task GetAll(TemplateListFilter? filter = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a new template with details specified by .
+ ///
+ ///
+ ///
+ /// Request containing template details for creation.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Created template details.
+ ///
+ public Task Create(CreateTemplateRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/src/Mailtrap.Abstractions/Templates/ITemplateResource.cs b/src/Mailtrap.Abstractions/Templates/ITemplateResource.cs
new file mode 100644
index 00000000..0dd23607
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/ITemplateResource.cs
@@ -0,0 +1,61 @@
+namespace Mailtrap.Templates;
+
+
+///
+/// Represents Template resource, served by the /api/templates endpoints.
+///
+///
+/// The endpoints are experimental: their request and response shapes may change before general availability.
+///
+public interface ITemplateResource : IRestResource
+{
+ ///
+ /// Gets details of the template, represented by the current resource instance.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Requested template details.
+ ///
+ public Task GetDetails(CancellationToken cancellationToken = default);
+
+ ///
+ /// Updates the template, represented by the current resource instance, with details specified by .
+ ///
+ ///
+ ///
+ /// Template details for update.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Updated template details.
+ ///
+ public Task Update(UpdateTemplateRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Deletes the template, represented by the current resource instance.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Nothing is returned upon successful deletion.
+ ///
+ ///
+ ///
+ ///
+ /// After deletion of the template, represented by the current resource instance, it will no longer be available.
+ /// Thus any further operations on it will result in an error.
+ ///
+ ///
+ public Task Delete(CancellationToken cancellationToken = default);
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Models/Template.cs b/src/Mailtrap.Abstractions/Templates/Models/Template.cs
new file mode 100644
index 00000000..bc44998b
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Models/Template.cs
@@ -0,0 +1,114 @@
+namespace Mailtrap.Templates.Models;
+
+///
+/// Represents Template details.
+///
+public sealed record Template
+{
+ ///
+ /// Gets or sets template identifier.
+ ///
+ ///
+ ///
+ /// Template identifier.
+ ///
+ [JsonPropertyName("id")]
+ [JsonPropertyOrder(1)]
+ public long Id { get; set; }
+
+ ///
+ /// Gets or sets template unique identifier.
+ ///
+ ///
+ ///
+ /// Template unique identifier.
+ ///
+ [JsonPropertyName("uuid")]
+ [JsonPropertyOrder(2)]
+ public string Uuid { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets template name.
+ ///
+ ///
+ /// Template name must be no longer than 255 characters.
+ ///
+ ///
+ /// Template name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(3)]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the template category.
+ ///
+ ///
+ /// Template category must be no longer than 255 characters.
+ ///
+ ///
+ /// Template category.
+ ///
+ [JsonPropertyName("category")]
+ [JsonPropertyOrder(4)]
+ public string Category { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the template subject.
+ ///
+ ///
+ /// Template subject must be no longer than 255 characters.
+ ///
+ ///
+ /// Template subject.
+ ///
+ [JsonPropertyName("subject")]
+ [JsonPropertyOrder(5)]
+ public string Subject { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the template body text.
+ ///
+ ///
+ /// Template body text must be no longer than 10_000_000 characters.
+ ///
+ ///
+ /// Template's body text, or when the template has none.
+ ///
+ [JsonPropertyName("body_text")]
+ [JsonPropertyOrder(6)]
+ public string? BodyText { get; set; }
+
+ ///
+ /// Gets or sets the template body HTML.
+ ///
+ ///
+ /// Template body HTML must be no longer than 10_000_000 characters.
+ ///
+ ///
+ /// Template's body HTML, or when the template has none.
+ ///
+ [JsonPropertyName("body_html")]
+ [JsonPropertyOrder(7)]
+ public string? BodyHtml { get; set; }
+
+ ///
+ /// Gets or sets the template creation date and time.
+ ///
+ ///
+ /// Template creation date and time.
+ ///
+ [JsonPropertyName("created_at")]
+ [JsonPropertyOrder(8)]
+ public DateTimeOffset? CreatedAt { get; set; }
+
+ ///
+ /// Gets or sets the template date and time of update.
+ ///
+ ///
+ /// Template date and time of update.
+ ///
+ [JsonPropertyName("updated_at")]
+ [JsonPropertyOrder(9)]
+ public DateTimeOffset? UpdatedAt { get; set; }
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Models/TemplateList.cs b/src/Mailtrap.Abstractions/Templates/Models/TemplateList.cs
new file mode 100644
index 00000000..03539b52
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Models/TemplateList.cs
@@ -0,0 +1,31 @@
+namespace Mailtrap.Templates.Models;
+
+
+///
+/// Represents a paginated page of templates.
+///
+public sealed record TemplateList
+{
+ ///
+ /// Gets the page of templates.
+ ///
+ ///
+ ///
+ /// Page of templates.
+ ///
+ [JsonPropertyName("data")]
+ [JsonPropertyOrder(1)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Data { get; } = [];
+
+ ///
+ /// Gets or sets the page-token pagination metadata.
+ ///
+ ///
+ ///
+ /// Pagination metadata, or when not present.
+ ///
+ [JsonPropertyName("pagination")]
+ [JsonPropertyOrder(2)]
+ public Pagination? Pagination { get; set; }
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Models/TemplateListFilter.cs b/src/Mailtrap.Abstractions/Templates/Models/TemplateListFilter.cs
new file mode 100644
index 00000000..f5a60650
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Models/TemplateListFilter.cs
@@ -0,0 +1,27 @@
+namespace Mailtrap.Templates.Models;
+
+
+///
+/// Represents a set of pagination parameters for listing templates.
+///
+public sealed record TemplateListFilter
+{
+ ///
+ /// Gets or sets the page number to retrieve (page-token pagination).
+ ///
+ ///
+ ///
+ /// Page number to retrieve. Defaults to 1 when not specified.
+ ///
+ public int? Token { get; set; }
+
+ ///
+ /// Gets or sets the number of templates per page.
+ ///
+ ///
+ ///
+ /// Number of templates per page. The API clamps the value rather than rejecting it:
+ /// below 1 becomes 50 and above 100 becomes 100.
+ ///
+ public int? PerPage { get; set; }
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Requests/CreateTemplateRequest.cs b/src/Mailtrap.Abstractions/Templates/Requests/CreateTemplateRequest.cs
new file mode 100644
index 00000000..b9a05abf
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Requests/CreateTemplateRequest.cs
@@ -0,0 +1,88 @@
+namespace Mailtrap.Templates.Requests;
+
+
+///
+/// Request object for creating a template.
+/// The request body is sent flat (no envelope).
+///
+public sealed record CreateTemplateRequest : IValidatable
+{
+ ///
+ /// Gets or sets the template name. Required.
+ ///
+ ///
+ /// Template name must be no longer than 255 characters.
+ ///
+ ///
+ /// Template name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the template category. Required.
+ ///
+ ///
+ /// Template category must be no longer than 255 characters.
+ ///
+ ///
+ /// Template category.
+ ///
+ [JsonPropertyName("category")]
+ [JsonPropertyOrder(2)]
+ [JsonRequired]
+ public string? Category { get; set; }
+
+ ///
+ /// Gets or sets the template subject. Required.
+ ///
+ ///
+ /// Template subject must be no longer than 255 characters.
+ ///
+ ///
+ /// Template subject.
+ ///
+ [JsonPropertyName("subject")]
+ [JsonPropertyOrder(3)]
+ [JsonRequired]
+ public string? Subject { get; set; }
+
+ ///
+ /// Gets or sets the template body HTML.
+ ///
+ ///
+ /// Template body HTML must be no longer than 10_000_000 characters.
+ ///
+ ///
+ /// Template body HTML, or to omit it.
+ ///
+ [JsonPropertyName("body_html")]
+ [JsonPropertyOrder(4)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? BodyHtml { get; set; }
+
+ ///
+ /// Gets or sets the template body text.
+ ///
+ ///
+ /// Template body text must be no longer than 10_000_000 characters.
+ ///
+ ///
+ /// Template body text, or to omit it.
+ ///
+ [JsonPropertyName("body_text")]
+ [JsonPropertyOrder(5)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? BodyText { get; set; }
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return CreateTemplateRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Requests/UpdateTemplateRequest.cs b/src/Mailtrap.Abstractions/Templates/Requests/UpdateTemplateRequest.cs
new file mode 100644
index 00000000..1ea1d71c
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Requests/UpdateTemplateRequest.cs
@@ -0,0 +1,91 @@
+namespace Mailtrap.Templates.Requests;
+
+
+///
+/// Request object for updating a template.
+/// The request body is sent flat (no envelope). All properties are optional -
+/// only properties set on the request are sent and changed.
+///
+public sealed record UpdateTemplateRequest : IValidatable
+{
+ ///
+ /// Gets or sets the template name.
+ ///
+ ///
+ /// Template name must be no longer than 255 characters.
+ ///
+ ///
+ /// Template name, or to leave unchanged.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(1)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the template category.
+ ///
+ ///
+ /// Template category must be no longer than 255 characters.
+ ///
+ ///
+ /// Template category, or to leave unchanged.
+ ///
+ [JsonPropertyName("category")]
+ [JsonPropertyOrder(2)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Category { get; set; }
+
+ ///
+ /// Gets or sets the template subject.
+ ///
+ ///
+ /// Template subject must be no longer than 255 characters.
+ ///
+ ///
+ /// Template subject, or to leave unchanged.
+ ///
+ [JsonPropertyName("subject")]
+ [JsonPropertyOrder(3)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Subject { get; set; }
+
+ ///
+ /// Gets or sets the template body HTML.
+ ///
+ ///
+ /// Template body HTML must be no longer than 10_000_000 characters.
+ ///
+ ///
+ /// Template body HTML, or to leave unchanged.
+ /// An empty string clears the body HTML.
+ ///
+ [JsonPropertyName("body_html")]
+ [JsonPropertyOrder(4)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? BodyHtml { get; set; }
+
+ ///
+ /// Gets or sets the template body text.
+ ///
+ ///
+ /// Template body text must be no longer than 10_000_000 characters.
+ ///
+ ///
+ /// Template body text, or to leave unchanged.
+ /// An empty string clears the body text.
+ ///
+ [JsonPropertyName("body_text")]
+ [JsonPropertyOrder(5)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? BodyText { get; set; }
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return UpdateTemplateRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Validators/CreateTemplateRequestValidator.cs b/src/Mailtrap.Abstractions/Templates/Validators/CreateTemplateRequestValidator.cs
new file mode 100644
index 00000000..049c67b6
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Validators/CreateTemplateRequestValidator.cs
@@ -0,0 +1,27 @@
+namespace Mailtrap.Templates.Validators;
+
+
+///
+/// Validator for .
+/// Ensures Name, Category, Subject are not empty and do not exceed 255 characters each,
+/// and BodyHtml and BodyText do not exceed 10,000,000 characters each if provided.
+///
+public sealed class CreateTemplateRequestValidator : AbstractValidator
+{
+ ///
+ /// Static validator instance for reuse.
+ ///
+ public static CreateTemplateRequestValidator Instance { get; } = new();
+
+ ///
+ /// Primary constructor.
+ ///
+ public CreateTemplateRequestValidator()
+ {
+ RuleFor(r => r.Name).NotEmpty().MaximumLength(255);
+ RuleFor(r => r.Category).NotEmpty().MaximumLength(255);
+ RuleFor(r => r.Subject).NotEmpty().MaximumLength(255);
+ RuleFor(r => r.BodyHtml).MaximumLength(10_000_000);
+ RuleFor(r => r.BodyText).MaximumLength(10_000_000);
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Templates/Validators/UpdateTemplateRequestValidator.cs b/src/Mailtrap.Abstractions/Templates/Validators/UpdateTemplateRequestValidator.cs
new file mode 100644
index 00000000..0a6899ed
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Templates/Validators/UpdateTemplateRequestValidator.cs
@@ -0,0 +1,27 @@
+namespace Mailtrap.Templates.Validators;
+
+
+///
+/// Validator for .
+/// All fields are optional; ensures provided Name, Category, Subject are not empty and do not exceed 255 characters each,
+/// and provided BodyHtml and BodyText do not exceed 10,000,000 characters each.
+///
+public sealed class UpdateTemplateRequestValidator : AbstractValidator
+{
+ ///
+ /// Static validator instance for reuse.
+ ///
+ public static UpdateTemplateRequestValidator Instance { get; } = new();
+
+ ///
+ /// Primary constructor.
+ ///
+ public UpdateTemplateRequestValidator()
+ {
+ When(r => r.Name is not null, () => RuleFor(r => r.Name).NotEmpty().MaximumLength(255));
+ When(r => r.Category is not null, () => RuleFor(r => r.Category).NotEmpty().MaximumLength(255));
+ When(r => r.Subject is not null, () => RuleFor(r => r.Subject).NotEmpty().MaximumLength(255));
+ When(r => r.BodyHtml is not null, () => RuleFor(r => r.BodyHtml).MaximumLength(10_000_000));
+ When(r => r.BodyText is not null, () => RuleFor(r => r.BodyText).MaximumLength(10_000_000));
+ }
+}
diff --git a/src/Mailtrap/Accounts/AccountResource.cs b/src/Mailtrap/Accounts/AccountResource.cs
index 2b596b47..8d7f3945 100644
--- a/src/Mailtrap/Accounts/AccountResource.cs
+++ b/src/Mailtrap/Accounts/AccountResource.cs
@@ -84,6 +84,20 @@ public IEmailTemplateResource EmailTemplate(long emailTemplateId)
#endregion
+ #region Templates
+
+ public ITemplateCollectionResource Templates()
+ => new TemplateCollectionResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.TemplatesSegment));
+
+ public ITemplateResource Template(long templateId)
+ {
+ Ensure.GreaterThanZero(templateId, nameof(templateId));
+
+ return new TemplateResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.TemplatesSegment).Append(templateId));
+ }
+
+ #endregion
+
#region Suppressions
public ISuppressionCollectionResource Suppressions()
diff --git a/src/Mailtrap/Core/Constants/UrlSegments.cs b/src/Mailtrap/Core/Constants/UrlSegments.cs
index 74541a68..ebff69bd 100644
--- a/src/Mailtrap/Core/Constants/UrlSegments.cs
+++ b/src/Mailtrap/Core/Constants/UrlSegments.cs
@@ -15,6 +15,7 @@ internal static class UrlSegments
internal static string InboxesSegment { get; } = "inboxes";
internal static string ContactsSegment { get; } = "contacts";
internal static string EmailTemplatesSegment { get; } = "email_templates";
+ internal static string TemplatesSegment { get; } = "templates";
internal static string SuppressionsSegment { get; } = "suppressions";
internal static string TrackingOptOutsSegment { get; } = "tracking_opt_outs";
internal static string StatsSegment { get; } = "stats";
diff --git a/src/Mailtrap/GlobalUsings.cs b/src/Mailtrap/GlobalUsings.cs
index b5fc327f..d5df82ab 100644
--- a/src/Mailtrap/GlobalUsings.cs
+++ b/src/Mailtrap/GlobalUsings.cs
@@ -92,6 +92,10 @@
global using Mailtrap.Suppressions.Models;
global using Mailtrap.Suppressions.Requests;
global using Mailtrap.Suppressions.Responses;
+global using Mailtrap.Templates;
+global using Mailtrap.Templates.Models;
+global using Mailtrap.Templates.Requests;
+global using Mailtrap.Templates.Responses;
global using Mailtrap.TrackingOptOuts;
global using Mailtrap.TrackingOptOuts.Models;
global using Mailtrap.TrackingOptOuts.Requests;
diff --git a/src/Mailtrap/Templates/Responses/TemplateListResponseDto.cs b/src/Mailtrap/Templates/Responses/TemplateListResponseDto.cs
new file mode 100644
index 00000000..42651e2b
--- /dev/null
+++ b/src/Mailtrap/Templates/Responses/TemplateListResponseDto.cs
@@ -0,0 +1,31 @@
+namespace Mailtrap.Templates.Responses;
+
+
+///
+/// Response DTO that unwraps the page of templates from the data envelope
+/// and its pagination metadata.
+///
+internal sealed record TemplateListResponseDto
+{
+ [JsonPropertyName("data")]
+ [JsonPropertyOrder(1)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Data { get; } = [];
+
+ [JsonPropertyName("pagination")]
+ [JsonPropertyOrder(2)]
+ public Pagination? Pagination { get; set; }
+
+
+ public TemplateList FromDto()
+ {
+ var result = new TemplateList { Pagination = Pagination };
+
+ foreach (var template in Data)
+ {
+ result.Data.Add(template);
+ }
+
+ return result;
+ }
+}
diff --git a/src/Mailtrap/Templates/Responses/TemplateResponseDto.cs b/src/Mailtrap/Templates/Responses/TemplateResponseDto.cs
new file mode 100644
index 00000000..9f104782
--- /dev/null
+++ b/src/Mailtrap/Templates/Responses/TemplateResponseDto.cs
@@ -0,0 +1,13 @@
+namespace Mailtrap.Templates.Responses;
+
+
+///
+/// Response DTO that unwraps a single template from the data envelope.
+///
+internal sealed record TemplateResponseDto
+{
+ [JsonPropertyName("data")]
+ [JsonPropertyOrder(1)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public Template Template { get; } = new();
+}
diff --git a/src/Mailtrap/Templates/TemplateCollectionResource.cs b/src/Mailtrap/Templates/TemplateCollectionResource.cs
new file mode 100644
index 00000000..e925fb90
--- /dev/null
+++ b/src/Mailtrap/Templates/TemplateCollectionResource.cs
@@ -0,0 +1,57 @@
+namespace Mailtrap.Templates;
+
+
+internal sealed class TemplateCollectionResource : RestResource, ITemplateCollectionResource
+{
+ private const string TokenQueryParameter = "token";
+ private const string PerPageQueryParameter = "per_page";
+
+
+ public TemplateCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task GetAll(TemplateListFilter? filter = null, CancellationToken cancellationToken = default)
+ {
+ var uri = CreateListUri(filter);
+
+ var response = await RestResourceCommandFactory
+ .CreateGet(uri)
+ .Execute(cancellationToken)
+ .ConfigureAwait(false);
+
+ return response.FromDto();
+ }
+
+ public async Task Create(CreateTemplateRequest request, CancellationToken cancellationToken = default)
+ {
+ Ensure.NotNull(request, nameof(request));
+
+ var response = await Create(request, cancellationToken).ConfigureAwait(false);
+
+ return response.Template;
+ }
+
+
+ private Uri CreateListUri(TemplateListFilter? filter)
+ {
+ var uri = ResourceUri;
+
+ if (filter is null)
+ {
+ return uri;
+ }
+
+ if (filter.Token is not null)
+ {
+ uri = uri.AppendQueryParameter(TokenQueryParameter, filter.Token.Value.ToUriSegment());
+ }
+
+ if (filter.PerPage is not null)
+ {
+ uri = uri.AppendQueryParameter(PerPageQueryParameter, filter.PerPage.Value.ToUriSegment());
+ }
+
+ return uri;
+ }
+}
diff --git a/src/Mailtrap/Templates/TemplateResource.cs b/src/Mailtrap/Templates/TemplateResource.cs
new file mode 100644
index 00000000..fa6e7d17
--- /dev/null
+++ b/src/Mailtrap/Templates/TemplateResource.cs
@@ -0,0 +1,28 @@
+namespace Mailtrap.Templates;
+
+
+internal sealed class TemplateResource : RestResource, ITemplateResource
+{
+ public TemplateResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task GetDetails(CancellationToken cancellationToken = default)
+ {
+ var response = await Get(cancellationToken).ConfigureAwait(false);
+
+ return response.Template;
+ }
+
+ public async Task Update(UpdateTemplateRequest request, CancellationToken cancellationToken = default)
+ {
+ Ensure.NotNull(request, nameof(request));
+
+ var response = await Update(request, cancellationToken).ConfigureAwait(false);
+
+ return response.Template;
+ }
+
+ public async Task Delete(CancellationToken cancellationToken = default)
+ => await DeleteWithStatusCodeResult(cancellationToken).ConfigureAwait(false);
+}
diff --git a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs
index 617d954b..3562f09c 100644
--- a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs
+++ b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs
@@ -52,6 +52,8 @@
global using Mailtrap.Suppressions.Models;
global using Mailtrap.Suppressions.Requests;
global using Mailtrap.Suppressions.Responses;
+global using Mailtrap.Templates.Models;
+global using Mailtrap.Templates.Requests;
global using Mailtrap.TrackingOptOuts.Models;
global using Mailtrap.TrackingOptOuts.Requests;
global using Mailtrap.TrackingOptOuts.Responses;
diff --git a/tests/Mailtrap.IntegrationTests/Templates/Create_Success.json b/tests/Mailtrap.IntegrationTests/Templates/Create_Success.json
new file mode 100644
index 00000000..a1385a5a
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Templates/Create_Success.json
@@ -0,0 +1,13 @@
+{
+ "data": {
+ "id": 26732,
+ "uuid": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a134",
+ "name": "My Template",
+ "subject": "Promotional Email",
+ "category": "Promotional",
+ "body_html": "Template Body
",
+ "body_text": "Template Body",
+ "created_at": "2026-05-05T09:00:00.000Z",
+ "updated_at": "2026-05-05T09:00:00.000Z"
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/Templates/GetAll_Success.json b/tests/Mailtrap.IntegrationTests/Templates/GetAll_Success.json
new file mode 100644
index 00000000..53161ef9
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Templates/GetAll_Success.json
@@ -0,0 +1,35 @@
+{
+ "data": [
+ {
+ "id": 26730,
+ "uuid": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132",
+ "name": "Welcome",
+ "subject": "Welcome aboard",
+ "category": "Onboarding",
+ "body_html": "Welcome
",
+ "body_text": "Welcome",
+ "created_at": "2026-05-01T09:00:00.000Z",
+ "updated_at": "2026-05-02T09:00:00.000Z"
+ },
+ {
+ "id": 26731,
+ "uuid": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a133",
+ "name": "Receipt",
+ "subject": "Your receipt",
+ "category": "Billing",
+ "body_html": "Receipt
",
+ "body_text": "Receipt",
+ "created_at": "2026-05-03T09:00:00.000Z",
+ "updated_at": "2026-05-04T09:00:00.000Z"
+ }
+ ],
+ "pagination": {
+ "token": 1,
+ "prev_token": null,
+ "next_token": 2,
+ "first_url": "https://mailtrap.io/api/accounts/1/templates?token=1",
+ "prev_url": null,
+ "current_url": "https://mailtrap.io/api/accounts/1/templates?token=1",
+ "next_url": "https://mailtrap.io/api/accounts/1/templates?token=2"
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/Templates/GetDetails_NullBody_Success.json b/tests/Mailtrap.IntegrationTests/Templates/GetDetails_NullBody_Success.json
new file mode 100644
index 00000000..58e18946
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Templates/GetDetails_NullBody_Success.json
@@ -0,0 +1,13 @@
+{
+ "data": {
+ "id": 26730,
+ "uuid": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132",
+ "name": "Welcome",
+ "subject": "Welcome aboard",
+ "category": "Onboarding",
+ "body_html": null,
+ "body_text": "Welcome",
+ "created_at": "2026-05-01T09:00:00.000Z",
+ "updated_at": "2026-05-02T09:00:00.000Z"
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/Templates/GetDetails_Success.json b/tests/Mailtrap.IntegrationTests/Templates/GetDetails_Success.json
new file mode 100644
index 00000000..81af1ac1
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Templates/GetDetails_Success.json
@@ -0,0 +1,13 @@
+{
+ "data": {
+ "id": 26730,
+ "uuid": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132",
+ "name": "Welcome",
+ "subject": "Welcome aboard",
+ "category": "Onboarding",
+ "body_html": "Welcome
",
+ "body_text": "Welcome",
+ "created_at": "2026-05-01T09:00:00.000Z",
+ "updated_at": "2026-05-02T09:00:00.000Z"
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/Templates/TemplateIntegrationTests.cs b/tests/Mailtrap.IntegrationTests/Templates/TemplateIntegrationTests.cs
new file mode 100644
index 00000000..3d4188fd
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Templates/TemplateIntegrationTests.cs
@@ -0,0 +1,374 @@
+namespace Mailtrap.IntegrationTests.Templates;
+
+
+[TestFixture]
+internal sealed class TemplateIntegrationTests
+{
+ private const string Feature = "Templates";
+
+ private const string TokenQueryParameter = "token";
+ private const string PerPageQueryParameter = "per_page";
+
+ private readonly long _accountId;
+ private readonly Uri _resourceUri;
+ private readonly MailtrapClientOptions _clientConfig;
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+
+
+ public TemplateIntegrationTests()
+ {
+ var random = TestContext.CurrentContext.Random;
+
+ _accountId = random.NextLong(1, long.MaxValue);
+ _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.AccountsSegment)
+ .Append(_accountId)
+ .Append(UrlSegmentsTestConstants.TemplatesSegment);
+
+ _clientConfig = new MailtrapClientOptions(random.GetString());
+ _jsonSerializerOptions = _clientConfig.ToJsonSerializerOptions();
+ }
+
+
+ [Test]
+ public async Task GetAll_Success()
+ {
+ // Arrange
+ var requestUri = _resourceUri.AbsoluteUri;
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethod.Get, requestUri)
+ .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}")
+ .WithHeaders("Accept", MimeTypes.Application.Json)
+ .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString())
+ .Respond(HttpStatusCode.OK, responseContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var result = await client
+ .Account(_accountId)
+ .Templates()
+ .GetAll()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Data.Should().HaveCount(2);
+
+ var template = result.Data[0];
+ template.Id.Should().Be(26730);
+ template.Uuid.Should().Be("018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132");
+ template.Name.Should().Be("Welcome");
+ template.Subject.Should().Be("Welcome aboard");
+ template.Category.Should().Be("Onboarding");
+ template.BodyHtml.Should().Be("Welcome
");
+ template.BodyText.Should().Be("Welcome");
+ template.CreatedAt.Should().NotBeNull();
+ template.UpdatedAt.Should().NotBeNull();
+
+ result.Pagination.Should().NotBeNull();
+ result.Pagination!.Token.Should().Be(1);
+ result.Pagination.NextToken.Should().Be(2);
+ result.Pagination.PrevToken.Should().BeNull();
+ }
+
+ [Test]
+ public async Task GetAll_WithFilter_SerializesPerPageAndToken()
+ {
+ // Arrange
+ var requestUri = _resourceUri.AbsoluteUri;
+
+ var page = 2;
+ var perPage = 25;
+ var filter = new TemplateListFilter { Token = page, PerPage = perPage };
+
+ using var responseContent = await Feature.LoadFileToStringContent(fileName: "GetAll_Success");
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethod.Get, requestUri)
+ .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}")
+ .WithHeaders("Accept", MimeTypes.Application.Json)
+ .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString())
+ .WithQueryString(PerPageQueryParameter, perPage.ToString(CultureInfo.InvariantCulture))
+ .WithQueryString(TokenQueryParameter, page.ToString(CultureInfo.InvariantCulture))
+ .Respond(HttpStatusCode.OK, responseContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var result = await client
+ .Account(_accountId)
+ .Templates()
+ .GetAll(filter)
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Data.Should().HaveCount(2);
+ }
+
+ [Test]
+ public async Task Create_Success()
+ {
+ // Arrange
+ var requestUri = _resourceUri.AbsoluteUri;
+
+ var request = new CreateTemplateRequest
+ {
+ Name = "My Template",
+ Category = "Promotional",
+ Subject = "Promotional Email",
+ BodyHtml = "Template Body
",
+ BodyText = "Template Body"
+ };
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethod.Post, requestUri)
+ .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}")
+ .WithHeaders("Accept", MimeTypes.Application.Json)
+ .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString())
+ // Body must be flat - no "email_template" envelope.
+ .WithJsonContent(request, _jsonSerializerOptions)
+ .Respond(HttpStatusCode.Created, responseContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var result = await client
+ .Account(_accountId)
+ .Templates()
+ .Create(request)
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Id.Should().Be(26732);
+ result.Name.Should().Be("My Template");
+ result.Category.Should().Be("Promotional");
+ result.Subject.Should().Be("Promotional Email");
+ result.BodyHtml.Should().Be("Template Body
");
+ }
+
+ [Test]
+ public async Task Create_ShouldFailValidation_WhenRequestIsNotValid()
+ {
+ // Arrange
+ var requestUri = _resourceUri.AbsoluteUri;
+
+ var request = new CreateTemplateRequest
+ {
+ Name = TestContext.CurrentContext.Random.GetString(256),
+ Category = "Promotional",
+ Subject = "Promotional Email"
+ };
+
+ using var mockHttp = new MockHttpMessageHandler();
+ var mockedRequest = mockHttp
+ .Expect(HttpMethod.Post, requestUri)
+ .Respond(HttpStatusCode.UnprocessableContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var act = () => client
+ .Account(_accountId)
+ .Templates()
+ .Create(request);
+
+ // Assert
+ await act.Should().ThrowAsync();
+
+ mockHttp.GetMatchCount(mockedRequest).Should().Be(0);
+ }
+
+ [Test]
+ public async Task GetDetails_Success()
+ {
+ // Arrange
+ var templateId = 26730;
+ var requestUri = _resourceUri.Append(templateId).AbsoluteUri;
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethod.Get, requestUri)
+ .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}")
+ .WithHeaders("Accept", MimeTypes.Application.Json)
+ .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString())
+ .Respond(HttpStatusCode.OK, responseContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var result = await client
+ .Account(_accountId)
+ .Template(templateId)
+ .GetDetails()
+ .ConfigureAwait(false);
+
+ // Assert - the single template must be unwrapped from the "data" envelope.
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Id.Should().Be(templateId);
+ result.Name.Should().Be("Welcome");
+ result.Subject.Should().Be("Welcome aboard");
+ }
+
+ [Test]
+ public async Task GetDetails_NullBody_Success()
+ {
+ // Arrange
+ var templateId = 26730;
+ var requestUri = _resourceUri.Append(templateId).AbsoluteUri;
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethod.Get, requestUri)
+ .Respond(HttpStatusCode.OK, responseContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var result = await client
+ .Account(_accountId)
+ .Template(templateId)
+ .GetDetails()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.BodyHtml.Should().BeNull();
+ result.BodyText.Should().Be("Welcome");
+ }
+
+ [Test]
+ public async Task Update_Success()
+ {
+ // Arrange
+ var templateId = 26730;
+ var requestUri = _resourceUri.Append(templateId).AbsoluteUri;
+
+ var request = new UpdateTemplateRequest { Name = "Welcome v2" };
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethodEx.Patch, requestUri)
+ .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}")
+ .WithHeaders("Accept", MimeTypes.Application.Json)
+ .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString())
+ // Body must be flat and carry only the changed fields.
+ .WithContent("{\"name\":\"Welcome v2\"}")
+ .Respond(HttpStatusCode.OK, responseContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var result = await client
+ .Account(_accountId)
+ .Template(templateId)
+ .Update(request)
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Id.Should().Be(templateId);
+ result.Name.Should().Be("Welcome v2");
+ }
+
+ [Test]
+ public async Task Update_ShouldFailValidation_WhenRequestIsNotValid()
+ {
+ // Arrange
+ var templateId = 26730;
+ var requestUri = _resourceUri.Append(templateId).AbsoluteUri;
+
+ var request = new UpdateTemplateRequest { Subject = string.Empty };
+
+ using var mockHttp = new MockHttpMessageHandler();
+ var mockedRequest = mockHttp
+ .Expect(HttpMethodEx.Patch, requestUri)
+ .Respond(HttpStatusCode.UnprocessableContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ var act = () => client
+ .Account(_accountId)
+ .Template(templateId)
+ .Update(request);
+
+ // Assert
+ await act.Should().ThrowAsync();
+
+ mockHttp.GetMatchCount(mockedRequest).Should().Be(0);
+ }
+
+ [Test]
+ public async Task Delete_Success()
+ {
+ // Arrange
+ var templateId = 26730;
+ var requestUri = _resourceUri.Append(templateId).AbsoluteUri;
+
+ using var mockHttp = new MockHttpMessageHandler();
+ mockHttp
+ .Expect(HttpMethod.Delete, requestUri)
+ .WithHeaders("Authorization", $"Bearer {_clientConfig.ApiToken}")
+ .WithHeaders("Accept", MimeTypes.Application.Json)
+ .WithHeaders("User-Agent", HeaderValues.UserAgent.ToString())
+ .Respond(HttpStatusCode.NoContent);
+
+ using var services = BuildServices(mockHttp);
+ var client = services.GetRequiredService();
+
+ // Act
+ await client
+ .Account(_accountId)
+ .Template(templateId)
+ .Delete()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+ }
+
+
+ private ServiceProvider BuildServices(MockHttpMessageHandler mockHttp)
+ {
+ var serviceCollection = new ServiceCollection();
+
+ serviceCollection
+ .AddMailtrapClient(_clientConfig)
+ .ConfigurePrimaryHttpMessageHandler(() => mockHttp);
+
+ return serviceCollection.BuildServiceProvider();
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/Templates/Update_Success.json b/tests/Mailtrap.IntegrationTests/Templates/Update_Success.json
new file mode 100644
index 00000000..75024aab
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Templates/Update_Success.json
@@ -0,0 +1,13 @@
+{
+ "data": {
+ "id": 26730,
+ "uuid": "018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132",
+ "name": "Welcome v2",
+ "subject": "Welcome aboard",
+ "category": "Onboarding",
+ "body_html": "Welcome
",
+ "body_text": "Welcome",
+ "created_at": "2026-05-01T09:00:00.000Z",
+ "updated_at": "2026-05-06T09:00:00.000Z"
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs
index 1acd7433..39f7156d 100644
--- a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs
+++ b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs
@@ -19,6 +19,7 @@ internal static class UrlSegmentsTestConstants
internal static string EmailsSegment { get; } = "messages";
internal static string AttachmentsSegment { get; } = "attachments";
internal static string EmailTemplatesSegment { get; } = "email_templates";
+ internal static string TemplatesSegment { get; } = "templates";
internal static string SuppressionsSegment { get; } = "suppressions";
internal static string TrackingOptOutsSegment { get; } = "tracking_opt_outs";
internal static string SendEmailSegment { get; } = "send";
diff --git a/tests/Mailtrap.UnitTests/Accounts/AccountResourceTests.cs b/tests/Mailtrap.UnitTests/Accounts/AccountResourceTests.cs
index d32765bc..94e0663d 100644
--- a/tests/Mailtrap.UnitTests/Accounts/AccountResourceTests.cs
+++ b/tests/Mailtrap.UnitTests/Accounts/AccountResourceTests.cs
@@ -369,6 +369,52 @@ public void EmailTemplate_ShouldThrowOutOfRangeException_WhenIdIsEqualOrLessThan
#endregion
+ #region Templates
+
+ [Test]
+ public void Templates_ShouldReturnTemplateCollectionResource()
+ {
+ // Arrange
+ var client = CreateResource();
+
+ // Act
+ var result = client.Templates();
+
+ // Assert
+ ResourceValidator.Validate(
+ result, client.ResourceUri.Append(UrlSegmentsTestConstants.TemplatesSegment));
+ }
+
+ [Test]
+ public void Template_ShouldReturnTemplateResource()
+ {
+ // Arrange
+ var client = CreateResource();
+ var templateId = TestContext.CurrentContext.Random.NextLong(1, long.MaxValue);
+
+ // Act
+ var result = client.Template(templateId);
+
+ // Assert
+ ResourceValidator.Validate(
+ result, client.ResourceUri.Append(UrlSegmentsTestConstants.TemplatesSegment).Append(templateId));
+ }
+
+ [Test]
+ public void Template_ShouldThrowOutOfRangeException_WhenIdIsEqualOrLessThanZero([Values(0, -1)] long templateId)
+ {
+ // Arrange
+ var client = CreateResource();
+
+ // Act
+ var act = () => client.Template(templateId);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ #endregion
+
#region Suppressions
[Test]
diff --git a/tests/Mailtrap.UnitTests/GlobalUsings.cs b/tests/Mailtrap.UnitTests/GlobalUsings.cs
index 1e70d6ab..420860a5 100644
--- a/tests/Mailtrap.UnitTests/GlobalUsings.cs
+++ b/tests/Mailtrap.UnitTests/GlobalUsings.cs
@@ -75,6 +75,9 @@
global using Mailtrap.Suppressions.Models;
global using Mailtrap.Suppressions.Requests;
global using Mailtrap.Suppressions.Validators;
+global using Mailtrap.Templates;
+global using Mailtrap.Templates.Requests;
+global using Mailtrap.Templates.Validators;
global using Mailtrap.TrackingOptOuts;
global using Mailtrap.TrackingOptOuts.Requests;
global using Mailtrap.TrackingOptOuts.Validators;
diff --git a/tests/Mailtrap.UnitTests/Templates/Requests/CreateTemplateRequestTests.cs b/tests/Mailtrap.UnitTests/Templates/Requests/CreateTemplateRequestTests.cs
new file mode 100644
index 00000000..01437433
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Templates/Requests/CreateTemplateRequestTests.cs
@@ -0,0 +1,62 @@
+namespace Mailtrap.UnitTests.Templates.Requests;
+
+
+[TestFixture]
+internal sealed class CreateTemplateRequestTests
+{
+ [Test]
+ public void Validate_ShouldDelegateToValidator()
+ {
+ new CreateTemplateRequest { Name = "Welcome", Category = "Onboarding", Subject = "Hello" }
+ .Validate().IsValid.Should().BeTrue();
+
+ new CreateTemplateRequest { Category = "Onboarding", Subject = "Hello" }
+ .Validate().IsValid.Should().BeFalse();
+ }
+
+ [Test]
+ public void Serialize_ShouldProduceFlatBody()
+ {
+ // Arrange
+ var request = new CreateTemplateRequest
+ {
+ Name = "Welcome",
+ Category = "Onboarding",
+ Subject = "Hello",
+ BodyHtml = "Hi
",
+ BodyText = "Hi"
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(request, MailtrapJsonSerializerOptions.Default);
+
+ // Assert
+ json.Should().Be("{\"name\":\"Welcome\",\"category\":\"Onboarding\",\"subject\":\"Hello\",\"body_html\":\"\\u003Ch1\\u003EHi\\u003C/h1\\u003E\",\"body_text\":\"Hi\"}");
+ json.Should().NotContain("email_template");
+ }
+
+ [Test]
+ public void Serialize_ShouldOmitUnsetBodies()
+ {
+ // Arrange
+ var request = new CreateTemplateRequest { Name = "Welcome", Category = "Onboarding", Subject = "Hello" };
+
+ // Act
+ var json = JsonSerializer.Serialize(request, MailtrapJsonSerializerOptions.Default);
+
+ // Assert
+ json.Should().NotContain("body_html");
+ json.Should().NotContain("body_text");
+ }
+
+ [Test]
+ public void Deserialize_ShouldThrow_WhenRequiredFieldIsMissing()
+ {
+ // Act
+ var act = () => JsonSerializer.Deserialize(
+ "{\"name\":\"Welcome\",\"category\":\"Onboarding\"}", MailtrapJsonSerializerOptions.Default);
+
+ // Assert
+ act.Should().Throw();
+ }
+}
diff --git a/tests/Mailtrap.UnitTests/Templates/Requests/UpdateTemplateRequestTests.cs b/tests/Mailtrap.UnitTests/Templates/Requests/UpdateTemplateRequestTests.cs
new file mode 100644
index 00000000..87a0a288
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Templates/Requests/UpdateTemplateRequestTests.cs
@@ -0,0 +1,51 @@
+namespace Mailtrap.UnitTests.Templates.Requests;
+
+
+[TestFixture]
+internal sealed class UpdateTemplateRequestTests
+{
+ [Test]
+ public void Validate_ShouldDelegateToValidator()
+ {
+ new UpdateTemplateRequest().Validate().IsValid.Should().BeTrue();
+
+ new UpdateTemplateRequest { Name = string.Empty }
+ .Validate().IsValid.Should().BeFalse();
+ }
+
+ [Test]
+ public void Serialize_ShouldProduceFlatBody()
+ {
+ // Arrange
+ var request = new UpdateTemplateRequest
+ {
+ Name = "Welcome v2",
+ Category = "Onboarding",
+ Subject = "Hello again",
+ BodyHtml = "Hi
",
+ BodyText = "Hi"
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(request, MailtrapJsonSerializerOptions.Default);
+
+ // Assert
+ json.Should().StartWith("{\"name\":\"Welcome v2\",\"category\":\"Onboarding\",\"subject\":\"Hello again\"");
+ json.Should().Contain("\"body_html\":");
+ json.Should().Contain("\"body_text\":\"Hi\"");
+ json.Should().NotContain("email_template");
+ }
+
+ [Test]
+ public void Serialize_ShouldOmitUnsetFields()
+ {
+ // Arrange
+ var request = new UpdateTemplateRequest { Subject = "Only the subject changes" };
+
+ // Act
+ var json = JsonSerializer.Serialize(request, MailtrapJsonSerializerOptions.Default);
+
+ // Assert
+ json.Should().Be("{\"subject\":\"Only the subject changes\"}");
+ }
+}
diff --git a/tests/Mailtrap.UnitTests/Templates/TemplateCollectionResourceTests.cs b/tests/Mailtrap.UnitTests/Templates/TemplateCollectionResourceTests.cs
new file mode 100644
index 00000000..51beaaf5
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Templates/TemplateCollectionResourceTests.cs
@@ -0,0 +1,71 @@
+namespace Mailtrap.UnitTests.Templates;
+
+
+[TestFixture]
+internal sealed class TemplateCollectionResourceTests
+{
+ private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of();
+
+ private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.AccountsSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong())
+ .Append(UrlSegmentsTestConstants.TemplatesSegment);
+
+
+ #region Constructor
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull()
+ {
+ // Act
+ var act = () => new TemplateCollectionResource(null!, _resourceUri);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull()
+ {
+ // Act
+ var act = () => new TemplateCollectionResource(_commandFactoryMock, null!);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void ResourceUri_ShouldBeInitializedProperly()
+ {
+ // Arrange
+ var client = CreateResource();
+
+ // Assert
+ client.ResourceUri.Should().Be(_resourceUri);
+ }
+
+ #endregion
+
+
+ #region Create
+
+ [Test]
+ public async Task Create_ShouldThrowArgumentNullException_WhenRequestIsNull()
+ {
+ // Arrange
+ var resource = CreateResource();
+
+ // Act
+ var act = () => resource.Create(null!);
+
+ // Assert
+ await act.Should().ThrowAsync();
+ }
+
+ #endregion
+
+
+ private TemplateCollectionResource CreateResource() => new(_commandFactoryMock, _resourceUri);
+}
diff --git a/tests/Mailtrap.UnitTests/Templates/TemplateRequestValidatorTests.cs b/tests/Mailtrap.UnitTests/Templates/TemplateRequestValidatorTests.cs
new file mode 100644
index 00000000..26f06196
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Templates/TemplateRequestValidatorTests.cs
@@ -0,0 +1,137 @@
+namespace Mailtrap.UnitTests.Templates;
+
+
+[TestFixture]
+internal sealed class TemplateRequestValidatorTests
+{
+ private const int MaxShort = 255;
+ private const int MaxBody = 10_000_000;
+
+ private static readonly CreateTemplateRequestValidator s_createValidator = CreateTemplateRequestValidator.Instance;
+ private static readonly UpdateTemplateRequestValidator s_updateValidator = UpdateTemplateRequestValidator.Instance;
+
+
+ #region Create
+
+ [Test]
+ public void Create_WithRequiredFields_ShouldPass()
+ {
+ s_createValidator.TestValidate(ValidCreateRequest()).ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Create_WithMaxLengths_ShouldPass()
+ {
+ var request = new CreateTemplateRequest
+ {
+ Name = new string('a', MaxShort),
+ Category = new string('a', MaxShort),
+ Subject = new string('a', MaxShort),
+ BodyHtml = new string('a', MaxBody),
+ BodyText = new string('a', MaxBody)
+ };
+
+ s_createValidator.TestValidate(request).ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Create_WithMissingRequiredFields_ShouldFail([Values] bool nullValue)
+ {
+ var value = nullValue ? null : string.Empty;
+
+ var request = ValidCreateRequest() with { Name = value, Category = value, Subject = value };
+ var result = s_createValidator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Name);
+ result.ShouldHaveValidationErrorFor(r => r.Category);
+ result.ShouldHaveValidationErrorFor(r => r.Subject);
+ }
+
+ [Test]
+ public void Create_WithTooLongShortFields_ShouldFail()
+ {
+ var tooLong = new string('a', MaxShort + 1);
+
+ var request = ValidCreateRequest() with { Name = tooLong, Category = tooLong, Subject = tooLong };
+ var result = s_createValidator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Name);
+ result.ShouldHaveValidationErrorFor(r => r.Category);
+ result.ShouldHaveValidationErrorFor(r => r.Subject);
+ }
+
+ [Test]
+ public void Create_WithTooLongBodies_ShouldFail()
+ {
+ var tooLong = new string('a', MaxBody + 1);
+
+ var request = ValidCreateRequest() with { BodyHtml = tooLong, BodyText = tooLong };
+ var result = s_createValidator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.BodyHtml);
+ result.ShouldHaveValidationErrorFor(r => r.BodyText);
+ }
+
+ #endregion
+
+
+ #region Update
+
+ [Test]
+ public void Update_WithNoFields_ShouldPass()
+ {
+ s_updateValidator.TestValidate(new UpdateTemplateRequest()).ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Update_WithEmptyShortFields_ShouldFail()
+ {
+ var request = new UpdateTemplateRequest { Name = string.Empty, Category = string.Empty, Subject = string.Empty };
+ var result = s_updateValidator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Name);
+ result.ShouldHaveValidationErrorFor(r => r.Category);
+ result.ShouldHaveValidationErrorFor(r => r.Subject);
+ }
+
+ [Test]
+ public void Update_WithTooLongFields_ShouldFail()
+ {
+ var tooLong = new string('a', MaxShort + 1);
+ var tooLongBody = new string('a', MaxBody + 1);
+
+ var request = new UpdateTemplateRequest
+ {
+ Name = tooLong,
+ Category = tooLong,
+ Subject = tooLong,
+ BodyHtml = tooLongBody,
+ BodyText = tooLongBody
+ };
+ var result = s_updateValidator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Name);
+ result.ShouldHaveValidationErrorFor(r => r.Category);
+ result.ShouldHaveValidationErrorFor(r => r.Subject);
+ result.ShouldHaveValidationErrorFor(r => r.BodyHtml);
+ result.ShouldHaveValidationErrorFor(r => r.BodyText);
+ }
+
+ [Test]
+ public void Update_WithValidSubset_ShouldPass()
+ {
+ var request = new UpdateTemplateRequest { Name = "Renamed", BodyHtml = "Hi
" };
+
+ s_updateValidator.TestValidate(request).ShouldNotHaveAnyValidationErrors();
+ }
+
+ #endregion
+
+
+ private static CreateTemplateRequest ValidCreateRequest() => new()
+ {
+ Name = "Welcome",
+ Category = "Onboarding",
+ Subject = "Hello"
+ };
+}
diff --git a/tests/Mailtrap.UnitTests/Templates/TemplateResourceTests.cs b/tests/Mailtrap.UnitTests/Templates/TemplateResourceTests.cs
new file mode 100644
index 00000000..311da8f4
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Templates/TemplateResourceTests.cs
@@ -0,0 +1,72 @@
+namespace Mailtrap.UnitTests.Templates;
+
+
+[TestFixture]
+internal sealed class TemplateResourceTests
+{
+ private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of();
+
+ private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.AccountsSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong())
+ .Append(UrlSegmentsTestConstants.TemplatesSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong());
+
+
+ #region Constructor
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull()
+ {
+ // Act
+ var act = () => new TemplateResource(null!, _resourceUri);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull()
+ {
+ // Act
+ var act = () => new TemplateResource(_commandFactoryMock, null!);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void ResourceUri_ShouldBeInitializedProperly()
+ {
+ // Arrange
+ var client = CreateResource();
+
+ // Assert
+ client.ResourceUri.Should().Be(_resourceUri);
+ }
+
+ #endregion
+
+
+ #region Update
+
+ [Test]
+ public async Task Update_ShouldThrowArgumentNullException_WhenRequestIsNull()
+ {
+ // Arrange
+ var resource = CreateResource();
+
+ // Act
+ var act = () => resource.Update(null!);
+
+ // Assert
+ await act.Should().ThrowAsync();
+ }
+
+ #endregion
+
+
+ private TemplateResource CreateResource() => new(_commandFactoryMock, _resourceUri);
+}
diff --git a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs
index 3fb6eb4c..998b2bfb 100644
--- a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs
+++ b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs
@@ -14,6 +14,7 @@ internal static class UrlSegmentsTestConstants
internal static string MessagesSegment { get; } = "messages";
internal static string AttachmentsSegment { get; } = "attachments";
internal static string EmailTemplatesSegment { get; } = "email_templates";
+ internal static string TemplatesSegment { get; } = "templates";
internal static string SuppressionsSegment { get; } = "suppressions";
internal static string TrackingOptOutsSegment { get; } = "tracking_opt_outs";
internal static string SendEmailSegment { get; } = "send";