diff --git a/src/Postmark.Tests/ClientMessageStreamTests.cs b/src/Postmark.Tests/ClientMessageStreamTests.cs new file mode 100644 index 0000000..486b88c --- /dev/null +++ b/src/Postmark.Tests/ClientMessageStreamTests.cs @@ -0,0 +1,160 @@ +using Xunit; +using PostmarkDotNet; +using System; +using System.Linq; +using System.Threading.Tasks; +using Postmark.Model.MessageStreams; +using PostmarkDotNet.Model; + +namespace Postmark.Tests +{ + public class ClientMessageStreamTests : ClientBaseFixture, IDisposable + { + private PostmarkAdminClient _adminClient; + private PostmarkServer _server; + + protected override void Setup() + { + _adminClient = new PostmarkAdminClient(WRITE_ACCOUNT_TOKEN, BASE_URL); + _server = TestUtils.MakeSynchronous(() => _adminClient.CreateServerAsync($"integration-test-message-stream-{Guid.NewGuid()}")); + _client = new PostmarkClient(_server.ApiTokens.First(), BASE_URL); + } + + [Fact] + public async Task ClientCanCreateMessageStream() + { + var id = "test-id"; + var streamType = MessageStreamType.Broadcasts; + var streamName = "Test Stream"; + var description = "This is a description."; + + var messageStream = await _client.CreateMessageStream(id, streamType, streamName, description); + + Assert.Equal(id, messageStream.ID); + Assert.Equal(_server.ID, messageStream.ServerID); + Assert.Equal(streamType, messageStream.MessageStreamType); + Assert.Equal(streamName, messageStream.Name); + Assert.Equal(description, messageStream.Description); + Assert.Null(messageStream.UpdatedAt); + Assert.Null(messageStream.ArchivedAt); + } + + [Fact] + public async Task ClientCanEditMessageStream() + { + var messageStream = await CreateDummyMessageStream(MessageStreamType.Broadcasts); + + var newName = "Updated Stream Name"; + var newDescription = "Updated Stream Description"; + var updatedMessageStream = await _client.EditMessageStream(messageStream.ID, newName, newDescription); + + Assert.Equal(newName, updatedMessageStream.Name); + Assert.Equal(newDescription, updatedMessageStream.Description); + Assert.NotNull(updatedMessageStream.UpdatedAt); + } + + [Fact] + public async Task ClientCanGetMessageStream() + { + var expectedMessageStream = await CreateDummyMessageStream(MessageStreamType.Broadcasts); + var actualMessageStream = await _client.GetMessageStream(expectedMessageStream.ID); + + Assert.Equal(expectedMessageStream.ID, actualMessageStream.ID); + Assert.Equal(expectedMessageStream.ServerID, actualMessageStream.ServerID); + Assert.Equal(expectedMessageStream.MessageStreamType, actualMessageStream.MessageStreamType); + Assert.Equal(expectedMessageStream.Name, actualMessageStream.Name); + Assert.Equal(expectedMessageStream.Description, actualMessageStream.Description); + } + + [Fact] + public async Task ClientCanListMessageStreams() + { + var transactionalStream = await CreateDummyMessageStream(MessageStreamType.Transactional); + var broadcastsStream = await CreateDummyMessageStream(MessageStreamType.Broadcasts); + + // Listing All stream types + var listing = await _client.ListMessageStreams(MessageStreamTypeFilter.All); + Assert.Equal(4, listing.TotalCount); // includes the default streams + Assert.Equal(4, listing.MessageStreams.Count()); + Assert.Contains(transactionalStream.ID, listing.MessageStreams.Select(k => k.ID)); + Assert.Contains(broadcastsStream.ID, listing.MessageStreams.Select(k => k.ID)); + + // Filtering by stream type + var filteredListing = await _client.ListMessageStreams(MessageStreamTypeFilter.Transactional); + Assert.Equal(2, filteredListing.TotalCount); // includes default stream + Assert.Equal(2, filteredListing.MessageStreams.Count()); + Assert.Contains(transactionalStream.ID, listing.MessageStreams.Select(k => k.ID)); + } + + [Fact] + public async Task ClientCanListArchivedStreams() + { + var transactionalStream = await CreateDummyMessageStream(MessageStreamType.Broadcasts); + + await _client.ArchiveMessageStream(transactionalStream.ID); + + // By default we are not including archived streams + var filteredListing = await _client.ListMessageStreams(MessageStreamTypeFilter.Broadcasts, includeArchivedStreams: false); + Assert.Equal(0, filteredListing.TotalCount); + Assert.Empty(filteredListing.MessageStreams); + + // Including archived streams + var completeListing = await _client.ListMessageStreams(MessageStreamTypeFilter.Broadcasts, includeArchivedStreams: true); + Assert.Equal(1, completeListing.TotalCount); + Assert.Single(completeListing.MessageStreams); + Assert.Equal(transactionalStream.ID, completeListing.MessageStreams.First().ID); + Assert.NotNull(completeListing.MessageStreams.First().ArchivedAt); + } + + [Fact] + public async Task ClientCanArchiveStreams() + { + var transactionalStream = await CreateDummyMessageStream(MessageStreamType.Transactional); + + var confirmation = await _client.ArchiveMessageStream(transactionalStream.ID); + + Assert.Equal(transactionalStream.ID, confirmation.ID); + Assert.Equal(transactionalStream.ServerID, confirmation.ServerID); + Assert.True(confirmation.ExpectedPurgeDate > DateTime.UtcNow); + + var fetchedMessageStream = await _client.GetMessageStream(transactionalStream.ID); + Assert.NotNull(fetchedMessageStream.ArchivedAt); + } + + [Fact] + public async Task ClientCanUnArchiveStreams() + { + var transactionalStream = await CreateDummyMessageStream(MessageStreamType.Transactional); + + await _client.ArchiveMessageStream(transactionalStream.ID); + + var unarchivedStream = await _client.UnArchiveMessageStream(transactionalStream.ID); + + Assert.Equal(transactionalStream.ID, unarchivedStream.ID); + Assert.Equal(transactionalStream.ServerID, unarchivedStream.ServerID); + Assert.Null(unarchivedStream.ArchivedAt); + } + + private async Task CreateDummyMessageStream(MessageStreamType streamType) + { + var id = $"test-{Guid.NewGuid().ToString().Substring(0, 25)}"; // IDs are only 30 characters long. + var streamName = "Dummy Test Stream"; + var description = "This is a dummy description."; + + return await _client.CreateMessageStream(id, streamType, streamName, description); + } + + private Task Cleanup() + { + return Task.Run(async () => + { + await _adminClient.DeleteServerAsync(_server.ID); + }); + } + + public void Dispose() + { + Cleanup().Wait(); + } + } +} diff --git a/src/Postmark/Model/MessageStreams/MessageStreamType.cs b/src/Postmark/Model/MessageStreams/MessageStreamType.cs new file mode 100644 index 0000000..2da2d41 --- /dev/null +++ b/src/Postmark/Model/MessageStreams/MessageStreamType.cs @@ -0,0 +1,12 @@ +namespace Postmark.Model.MessageStreams +{ + /// + /// Valid types for a message stream. + /// + public enum MessageStreamType + { + Transactional = 0, + Inbound = 1, + Broadcasts = 2 + } +} diff --git a/src/Postmark/Model/MessageStreams/MessageStreamTypeFilter.cs b/src/Postmark/Model/MessageStreams/MessageStreamTypeFilter.cs new file mode 100644 index 0000000..1f915de --- /dev/null +++ b/src/Postmark/Model/MessageStreams/MessageStreamTypeFilter.cs @@ -0,0 +1,10 @@ +namespace Postmark.Model.MessageStreams +{ + /// + /// Valid values for filtering message streams by type. + /// + public enum MessageStreamTypeFilter + { + Transactional, Inbound, Broadcasts, All + } +} diff --git a/src/Postmark/Model/MessageStreams/PostmarkMessageStream.cs b/src/Postmark/Model/MessageStreams/PostmarkMessageStream.cs new file mode 100644 index 0000000..b61c25a --- /dev/null +++ b/src/Postmark/Model/MessageStreams/PostmarkMessageStream.cs @@ -0,0 +1,57 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Postmark.Model.MessageStreams +{ + /// + /// Model representing a message stream. + /// For more information about the MessageStreams API, please visit our API documentation. + /// + public class PostmarkMessageStream + { + /// + /// User defined identifier for this message stream that is unique at the server level. + /// + public string ID { get; set; } + + /// + /// Id of the server this stream belongs to. + /// + public int ServerID { get; set; } + + /// + /// Friendly name of the message stream. + /// + public string Name { get; set; } + + /// + /// Friendly description of the message stream. + /// + public string Description { get; set; } + + /// + /// The type of this message Stream. Can be Transactional, Inbound or Broadcasts. + /// + [JsonConverter(typeof(StringEnumConverter))] + public MessageStreamType MessageStreamType { get; set; } + + /// + /// The date when the message stream was created. + /// + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime CreatedAt { get; set; } + + /// + /// The date when the message stream was last updated. If null, this message stream was never updated. + /// + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime? UpdatedAt { get; set; } + + /// + /// The date when this message stream has been archived. If null, this message stream is not in an archival state. + /// + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime? ArchivedAt { get; set; } + } +} diff --git a/src/Postmark/Model/MessageStreams/PostmarkMessageStreamArchivalConfirmation.cs b/src/Postmark/Model/MessageStreams/PostmarkMessageStreamArchivalConfirmation.cs new file mode 100644 index 0000000..a3c03ea --- /dev/null +++ b/src/Postmark/Model/MessageStreams/PostmarkMessageStreamArchivalConfirmation.cs @@ -0,0 +1,26 @@ +using System; + +namespace Postmark.Model.MessageStreams +{ + /// + /// Confirmation of archiving a message stream. + /// + public class PostmarkMessageStreamArchivalConfirmation + { + /// + /// Identifier of the message stream that was archived. + /// + public string ID { get; set; } + + /// + /// Id of the server where this stream was archived. + /// + public int ServerID { get; set; } + + /// + /// Expected date when this archived message stream will be removed, alongside associated content. + /// The stream can be unarchived up until this date. + /// + public DateTime ExpectedPurgeDate { get; set; } + } +} diff --git a/src/Postmark/Model/MessageStreams/PostmarkMessageStreamListing.cs b/src/Postmark/Model/MessageStreams/PostmarkMessageStreamListing.cs new file mode 100644 index 0000000..9b7dd39 --- /dev/null +++ b/src/Postmark/Model/MessageStreams/PostmarkMessageStreamListing.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Postmark.Model.MessageStreams +{ + /// + /// List of message streams + /// + public class PostmarkMessageStreamListing + { + public IEnumerable MessageStreams { get; set; } + + /// + /// Count of total message streams + /// + public int TotalCount { get; set; } + } +} diff --git a/src/Postmark/PostmarkClient.cs b/src/Postmark/PostmarkClient.cs index 8aeb528..4c4e6a0 100644 --- a/src/Postmark/PostmarkClient.cs +++ b/src/Postmark/PostmarkClient.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Postmark.Model.MessageStreams; using Postmark.Model.Suppressions; using PostmarkDotNet.Model.Webhooks; @@ -1101,5 +1102,103 @@ public async Task DeleteSuppressions(IEnumerable } #endregion + + #region MessageStreams + + /// + /// Create a new message stream on your server. + /// + /// Identifier for your message stream, unique at server level. + /// Type of the message stream. E.g.: Transactional or Broadcasts. + /// Friendly name for your message stream. + /// Friendly description for your message stream. (optional) + /// Currently, you cannot create multiple inbound streams. + public async Task CreateMessageStream(string id, MessageStreamType type, string name, string description = null) + { + var body = new Dictionary + { + ["ID"] = id, + ["Name"] = name, + ["Description"] = description, + ["MessageStreamType"] = type.ToString() + }; + + var apiUrl = "/message-streams/"; + + return await ProcessRequestAsync, PostmarkMessageStream>(apiUrl, HttpMethod.Post, body); + } + + /// + /// Edit the properties of a message stream. + /// + /// The identifier for the stream you are trying to update. + /// New friendly name to use. (optional) + /// New description to use. (optional) + public async Task EditMessageStream(string id, string name = null, string description = null) + { + var body = new Dictionary + { + ["Name"] = name, + ["Description"] = description + }; + + var apiUrl = $"/message-streams/{id}"; + + return await ProcessRequestAsync, PostmarkMessageStream>(apiUrl, new HttpMethod("PATCH"), body); + } + + /// + /// Retrieve details about a message stream. + /// + /// Identifier of the stream to retrieve details for. + public async Task GetMessageStream(string id) + { + return await ProcessNoBodyRequestAsync($"/message-streams/{id}"); + } + + /// + /// Retrieve all message streams on the server. + /// + /// Filter by stream type. E.g.: Transactional. Defaults to: All. + /// Include archived streams in the result. Defaults to: false. + public async Task ListMessageStreams(MessageStreamTypeFilter messageStreamType = MessageStreamTypeFilter.All, + bool includeArchivedStreams = false) + { + var parameters = new Dictionary + { + ["MessageStreamType"] = messageStreamType.ToString(), + ["IncludeArchivedStreams"] = includeArchivedStreams + }; + + return await ProcessNoBodyRequestAsync("/message-streams/", parameters); + } + + /// + /// Archive a message stream. This will disable sending/receiving messages via that stream. + /// The stream will also stop being shown in the Postmark UI. + /// Once a stream has been archived, it will be deleted (alongside associated data) at the ExpectedPurgeDate in the response. + /// + /// Identifier of the stream to archive. + public async Task ArchiveMessageStream(string id) + { + var apiUrl = $"/message-streams/{id}/archive"; + + return await ProcessNoBodyRequestAsync(apiUrl, verb: HttpMethod.Post); + } + + /// + /// UnArchive a message stream. This will resume sending/receiving via that stream. + /// The stream will also re-appear in the Postmark UI. + /// A stream can be unarchived only before the stream ExpectedPurgeDate. + /// + /// Identifier of the stream to unArchive. + public async Task UnArchiveMessageStream(string id) + { + var apiUrl = $"/message-streams/{id}/unarchive"; + + return await ProcessNoBodyRequestAsync(apiUrl, verb: HttpMethod.Post); + } + + #endregion } }