Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for SMTP imposters. #88

Merged
merged 6 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions MbDotNet.Tests/Acceptance/ImposterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mail;
using System.Net.Mime;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -70,6 +72,7 @@ public async Task CanCreateAndGetHttpsImposter()
await _client.SubmitAsync(imposter);

var retrievedImposter = await _client.GetHttpsImposterAsync(port);

Assert.IsNotNull(retrievedImposter);
}

Expand Down Expand Up @@ -121,6 +124,21 @@ public async Task CanUpdateTcpImposter()
await _client.UpdateImposterAsync(imposter);
}

[TestMethod]
public async Task CanCreateAndGetSmtpImposter()
{
const int port = 6000;
const string name = "TestSmtp";

var imposter = _client.CreateSmtpImposter(port, name, true);

await _client.SubmitAsync(imposter);

var retrievedImposter = await _client.GetSmtpImposterAsync(port);
Assert.IsNotNull(retrievedImposter);
Assert.AreEqual(name, retrievedImposter.Name);
}

[TestMethod]
public async Task CanCreateHttpProxyImposter()
{
Expand Down Expand Up @@ -253,6 +271,55 @@ public async Task CanVerifyCallsOnImposter()
Assert.AreEqual("75", receivedRequest.Headers["Content-Length"]);
}

[TestMethod]
public async Task CanVerifyCallsOnSmtpImposter()
{
const int port = 6000;
const string from = "sender@test.com";
const string to1 = "recipient1@test.com";
const string to2 = "recipient2@test.com";
const string subject = "Test Subject";
const string body = "Test Body";
const string attachmentContent1 = "Test Content1";
const string attachmentContent2 = "Test Content2";

var imposter = _client.CreateSmtpImposter(port, recordRequests: true);
await _client.SubmitAsync(imposter);

var mail = new MailMessage
{
From = new MailAddress(from),
Subject = subject,
Body = body
};
mail.To.Add(to1);
mail.To.Add(to2);

var attachment1 = Attachment.CreateAttachmentFromString(attachmentContent1, new ContentType("text/plain"));
var attachment2 = Attachment.CreateAttachmentFromString(attachmentContent2, new ContentType("text/plain"));

mail.Attachments.Add(attachment1);
mail.Attachments.Add(attachment2);

var smtpClient = new SmtpClient("localhost")
{
Port = port
};

await smtpClient.SendMailAsync(mail);

var retrievedImposter = await _client.GetSmtpImposterAsync(port);

Assert.AreEqual(1, retrievedImposter.NumberOfRequests);
Assert.AreEqual(from, retrievedImposter.Requests.First().EnvelopeFrom);
Assert.AreEqual(to1, retrievedImposter.Requests.First().EnvelopeTo.First());
Assert.AreEqual(to2, retrievedImposter.Requests.First().EnvelopeTo.Last());
Assert.AreEqual(subject, retrievedImposter.Requests.First().Subject);
Assert.AreEqual(body, retrievedImposter.Requests.First().Text);
Assert.AreEqual(attachmentContent1, Encoding.UTF8.GetString(retrievedImposter.Requests.First().Attachments.First().Content.Data));
Assert.AreEqual(attachmentContent2, Encoding.UTF8.GetString(retrievedImposter.Requests.First().Attachments.Last().Content.Data));
}

[TestMethod]
public async Task CanVerifyCallsOnImposterWithDuplicateQueryStringKey()
{
Expand Down
46 changes: 46 additions & 0 deletions MbDotNet.Tests/Models/Imposters/SmtpImposterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using MbDotNet.Models.Imposters;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Tests.Models.Imposters
{
[TestClass, TestCategory("Unit")]
public class SmtpImposterTests
{
#region Constructor Tests

[TestMethod]
public void Constructor_SetsPort()
{
const int port = 123;
var imposter = new SmtpImposter(port, null);
Assert.AreEqual(port, imposter.Port);
}

[TestMethod]
public void Constructor_SetsProtocol()
{
var imposter = new SmtpImposter(123, null);
Assert.AreEqual("smtp", imposter.Protocol);
}

[TestMethod]
public void Constructor_SetsName()
{
const string expectedName = "Service";
var imposter = new SmtpImposter(123, expectedName);
Assert.AreEqual(expectedName, imposter.Name);
}

[TestMethod]
public void Constructor_AllowsNullPort()
{
var imposter = new SmtpImposter(null, null);
Assert.AreEqual(default(int), imposter.Port);
}

#endregion
}
}
29 changes: 29 additions & 0 deletions MbDotNet/IClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ HttpsImposter CreateHttpsImposter(int? port = null, string name = null, string k
TcpImposter CreateTcpImposter(int? port = null, string name = null, TcpMode mode = TcpMode.Text,
bool recordRequests = false, TcpResponseFields defaultResponse = null);

/// <summary>
/// Creates a new imposter on the specified port with the SMTP protocol. The Submit method
/// must be called on the client in order to submit the imposter to Mountebank. If the port
/// is blank, Mountebank will assign one which can be retrieved after Submit. Note that Mountebank does not yet support
/// stubs for SMTP imposters.
/// </summary>
/// <param name="port">
/// The port the imposter will be set up to receive requests on, or null to allow
/// Mountebank to set the port.
/// </param>
/// <param name="name">The name the imposter will recieve, useful for debugging/logging purposes</param>
/// <param name="recordRequests">
/// Enables recording requests to use the imposter as a mock. See
/// <see href="http://www.mbtest.org/docs/api/mocks">here</see> for more details on Mountebank
/// verification.
/// </param>
/// <returns>The newly created imposter</returns>
SmtpImposter CreateSmtpImposter(int? port = null, string name = null, bool recordRequests = false);

/// <summary>
/// Retrieves an HttpImposter along with information about requests made to that
/// imposter if mountebank is running with the "--mock" flag.
Expand Down Expand Up @@ -109,6 +128,16 @@ TcpImposter CreateTcpImposter(int? port = null, string name = null, TcpMode mode
/// <exception cref="MbDotNet.Exceptions.InvalidProtocolException">Thrown if the retrieved imposter was not an HTTP imposter</exception>
Task<RetrievedHttpsImposter> GetHttpsImposterAsync(int port, CancellationToken cancellationToken = default);

/// <summary>
/// Retrieves a SmtpImposter along with information about requests made to that
/// imposter if Mountebank is running with the "--mock" flag.
/// </summary>
/// <param name="port">The port number of the imposter to retrieve</param>
/// <returns>The retrieved imposter</returns>
/// <exception cref="MbDotNet.Exceptions.ImposterNotFoundException">Thrown if no imposter was found on the specified port.</exception>
/// <exception cref="MbDotNet.Exceptions.InvalidProtocolException">Thrown if the retrieved imposter was not an SMTP imposter</exception>
Task<RetrievedSmtpImposter> GetSmtpImposterAsync(int port, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes a single imposter from mountebank. Will also remove the imposter from the collection
/// of imposters that the client maintains.
Expand Down
1 change: 1 addition & 0 deletions MbDotNet/IRequestProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal interface IRequestProxy
Task<RetrievedHttpImposter> GetHttpImposterAsync(int port, CancellationToken cancellationToken = default);
Task<RetrievedTcpImposter> GetTcpImposterAsync(int port, CancellationToken cancellationToken = default);
Task<RetrievedHttpsImposter> GetHttpsImposterAsync(int port, CancellationToken cancellationToken = default);
Task<RetrievedSmtpImposter> GetSmtpImposterAsync(int port, CancellationToken cancellationToken = default);
Task DeleteSavedRequestsAsync(int port, CancellationToken cancellationToken = default);
}
}
12 changes: 12 additions & 0 deletions MbDotNet/Models/Imposters/RetrievedSmtpImposter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using MbDotNet.Models.Requests;
using MbDotNet.Models.Responses.Fields;
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Imposters
{
public class RetrievedSmtpImposter: RetrievedImposter<SmtpRequest, SmtpResponseFields>
{
}
}
14 changes: 14 additions & 0 deletions MbDotNet/Models/Imposters/SmtpImposter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Imposters
{
public class SmtpImposter: Imposter
{
public SmtpImposter(int? port, string name, bool recordRequests = false)
: base(port, Enums.Protocol.Smtp, name, recordRequests)
{
}
}
}
16 changes: 16 additions & 0 deletions MbDotNet/Models/Requests/EmailAddress.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Requests
{
public class EmailAddress
{
[JsonProperty("address")]
public string Address { get; set; }

[JsonProperty("name")]
public string Name { get; set; }
}
}
25 changes: 25 additions & 0 deletions MbDotNet/Models/Requests/EmailAttachment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Requests
{
public class EmailAttachment
{
[JsonProperty("type")]
public string Type { get; internal set; }

[JsonProperty("contentType")]
public string ContentType { get; internal set; }

[JsonProperty("content")]
public EmailContent Content { get; internal set; }

[JsonProperty("contentDisposition")]
public string ContentDisposition { get; internal set; }

[JsonProperty("size")]
public long Size { get; internal set; }
}
}
16 changes: 16 additions & 0 deletions MbDotNet/Models/Requests/EmailContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Requests
{
public class EmailContent
{
[JsonProperty("type")]
public string Type { get; internal set; }

[JsonProperty("data")]
public byte[] Data { get; internal set; }
}
}
46 changes: 46 additions & 0 deletions MbDotNet/Models/Requests/SmtpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Requests
{
public class SmtpRequest: Request
mattherman marked this conversation as resolved.
Show resolved Hide resolved
{
[JsonProperty("envelopeFrom")]
public string EnvelopeFrom { get; internal set; }

[JsonProperty("envelopeTo")]
public IList<string> EnvelopeTo { get; internal set; }
mattherman marked this conversation as resolved.
Show resolved Hide resolved

[JsonProperty("from")]
public EmailAddress From { get; internal set; }

[JsonProperty("to")]
public IList<EmailAddress> To { get; internal set; }

[JsonProperty("cc")]
public IList<EmailAddress> Cc { get; internal set; }

[JsonProperty("bcc")]
public IList<EmailAddress> Bcc { get; internal set; }

[JsonProperty("subject")]
public string Subject { get; internal set; }

[JsonProperty("priority")]
public string Priority { get; internal set; }

[JsonProperty("inReplyTo")]
public IList<EmailAddress> InReplyTo { get; internal set; }

[JsonProperty("text")]
public string Text { get; internal set; }

[JsonProperty("html")]
public string Html { get; internal set; }

[JsonProperty("attachments")]
public IList<EmailAttachment> Attachments { get; internal set; }
}
}
10 changes: 10 additions & 0 deletions MbDotNet/Models/Responses/Fields/SmtpResponseFields.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace MbDotNet.Models.Responses.Fields
{
public class SmtpResponseFields: ResponseFields
{
}
}
39 changes: 39 additions & 0 deletions MbDotNet/MountebankClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ public TcpImposter CreateTcpImposter(int? port = null, string name = null, TcpMo
return new TcpImposter(port, name, mode, recordRequests, defaultResponse);
}

/// <summary>
/// Creates a new imposter on the specified port with the SMTP protocol. The Submit method
/// must be called on the client in order to submit the imposter to Mountebank. If the port
/// is blank, Mountebank will assign one which can be retrieved after Submit. Note that Mountebank does not yet support
/// stubs for SMTP imposters.
/// </summary>
/// <param name="port">
/// The port the imposter will be set up to receive requests on, or null to allow
/// Mountebank to set the port.
/// </param>
/// <param name="name">The name the imposter will recieve, useful for debugging/logging purposes</param>
/// <param name="recordRequests">
/// Enables recording requests to use the imposter as a mock. See
/// <see href="http://www.mbtest.org/docs/api/mocks">here</see> for more details on Mountebank
/// verification.
/// </param>
/// <returns>The newly created imposter</returns>
public SmtpImposter CreateSmtpImposter(int? port = null, string name = null, bool recordRequests = false)
{
return new SmtpImposter(port, name, recordRequests);
}

/// <summary>
/// Retrieves an HttpImposter along with information about requests made to that
/// imposter if mountebank is running with the "--mock" flag.
Expand Down Expand Up @@ -158,6 +180,23 @@ public async Task<RetrievedHttpsImposter> GetHttpsImposterAsync(int port, Cancel
return imposter;
}

/// <summary>
/// Retrieves a SmtpImposter along with information about requests made to that
/// imposter if Mountebank is running with the "--mock" flag.
/// </summary>
/// <param name="port">The port number of the imposter to retrieve</param>
/// <returns>The retrieved imposter</returns>
/// <exception cref="MbDotNet.Exceptions.ImposterNotFoundException">Thrown if no imposter was found on the specified port.</exception>
/// <exception cref="MbDotNet.Exceptions.InvalidProtocolException">Thrown if the retrieved imposter was not an SMTP imposter</exception>
public async Task<RetrievedSmtpImposter> GetSmtpImposterAsync(int port, CancellationToken cancellationToken = default)
{
var imposter = await _requestProxy.GetSmtpImposterAsync(port, cancellationToken).ConfigureAwait(false);

ValidateRetrievedImposterProtocol(imposter, Protocol.Smtp);

return imposter;
}

private static void ValidateRetrievedImposterProtocol<TRequest, TResponseFields>(
RetrievedImposter<TRequest, TResponseFields> imposter, Protocol expectedProtocol)
where TRequest : Request
Expand Down
Loading