diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7f3f684 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Required to run the Mailinator integration tests. Leave unset to skip the suite. +MAILINATOR_TEST_API_TOKEN= + +# Optional values used by endpoint-specific integration tests. +MAILINATOR_TEST_DOMAIN_PRIVATE= +MAILINATOR_TEST_INBOX= +MAILINATOR_TEST_PHONE_NUMBER= +MAILINATOR_TEST_MESSAGE_WITH_ATTACHMENT_ID= +MAILINATOR_TEST_ATTACHMENT_ID= +MAILINATOR_TEST_DELETE_DOMAIN= +MAILINATOR_TEST_WEBHOOKTOKEN_PRIVATEDOMAIN= +MAILINATOR_TEST_WEBHOOKTOKEN_CUSTOMSERVICE= +MAILINATOR_TEST_AUTH_SECRET= +MAILINATOR_TEST_AUTH_ID= +MAILINATOR_TEST_WEBHOOK_INBOX= +MAILINATOR_TEST_WEBHOOK_CUSTOMSERVICE= diff --git a/.gitignore b/.gitignore index ad3b5f0..d8c4de2 100644 --- a/.gitignore +++ b/.gitignore @@ -275,4 +275,5 @@ Session.vim # Private test configuration and binaries. config.ps1 -**/IISApplications \ No newline at end of file +.env +**/IISApplications diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..74db6be --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +# Repository instructions + +## Project layout + +- `mailinator-csharp-client/` contains the SDK. It targets `net471` and `netstandard2.0`; do not change the supported frameworks without an explicit compatibility decision. +- API methods live in `Clients/ApiClients//`, and their request, response, and entity types live in the matching `Models//` tree. +- `mailinator-csharp-client-tests/` is a legacy .NET Framework 4.7.2 MSTest project whose tests call the live Mailinator API. +- `eng/OpenApiCoverageCheck/` is a .NET 8 tool for comparing the SDK request surface with the Mailinator OpenAPI specification. See `eng/README.md` for usage. + +## Making SDK changes + +- Treat the [Mailinator OpenAPI specification](https://github.com/manybrain/mailinatordocs/blob/main/openapi/mailinator-api.yaml) as the source of truth for documented endpoints. +- Preserve the `/api/v2` base path defined in `MailinatorClient.cs`. Endpoint clients receive a relative prefix and append their operation paths. +- Follow the existing RestSharp request pattern: create requests with `GetRequest`, use `AddUrlSegment` for path values, `AddSafeQueryParameter` for optional query values, and `AddJsonBody` for JSON bodies. +- Keep public operations asynchronous and named with the `Async` suffix. Put new models in the matching area and layer (`Requests`, `Responses`, or `Entities`) using the existing namespaces. +- Do not remove or silently change obsolete public members as part of unrelated work; that is a breaking API change. +- When the public API changes, update relevant examples and release documentation (`EXAMPLES.md`, `README.md`, and `CHANGELOG.md`) in the same change. + +## Verification + +- Build the solution with `dotnet build mailinator-csharp-client.sln` when the installed SDK supports all target frameworks. +- For endpoint or parameter changes, run the coverage tool described in `eng/README.md`. Prefer a checked-out spec with `--spec`; omitting it fetches the current spec from GitHub. +- Treat the MSTest suite as integration testing, not as an offline unit suite. It requires a deliberately configured Mailinator account and can create or delete domains, rules, and messages. Do not run it against live credentials—or run deletion tests—without explicit authorization. +- If the local environment cannot build the legacy targets or lacks the .NET SDK, report that limitation instead of claiming verification. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..abcd3e3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on *Keep a Changelog* and this project aims to follow *Semantic Versioning*. + + +## [1.0.7] - (Unreleased) + +### Added + +- `ROADMAP.md` +- `CHANGELOG.md` +- `AGENTS.md` +- `EXAMPLES.md` + +### Deprecated + +- All `RulesClient` endpoints (`CreateRuleAsync`, `DeleteRuleAsync`, `EnableRuleAsync`, `DisableRuleAsync`, `GetAllRulesAsync`, `GetRuleAsync`). +- `DomainsClient` create/delete endpoints (`CreateDomainAsync`, `DeleteDomainAsync`). +- Messages “Latest” wildcard endpoints (`FetchLatestMessagesAsync`, `FetchLatestInboxMessagesAsync`). diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..16341e9 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,220 @@ +# Examples + +This file is a living collection of copy/pasteable examples for the Mailinator C# client. + +## Setup + +Install via NuGet: + +``` +PM> Install-Package MailinatorApiClient +``` + +Create a client using an API token: + +```csharp +using mailinator_csharp_client; + +var client = new MailinatorClient("yourApiTokenHere"); +``` + +## Quickstart + +Fetch message summaries for an inbox: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Messages.Requests; +using mailinator_csharp_client.Models.Messages.Entities; + +var client = new MailinatorClient("yourApiTokenHere"); + +var request = new FetchInboxRequest +{ + Domain = "your_private_domain.com", + Inbox = "your_inbox", + Skip = 0, + Limit = 20, + Sort = Sort.desc +}; + +var response = await client.MessagesClient.FetchInboxAsync(request); +``` + +## Authenticators + +Instant TOTP code + list authenticators: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Authenticators.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var totp = await client.AuthenticatorsClient.InstantTOTP2FACodeAsync( + new InstantTOTP2FACodeRequest { TotpSecretKey = "yourAuthSecret" }); + +var authenticators = await client.AuthenticatorsClient.GetAuthenticatorsAsync(); + +var byId = await client.AuthenticatorsClient.GetAuthenticatorsByIdAsync( + new GetAuthenticatorsByIdRequest { Id = "yourAuthId" }); +``` + +## Domains + +List domains + fetch a domain: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Domains.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var all = await client.DomainsClient.GetAllDomainsAsync(); + +var domain = await client.DomainsClient.GetDomainAsync( + new GetDomainRequest { DomainId = "yourDomainIdHere" }); +``` + +## Messages + +Post (inject) a message: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Messages.Entities; +using mailinator_csharp_client.Models.Messages.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var message = new MessageToPost +{ + Subject = "Testing message", + From = "test_email@test.com", + Text = "Hello World!" +}; + +var response = await client.MessagesClient.PostMessageAsync( + new PostMessageRequest { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", Message = message }); +``` + +Fetch inbox summaries + fetch message by id: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Messages.Requests; +using mailinator_csharp_client.Models.Messages.Entities; + +var client = new MailinatorClient("yourApiTokenHere"); + +var inbox = await client.MessagesClient.FetchInboxAsync( + new FetchInboxRequest { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", Skip = 0, Limit = 20, Sort = Sort.desc }); + +var message = await client.MessagesClient.FetchMessageAsync( + new FetchMessageRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }); +``` + +Fetch attachments + download a single attachment: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Messages.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var attachments = await client.MessagesClient.FetchMessageAttachmentsAsync( + new FetchMessageAttachmentsRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }); + +var attachment = await client.MessagesClient.FetchMessageAttachmentAsync( + new FetchMessageAttachmentRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere", AttachmentId = "yourAttachmentIdHere" }); +``` + +Links, SMTP log, and raw content: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Messages.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var links = await client.MessagesClient.FetchMessageLinksAsync( + new FetchMessageLinksRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }); + +var linksFull = await client.MessagesClient.FetchMessageLinksFullAsync( + new FetchMessageLinksFullRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }); + +var smtp = await client.MessagesClient.FetchMessageSmtpLogAsync( + new FetchMessageSmtpLogRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }); + +var raw = await client.MessagesClient.FetchMessageRawAsync( + new FetchMessageRawRequest { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }); +``` + +Deletes: + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Messages.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var deleted = await client.MessagesClient.DeleteMessageAsync( + new DeleteMessageRequest { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", MessageId = "yourMessageIdHere" }); + +var deletedInbox = await client.MessagesClient.DeleteAllInboxMessagesAsync( + new DeleteAllInboxMessagesRequest { Domain = "yourDomainNameHere", Inbox = "yourInboxHere" }); + +var deletedDomain = await client.MessagesClient.DeleteAllDomainMessagesAsync( + new DeleteAllDomainMessagesRequest { Domain = "yourDomainNameHere" }); +``` + +## Stats + +Team summary: + +```csharp +using mailinator_csharp_client; + +var client = new MailinatorClient("yourApiTokenHere"); + +var team = await client.StatsClient.GetTeamAsync(); +var stats = await client.StatsClient.GetTeamStatsAsync(); +var info = await client.StatsClient.GetTeamInfoAsync(); +``` + +## Webhooks + +Inject via webhook endpoints (uses `whtoken`): + +```csharp +using mailinator_csharp_client; +using mailinator_csharp_client.Models.Webhooks.Entities; +using mailinator_csharp_client.Models.Webhooks.Requests; + +var client = new MailinatorClient("yourApiTokenHere"); + +var webhook = new Webhook +{ + From = "MyMailinatorCSharpTest", + Subject = "testing message", + Text = "hello world", + To = "jack" +}; + +var privateWebhook = await client.WebhooksClient.PrivateWebhookAsync( + new PrivateWebhookRequest { WebhookToken = "yourWebhookTokenPrivateDomain", Webhook = webhook }); + +var privateInboxWebhook = await client.WebhooksClient.PrivateInboxWebhookAsync( + new PrivateInboxWebhookRequest { WebhookToken = "yourWebhookTokenPrivateDomain", Inbox = "yourWebhookInbox", Webhook = webhook }); + +var customServiceWebhook = await client.WebhooksClient.PrivateCustomServiceWebhookAsync( + new PrivateCustomServiceWebhookRequest { WebhookToken = "yourWebhookTokenCustomService", CustomService = "yourWebhookCustomService", Webhook = webhook }); + +var customServiceInboxWebhook = await client.WebhooksClient.PrivateCustomServiceInboxWebhookAsync( + new PrivateCustomServiceInboxWebhookRequest { WebhookToken = "yourWebhookTokenCustomService", CustomService = "yourWebhookCustomService", Inbox = "yourWebhookInbox", Webhook = webhook }); +``` + +## Troubleshooting + +- Ensure you’re using an API token from your Mailinator team settings. +- For webhook injection, use webhook tokens (`whtoken`) instead of your API token. diff --git a/README.md b/README.md index bdfc4a0..a3754cd 100644 --- a/README.md +++ b/README.md @@ -14,318 +14,17 @@ To start using the API you need to first create an account at [mailinator.com](h Once you have an account you will need an API Token which you can generate in [mailinator.com/v3/#/#team_settings_pane](https://www.mailinator.com/v3/#/#team_settings_pane). -Then you can configure the library with: +Usage examples live in [EXAMPLES.md](https://github.com/manybrain/mailinator-csharp-client/blob/master/EXAMPLES.md). -```csharp - using mailinator_csharp_client; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); -``` - -## Examples - -##### Authenticators methods: - -- InstantTOTP2FACode / Get Authenticators / Get Authenticators By Id: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Domains.Requests; - using mailinator_csharp_client.Models.Domains.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //InstantTOTP2FACode - InstantTOTP2FACodeRequest instantTOTP2FACodeRequest = new InstantTOTP2FACodeRequest() { TotpSecretKey = "yourAuthSecret" }; - var instantTOTP2FACodeResponse = await mailinatorClient.AuthenticatorsClient.InstantTOTP2FACodeAsync(instantTOTP2FACodeRequest); - - //Get Authenticators - GetAuthenticatorsResponse getAuthenticatorsResponse = await mailinatorClient.AuthenticatorsClient.GetAuthenticatorsAsync(); - - //Get Authenticators By Id - GetAuthenticatorsByIdRequest getAuthenticatorsByIdRequest = new GetAuthenticatorsByIdRequest() { Id = "yourAuthId" }; - GetAuthenticatorsByIdResponse getAuthenticatorsByIdResponse = await mailinatorClient.AuthenticatorsClient.GetAuthenticatorsByIdAsync(getAuthenticatorsByIdRequest); - - // ... - ``` - -##### Domains methods: - -- Get AllDomains / Domain: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Domains.Requests; - using mailinator_csharp_client.Models.Domains.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Get All Domains - GetAllDomainsResponse getAllDomainsResponse = await mailinatorClient.DomainsClient.GetAllDomainsAsync(); - - //Get Domain - GetDomainRequest getDomainRequest = new GetDomainRequest() { DomainId = "yourDomainIdHere" }; - GetDomainResponse getDomainResponse = await mailinatorClient.DomainsClient.GetDomainAsync(getDomainRequest); - // ... - ``` - -- Create / Delete Domain: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Domains.Requests; - using mailinator_csharp_client.Models.Domains.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Create Domain - CreateDomainRequest createDomainRequest = new CreateDomainRequest() - { - Name = DateTime.UtcNow.Ticks.ToString(), - Description = "Description", - Enabled = true, - Rules = new System.Collections.Generic.List() - }; - CreateDomainResponse createDomainResponse = await mailinatorClient.DomainsClient.CreateDomainAsync(createDomainRequest ); - - //Delete Domain - var deleteDomainRequest = new DeleteDomainRequest() { DomainId = "yourDomainIdHere" }; - DeleteDomainResponse deleteDomainResponse = await mailinatorClient.DomainsClient.DeleteDomainAsync(deleteDomainRequest); - // ... - ``` - -##### Rules methods: - -- Create / Delete Rule: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Rules.Entities; - using mailinator_csharp_client.Models.Rules.Requests; - using mailinator_csharp_client.Models.Rules.Responses; - using System.Collections.Generic; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Create Rule - RuleToCreate ruleToCreate = new RuleToCreate() - { - Name = "RuleName", - Priority = 15, - Description = "Description", - Conditions = new List() - { - new Condition() - { - Operation = OperationType.PREFIX, - ConditionData = new ConditionData() - { - Field = "to", - Value = "raul" - } - } - }, - Enabled = true, - Match = MatchType.ANY, - Actions = new List() { new ActionRule() { Action = ActionType.WEBHOOK, ActionData = new ActionData() { Url = "https://www.google.com" } } } - }; - CreateRuleRequest createRuleRequest = new CreateRuleRequest() { DomainId = "yourDomainIdHere", Rule = ruleToCreate }; - CreateRuleResponse createRuleResponse = await mailinatorClient.RulesClient.CreateRuleAsync(createRuleRequest); - - //Delete Rule - DeleteRuleRequest deleteRuleRequest = new DeleteRuleRequest() { DomainId = "yourDomainIdHere", RuleId = "yourRuleIdHere" }; - DeleteRuleResponse deleteRuleResponse = await mailinatorClient.RulesClient.DeleteRuleAsync(deleteRuleRequest); - // ... - ``` - -- Enable / Disable Rule: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Rules.Requests; - using mailinator_csharp_client.Models.Rules.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Enable Rule - EnableRuleRequest enableRuleRequest = new EnableRuleRequest() { DomainId = "yourDomainIdHere", RuleId = "yourRuleIdHere" }; - EnableRuleResponse enableRuleResponse = await mailinatorClient.RulesClient.EnableRuleAsync(enableRuleRequest); - - //Disable Rule - DisableRuleRequest disableRuleRequest = new DisableRuleRequest() { DomainId = "yourDomainIdHere", RuleId = "yourRuleIdHere" }; - DisableRuleResponse disableRuleResponse = await mailinatorClient.RulesClient.DisableRuleAsync(disableRuleRequest); - ``` - -- Get All Rules / Rule: - -```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Rules.Requests; - using mailinator_csharp_client.Models.Rules.Responses; +##### Build with tests - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); +Run the fast, offline unit tests with: - //Get All Rules - var getAllRulesRequest = new GetAllRulesRequest() { DomainId = "yourDomainIdHere" }; - var getAllRulesResponse = await mailinatorClient.RulesClient.GetAllRulesAsync(getAllRulesRequest); - - //Get Rule - var getRuleRequest = new GetRuleRequest() { DomainId = "yourDomainIdHere", RuleId = "yourRuleIdHere" }; - var getRuleResponse = await mailinatorClient.RulesClient.GetRuleAsync(getRuleRequest); +``` +dotnet test mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj ``` -##### Messages methods: - -- Post Message: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Messages.Entities; - using mailinator_csharp_client.Models.Messages.Requests; - using mailinator_csharp_client.Models.Messages.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - MessageToPost messageToPost = new MessageToPost() - { - Subject = "Testing message", - From = "test_email@test.com", - Text = "Hello World!" - }; - PostMessageRequest postMessageRequest = new PostMessageRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", Message = messageToPost }; - PostMessageResponse postMessageResponse = await mailinatorClient.MessagesClient.PostNewMessageAsync(postMessageRequest); - // ... - ``` - -- Fetch Inbox / Message / SMS Messages / Attachments / Attachment / Smtp Log / Raw / Latest: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Messages.Entities; - using mailinator_csharp_client.Models.Messages.Requests; - using mailinator_csharp_client.Models.Messages.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Fetch Inbox - FetchInboxRequest fetchInboxRequest = new FetchInboxRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", Skip = 0, Limit = 20, Sort = Sort.asc }; - FetchInboxResponse fetchInboxResponse = await mailinatorClient.MessagesClient.FetchInboxAsync(fetchInboxRequest); - - //Fetch Message - FetchMessageRequest fetchMessageRequest = new FetchMessageRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", MessageId = "yourMessageIdHere" }; - FetchMessageResponse fetchMessageResponse = await mailinatorClient.MessagesClient.FetchMessageAsync(fetchMessageRequest); - - //Fetch SMS Messages - FetchSMSMessagesRequest fetchSMSMessagesRequest = new FetchSMSMessagesRequest() { Domain = "yourDomainNameHere", TeamSMSNumber = "yourTeamSMSNumberHere" }; - FetchSMSMessagesResponse fetchSMSMessagesResponse = await mailinatorClient.MessagesClient.FetchSMSMessagesAsync(fetchSMSMessagesRequest); - - //Fetch Attachments - FetchAttachmentsRequest fetchAttachmentsRequest = new FetchAttachmentsRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", MessageId = "yourMessageIdWithAttachmentHere" }; - FetchAttachmentsResponse fetchAttachmentsResponse = await mailinatorClient.MessagesClient.FetchAttachmentsAsync(fetchAttachmentsRequest); - - //Fetch Attachment - FetchAttachmentRequest fetchAttachmentRequest = new FetchAttachmentRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", MessageId = "yourMessageIdWithAttachmentHere", AttachmentId = "yourAttachmentIdHere" }; - FetchAttachmentResponse fetchAttachmentResponse = await mailinatorClient.MessagesClient.FetchAttachmentAsync(fetchAttachmentRequest); - - //Fetch Message Links - FetchMessageLinksRequest fetchMessageLinksRequest = new FetchMessageLinksRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", MessageId = "yourMessageIdWithAttachmentHere" }; - FetchMessageLinksResponse fetchMessageLinksResponse = await mailinatorClient.MessagesClient.FetchMessageLinksAsync(fetchMessageLinksRequest); - - //Fetch Message Links Full - FetchMessageLinksFullRequest fetchMessageLinksFullRequest = new FetchMessageLinksFullRequest() { Domain = "yourDomainNameHere", MessageId = "yourMessageIdWithAttachmentHere" }; - FetchMessageLinksFullResponse fetchMessageLinksFullResponse = await mailinatorClient.MessagesClient.FetchMessageLinksFullAsync(fetchMessageLinksFullRequest); - - //Fetch Message Smtp Log - FetchMessageSmtpLogRequest fetchMessageSmtpLogRequest = new FetchMessageSmtpLogRequest() { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }; - FetchMessageSmtpLogResponse fetchMessageSmtpLogResponse= await mailinatorClient.MessagesClient.FetchMessageSmtpLogAsync(fetchMessageSmtpLogRequest); - - //Fetch Message Raw - FetchMessageRawRequest fetchMessageRawRequest = new FetchMessageRawRequest() { Domain = "yourDomainNameHere", MessageId = "yourMessageIdHere" }; - FetchMessageRawResponse fetchMessageRawResponse= await mailinatorClient.MessagesClient.FetchMessageRawAsync(fetchMessageRawRequest); - - //Fetch Latest Messages - FetchLatestMessagesRequest fetchLatestMessagesRequest = new FetchLatestMessagesRequest() { Domain = "yourDomainNameHere" }; - FetchLatestMessagesResponse fetchLatestMessagesResponse = await mailinatorClient.MessagesClient.FetchLatestMessagesAsync(fetchLatestMessagesRequest); - ``` - -- Delete Message / AllInboxMessages / AllDomainMessages - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Messages.Requests; - using mailinator_csharp_client.Models.Messages.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Delete Message - DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere", MessageId = "yourMessageIdHere" }; - DeleteMessageResponse deleteMessageResponse = await mailinatorClient.MessagesClient.DeleteMessageAsync(deleteMessageRequest); - - //Delete All Inbox Messages - DeleteAllInboxMessagesRequest deleteAllInboxMessagesRequest = new DeleteAllInboxMessagesRequest() { Domain = "yourDomainNameHere", Inbox = "yourInboxHere" }; - DeleteAllInboxMessagesResponse deleteAllInboxMessagesResponse = await mailinatorClient.MessagesClient.DeleteAllInboxMessagesAsync(deleteAllInboxMessagesRequest); - - //Delete All Domain Messages - DeleteAllDomainMessagesRequest deleteAllDomainMessagesRequest = new DeleteAllDomainMessagesRequest() { Domain = "yourDomainNameHere" }; - DeleteAllDomainMessagesResponse deleteAllDomainMessagesResponse = await mailinatorClient.MessagesClient.DeleteAllDomainMessagesAsync(deleteAllDomainMessagesRequest); - ``` - -##### Stats methods: - -- Get Team / Team Stats / Team Info: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Domains.Requests; - using mailinator_csharp_client.Models.Domains.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - - //Get Team - GetTeamResponse getTeamResponse = await mailinatorClient.StatsClient.GetTeamAsync(); - - //Get TeamStats - GetTeamStatsResponse getTeamStatsResponse = await mailinatorClient.StatsClient.GetTeamStatsAsync(); - - //Get TeamInfo - GetTeamInfoResponse getTeamInfoResponse = await mailinatorClient.StatsClient.GetTeamInfoAsync(); - - // ... - ``` - -##### Webhooks methods: - -- Private Webhook / Private Inbox Webhook / Private Custom Service Webhook / Private Custom Service Inbox Webhook: - - ```csharp - using mailinator_csharp_client; - using mailinator_csharp_client.Models.Domains.Requests; - using mailinator_csharp_client.Models.Domains.Responses; - - MailinatorClient mailinatorClient = new MailinatorClient("yourApiTokenHere"); - Webhook webhookToAdd = new Webhook { From = "MyMailinatorCSharpTest", Subject = "testing message", Text = "hello world", To = "jack" }; - - //Private Webhook - PrivateWebhookRequest privateWebhookRequest = new PrivateWebhookRequest() { WebhookToken = "yourWebhookTokenPrivateDomain", Webhook = webhookToAdd }; - PrivateWebhookResponse privateWebhookResponse = await mailinatorClient.WebhooksClient.PrivateWebhookAsync(privateWebhookRequest); - - //Private Inbox Webhook - PrivateInboxWebhookRequest privateInboxWebhookRequest = new PrivateInboxWebhookRequest() { WebhookToken = "yourWebhookTokenPrivateDomain", Inbox = "yourWebhookInbox", Webhook = webhookToAdd }; - PrivateWebhookResponse privateInboxWebhookResponse = await mailinatorClient.WebhooksClient.PrivateInboxWebhookAsync(privateInboxWebhookRequest); - - //Private Custom Service Webhook - PrivateCustomServiceWebhookRequest privateCustomServiceWebhookRequest = new PrivateCustomServiceWebhookRequest() { WebhookToken = "yourWebhookTokenCustomService", CustomService = "yourWebhookCustomService", Webhook = webhookToAdd }; - PrivateCustomServiceWebhookResponse privateCustomServiceWebhookResponse = await mailinatorClient.WebhooksClient.PrivateCustomServiceWebhookAsync(privateCustomServiceWebhookRequest); - - //Private Custom Service Inbox Webhook - PrivateCustomServiceInboxWebhookRequest privateCustomServiceInboxWebhookRequest = new PrivateCustomServiceInboxWebhookRequest() { WebhookToken = "yourWebhookTokenCustomService", CustomService = "yourWebhookCustomService", Inbox = "yourWebhookInbox", Webhook = webhookToAdd }; - PrivateCustomServiceWebhookResponse privateCustomServiceInboxWebhookResponse = await mailinatorClient.WebhooksClient.PrivateCustomServiceInboxWebhookAsync(privateCustomServiceInboxWebhookRequest); - // ... - ``` - -##### Build with tests - -Some of the tests require env variables with valid values. Visit tests source code and review `TastBase.cs` class. The more env variables you set, the more tests are run. +The tests are live integration tests. Configure them in a repository-root `.env` file (which is ignored by Git); process environment variables take precedence. If `MAILINATOR_TEST_API_TOKEN` is missing, the entire integration suite is skipped. Copy `.env.example` as a starting point. * `MAILINATOR_TEST_API_TOKEN` - API tokens for authentication; basic requirement across many tests;see also https://manybrain.github.io/m8rdocs/#api-authentication * `MAILINATOR_TEST_DOMAIN_PRIVATE` - private domain; visit https://www.mailinator.com/ @@ -339,4 +38,4 @@ Some of the tests require env variables with valid values. Visit tests source co * `MAILINATOR_TEST_AUTH_SECRET` - authenticator secret * `MAILINATOR_TEST_AUTH_ID` - authenticator id * `MAILINATOR_TEST_WEBHOOK_INBOX` - inbox for webhook -* `MAILINATOR_TEST_WEBHOOK_CUSTOMSERVICE` - custom service for webhook \ No newline at end of file +* `MAILINATOR_TEST_WEBHOOK_CUSTOMSERVICE` - custom service for webhook diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..c1e1f6b --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,128 @@ +# Roadmap + +This document is a living roadmap for the Mailinator C# client. It’s intentionally high-level and should evolve as we audit the SDK against the Mailinator OpenAPI spec and customer needs. + +## Goals + +- Stay aligned with the Mailinator OpenAPI specification. +- Maintain backwards compatibility where practical (or document breaking changes clearly). +- Provide clear, copy/pasteable examples for common workflows. +- Make releases predictable and easy to consume. + +## Current Status: + +- Target frameworks: `net471`; `netstandard2.0` +- API coverage vs spec: see “Gap Analysis” +- Known gaps / bugs: missing spec endpoints, SDK-only endpoints, path parameter-name mismatches, and one missing query parameter listed below. + +## Dependency Maintenance + +Audit refreshed: 2026-08-10. + +Security status: + +- No current direct dependency or package listed in `mailinator-csharp-client-tests/packages.config` falls within a known advisory range in NuGet's vulnerability feed. +- `RestSharp` `112.0.0` resolves `System.Text.Json` at `8.0.4` or newer for `net471` and `netstandard2.0`; the advisories currently listed for the 8.x line affect versions through `8.0.3`. +- The repository has no lock files, and the .NET SDK is not available in the current audit environment, so a restored full transitive graph could not be verified with `dotnet list package --vulnerable --include-transitive`. + +Production and tooling dependencies: + +- `Newtonsoft.Json`: current `13.0.3`; latest stable `13.0.4`. +- `RestSharp`: current `112.0.0`; latest stable `114.0.0`. Version `114.0.0` still supports `net471` and `netstandard2.0`, but raises its `System.Text.Json` dependency from `8.0.4` to `10.0.0` and requires API compatibility testing. +- `Microsoft.OpenApi.Readers`: current `1.6.29`; latest stable `1.6.29` (2.x remains preview-only). + +Legacy test-project dependencies: + +- `Microsoft.ApplicationInsights`: `2.22.0` → `3.1.2`. +- `Microsoft.Testing.Platform` and related extensions: `1.3.2` → `2.3.3`. +- `Microsoft.TestPlatform.ObjectModel`: `17.10.0` → `18.8.1`. +- `MSTest.TestAdapter` and `MSTest.TestFramework`: `3.5.2` → `4.3.3`. +- Explicitly pinned support packages are also behind: `System.Buffers` (`4.5.1` → `4.6.1`), `System.Collections.Immutable` (`1.5.0` → `10.0.10`), `System.Diagnostics.DiagnosticSource` (`5.0.0` → `10.0.10`), `System.Memory` (`4.5.4` → `4.6.3`), `System.Numerics.Vectors` (`4.5.0` → `4.6.1`), `System.Reflection.Metadata` (`1.6.0` → `10.0.10`), and `System.Runtime.CompilerServices.Unsafe` (`5.0.0` → `6.1.2`). + +Work items: + +- Update `Newtonsoft.Json` to `13.0.4` and run build/tests. +- Evaluate `RestSharp` `114.0.0` in a dedicated change; verify source compatibility, serialization behavior, all target frameworks, and the full SDK test suite. +- Convert the legacy `net472` test project from `packages.config` to SDK-style `PackageReference`, then upgrade the Microsoft testing packages as one coordinated stack and remove direct pins for transitive `System.*` dependencies where possible. +- Keep `Microsoft.OpenApi.Readers` on `1.6.29` until a stable 2.x release or a specific tooling requirement justifies a preview. +- Add lock files and a CI dependency check (`dotnet list package --vulnerable --include-transitive`) after restore tooling is available. + +## Gap Analysis (2026-03-23) + +This snapshot compares the SDK’s implemented operations to the Mailinator OpenAPI spec (`mailinator-api.yaml`). + +- Spec operations: 35 +- SDK operations: 43 +- Exact matches: 21 +- Missing from SDK: 10 +- SDK-only (no spec match): 17 +- SDK aliases / convenience wrappers: 1 +- Path parameter-name mismatches: 4 +- Operations with missing query params: 1 + +Re-run locally: + +- Fetch the spec YAML and compare it to `mailinator-csharp-client/Clients/ApiClients/**` operations (method + effective path + query params). +- Or run `dotnet run --project eng/OpenApiCoverageCheck -- --spec path/to/mailinator-api.yaml`. + +### Work Items (spec → SDK) + +Add these operations that exist in the spec but are missing from the SDK: + +- **Messages** + - `listDomainMessages` — `GET /api/v2/domains/{domain}/inboxes` + - `getMessageHeaders` — `GET /api/v2/domains/{domain}/messages/{messageId}/headers` + - `getMessageSummary` — `GET /api/v2/domains/{domain}/messages/{messageId}/summary` + - `getMessageText` — `GET /api/v2/domains/{domain}/messages/{messageId}/text` + - `getMessageTextHtml` — `GET /api/v2/domains/{domain}/messages/{messageId}/texthtml` + - `getMessageTextPlain` — `GET /api/v2/domains/{domain}/messages/{messageId}/textplain` + - `streamDomainMessages` — `GET /api/v2/domains/{domain}/stream` + - `streamInboxMessages` — `GET /api/v2/domains/{domain}/stream/{inbox}` +- **Webhook** + - `postWebhookMessage` — `POST /api/v2/domains/{domain}/webhook` + - `postWebhookInboxMessage` — `POST /api/v2/domains/{domain}/webhook/{inbox}` + +### Work Items (SDK → spec) + +These SDK operations do not have a matching operation in the current OpenAPI spec. Decide for each group whether to (a) update the spec, (b) deprecate/remove the SDK surface, or (c) keep but document explicitly as “not in spec”. + +- **Rules** (6 operations under `/api/v2/domains/{domain_id}/rules...`) +- **Domains** create/delete (`POST`/`DELETE /api/v2/domains/{domain_id}`) +- **Authenticators** list/get variants (`/api/v2/authenticator...` and `/api/v2/authenticators`) +- **Messages** “latest” wildcard endpoints (`GET .../messages/*`) +- **Webhooks** private/custom-service endpoints (`POST /api/v2/domains/private/...`) + +### Work Items (spec alignment) + +Path template parameter names differ from the spec (non-breaking, but worth aligning for clarity and consistency): + +- Attachments: `{attachmentName}` (spec) vs `{attachmentId}` (SDK) +- Authenticators: `{authenticator_id}` (spec) vs `{auth_id}` (SDK) +- Domains: `{domain_name}` (spec) vs `{domain_id}` (SDK) + +Query parameters differ from the spec: + +- `GET /api/v2/domains/{domain}/inboxes/{inbox}/messages/{messageId}` is missing the optional `delete` query parameter in the SDK. + +## Near-Term (next 1–3 updates) + +- Keep gap analysis up to date (re-run after changes). +- Decide on versioning and release cadence. +- Implement missing spec endpoints (see “Work Items (spec → SDK)”). +- Resolve spec alignment issues (path template parameter names). +- Make an explicit decision on SDK-only endpoints (spec update vs deprecate vs document). +- Improve docs: examples, configuration, troubleshooting. + +## Mid-Term + +- Improve test coverage and add integration test guidance. +- Add more ergonomic APIs / helpers while keeping the low-level request mapping. + +## Long-Term + +- Automate spec drift detection and regeneration / validation workflows. +- Improve observability and diagnostics (logging hooks, request/response tracing). + +## Out of Scope (for now) + +- Anything that depends on undocumented endpoints without confirmation. diff --git a/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj b/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj new file mode 100644 index 0000000..8a4f8a9 --- /dev/null +++ b/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/eng/OpenApiCoverageCheck/Program.cs b/eng/OpenApiCoverageCheck/Program.cs new file mode 100644 index 0000000..32ee4f8 --- /dev/null +++ b/eng/OpenApiCoverageCheck/Program.cs @@ -0,0 +1,408 @@ +using System.Text.RegularExpressions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +const string DefaultSpecUrl = "https://raw.githubusercontent.com/manybrain/mailinatordocs/main/openapi/mailinator-api.yaml"; + +var options = Options.Parse(args); +if (options.ShowHelp) +{ + Options.PrintHelp(); + return 0; +} + +var spec = await LoadSpecAsync(options); +var specOperations = GetOpenApiOperations(spec.Document).ToList(); +var sdkOperations = GetCSharpOperations(options.ClientRoot).ToList(); + +var specByKey = specOperations.GroupBy(operation => operation.Key).ToDictionary(group => group.Key, group => group.ToList()); +var sdkByKey = sdkOperations.GroupBy(operation => operation.Key).ToDictionary(group => group.Key, group => group.ToList()); +var specByStructuralKey = specOperations.GroupBy(operation => operation.StructuralKey).ToDictionary(group => group.Key, group => group.ToList()); +var sdkByStructuralKey = sdkOperations.GroupBy(operation => operation.StructuralKey).ToDictionary(group => group.Key, group => group.ToList()); + +var pathParameterMismatches = specOperations + .Where(specOperation => !sdkByKey.ContainsKey(specOperation.Key)) + .Select(specOperation => + { + sdkByStructuralKey.TryGetValue(specOperation.StructuralKey, out var candidates); + var sdkOperation = candidates?.FirstOrDefault(candidate => candidate.Method == specOperation.Method); + return sdkOperation is null || sdkOperation.PathParams.SetEquals(specOperation.PathParams) + ? null + : new OperationPair(specOperation, sdkOperation); + }) + .Where(pair => pair is not null) + .Cast() + .ToList(); + +var exactMatchKeys = specByKey.Keys.Intersect(sdkByKey.Keys).ToHashSet(StringComparer.Ordinal); +var mismatchedSpecKeys = pathParameterMismatches.Select(pair => pair.SpecOperation.Key).ToHashSet(StringComparer.Ordinal); +var mismatchedSdkKeys = pathParameterMismatches.Select(pair => pair.SdkOperation.Key).ToHashSet(StringComparer.Ordinal); + +var missingFromSdk = specOperations + .Where(operation => !sdkByKey.ContainsKey(operation.Key) && !mismatchedSpecKeys.Contains(operation.Key)) + .ToList(); + +var sdkAliasOperations = sdkOperations + .Where(operation => !specByKey.ContainsKey(operation.Key) && !mismatchedSdkKeys.Contains(operation.Key)) + .Where(operation => + specByStructuralKey.TryGetValue(operation.StructuralKey, out var specMatches) && + specMatches.Any(specOperation => exactMatchKeys.Contains(specOperation.Key))) + .ToList(); + +var sdkAliasKeys = sdkAliasOperations.Select(operation => operation.Key).ToHashSet(StringComparer.Ordinal); +var sdkOnly = sdkOperations + .Where(operation => !specByKey.ContainsKey(operation.Key) && !mismatchedSdkKeys.Contains(operation.Key)) + .Where(operation => !sdkAliasKeys.Contains(operation.Key)) + .ToList(); + +var missingQueryParams = exactMatchKeys + .Select(key => + { + var specOperation = specByKey[key][0]; + var sdkOperation = sdkByKey[key][0]; + var missing = specOperation.QueryParams.Except(sdkOperation.QueryParams).OrderBy(param => param).ToList(); + return missing.Count == 0 ? null : new MissingQueryParams(specOperation, sdkOperation, missing); + }) + .Where(item => item is not null) + .Cast() + .OrderBy(item => item.SpecOperation.Key) + .ToList(); + +var lines = new List +{ + options.Format == "markdown" ? "## OpenAPI coverage check" : "OpenAPI coverage check", + $"Spec source: {spec.Source}", + $"SDK root: {options.ClientRoot}", + string.Empty, + $"Spec operations: {specOperations.Count}", + $"SDK operations: {sdkOperations.Count}", + $"Exact matches: {exactMatchKeys.Count}", + $"Missing from SDK: {missingFromSdk.Count}", + $"SDK-only: {sdkOnly.Count}", + $"SDK aliases/convenience wrappers: {sdkAliasOperations.Count}", + $"Path parameter-name mismatches: {pathParameterMismatches.Count}", + $"Operations with missing query params: {missingQueryParams.Count}", + string.Empty +}; + +RenderList(lines, "Missing from SDK:", missingFromSdk); +RenderList(lines, "SDK-only:", sdkOnly); +RenderList(lines, "SDK aliases/convenience wrappers:", sdkAliasOperations); + +if (pathParameterMismatches.Count > 0) +{ + lines.Add("Path parameter-name mismatches:"); + foreach (var pair in pathParameterMismatches.OrderBy(pair => pair.SpecOperation.Key)) + { + lines.Add($" - {pair.SpecOperation.Key}"); + lines.Add($" spec path: {pair.SpecOperation.Path}"); + lines.Add($" sdk path: {pair.SdkOperation.Path}"); + } + + lines.Add(string.Empty); +} + +if (missingQueryParams.Count > 0) +{ + lines.Add("Missing query params:"); + foreach (var item in missingQueryParams) + { + lines.Add($" - {item.SpecOperation.Key}: {string.Join(", ", item.Missing)}"); + } + + lines.Add(string.Empty); +} + +Console.WriteLine(string.Join(Environment.NewLine, lines).TrimEnd()); + +var driftDetected = + missingFromSdk.Count > 0 || + sdkOnly.Count > 0 || + pathParameterMismatches.Count > 0 || + missingQueryParams.Count > 0; + +return driftDetected && options.FailOnDrift ? 1 : 0; + +static async Task LoadSpecAsync(Options options) +{ + if (!string.IsNullOrWhiteSpace(options.SpecPath) && !string.IsNullOrWhiteSpace(options.SpecUrl)) + { + throw new InvalidOperationException("Use either --spec or --spec-url, not both."); + } + + var source = options.SpecPath ?? options.SpecUrl ?? DefaultSpecUrl; + using var httpClient = new HttpClient(); + await using var stream = !string.IsNullOrWhiteSpace(options.SpecPath) + ? File.OpenRead(options.SpecPath) + : await httpClient.GetStreamAsync(source); + + var document = new OpenApiStreamReader().Read(stream, out var diagnostic); + if (diagnostic.Errors.Count > 0) + { + var errors = string.Join(Environment.NewLine, diagnostic.Errors.Select(error => $" - {error.Message}")); + throw new InvalidOperationException($"Unable to parse OpenAPI document:{Environment.NewLine}{errors}"); + } + + return new LoadedSpec(document, source); +} + +static IEnumerable GetOpenApiOperations(OpenApiDocument document) +{ + foreach (var path in document.Paths) + { + foreach (var operation in path.Value.Operations) + { + var pathParameters = path.Value.Parameters ?? Enumerable.Empty(); + var operationParameters = operation.Value.Parameters ?? Enumerable.Empty(); + var parameters = pathParameters + .Concat(operationParameters) + .Select(parameter => ResolveParameter(document, parameter)); + + yield return new ApiOperation( + Method: operation.Key.ToString().ToUpperInvariant(), + Path: NormalizePath(path.Key), + OperationId: operation.Value.OperationId, + QueryParams: parameters + .Where(parameter => parameter.In == ParameterLocation.Query) + .Select(parameter => parameter.Name) + .ToHashSet(StringComparer.Ordinal), + PathParams: parameters + .Where(parameter => parameter.In == ParameterLocation.Path) + .Select(parameter => parameter.Name) + .ToHashSet(StringComparer.Ordinal), + Source: "OpenAPI"); + } + } +} + +static OpenApiParameter ResolveParameter(OpenApiDocument document, OpenApiParameter parameter) +{ + if (parameter.Reference?.Id is { Length: > 0 } referenceId && + document.Components?.Parameters.TryGetValue(referenceId, out var referencedParameter) == true) + { + return referencedParameter; + } + + return parameter; +} + +static IEnumerable GetCSharpOperations(string clientRoot) +{ + var endpointMap = GetClassEndpointMap(clientRoot); + var apiClientsRoot = Path.Combine(clientRoot, "Clients", "ApiClients"); + if (!Directory.Exists(apiClientsRoot)) + { + throw new DirectoryNotFoundException($"Unable to find API clients directory: {apiClientsRoot}"); + } + + foreach (var file in Directory.GetFiles(apiClientsRoot, "*Client.cs", SearchOption.AllDirectories)) + { + var source = File.ReadAllText(file); + var className = Path.GetFileNameWithoutExtension(file); + var baseEndpoint = endpointMap.TryGetValue(className, out var endpoint) ? endpoint : string.Empty; + + foreach (var method in GetMethodBlocks(source)) + { + var requestMatch = Regex.Match( + method.Block, + @"httpClient\.GetRequest\s*\(\s*endpointUrl\s*\+\s*""([^""]*)""\s*,\s*Method\.(\w+)", + RegexOptions.Singleline); + + if (!requestMatch.Success) + { + continue; + } + + var path = CombineEndpointPath(baseEndpoint, requestMatch.Groups[1].Value); + var queryParams = Regex.Matches(method.Block, @"AddSafeQueryParameter\s*\(\s*""([^""]+)""") + .Select(match => match.Groups[1].Value) + .ToHashSet(StringComparer.Ordinal); + var pathParams = Regex.Matches(method.Block, @"AddUrlSegment\s*\(\s*""([^""]+)""") + .Select(match => match.Groups[1].Value) + .ToHashSet(StringComparer.Ordinal); + + yield return new ApiOperation( + Method: requestMatch.Groups[2].Value.ToUpperInvariant(), + Path: path, + OperationId: method.Name, + QueryParams: queryParams, + PathParams: pathParams, + Source: Path.GetRelativePath(Directory.GetCurrentDirectory(), file)); + } + } +} + +static Dictionary GetClassEndpointMap(string clientRoot) +{ + var mailinatorClient = Path.Combine(clientRoot, "MailinatorClient.cs"); + if (!File.Exists(mailinatorClient)) + { + return new Dictionary(StringComparer.Ordinal); + } + + var source = File.ReadAllText(mailinatorClient); + return Regex.Matches(source, @"(\w+Client)\s*=\s*new\s+\w+Client\s*\([^,]+,\s*""([^""]*)""\s*\)") + .ToDictionary( + match => match.Groups[1].Value, + match => match.Groups[2].Value, + StringComparer.Ordinal); +} + +static IEnumerable GetMethodBlocks(string source) +{ + var matches = Regex.Matches( + source, + @"public\s+async\s+Task<[^>]+>\s+(\w+)\s*\([^)]*\)\s*\{", + RegexOptions.Singleline) + .Cast() + .ToList(); + + for (var index = 0; index < matches.Count; index++) + { + var match = matches[index]; + var nextStart = index + 1 < matches.Count ? matches[index + 1].Index : source.Length; + yield return new MethodBlock(match.Groups[1].Value, source[match.Index..nextStart]); + } +} + +static string CombineEndpointPath(string baseEndpoint, string relativePath) +{ + var path = string.IsNullOrWhiteSpace(baseEndpoint) + ? relativePath + : relativePath.StartsWith("/", StringComparison.Ordinal) + ? $"{baseEndpoint}{relativePath}" + : $"{baseEndpoint}/{relativePath}"; + + return NormalizePath($"/api/v2/{path}"); +} + +static string NormalizePath(string path) +{ + var normalized = Regex.Replace(path, "/+", "/"); + if (!normalized.StartsWith("/", StringComparison.Ordinal)) + { + normalized = $"/{normalized}"; + } + + return normalized.Length > 1 ? normalized.TrimEnd('/') : normalized; +} + +static void RenderList(List lines, string title, List operations) +{ + if (operations.Count == 0) + { + return; + } + + lines.Add(title); + foreach (var operation in operations.OrderBy(operation => operation.Key)) + { + lines.Add($" - {OperationLabel(operation)}"); + } + + lines.Add(string.Empty); +} + +static string OperationLabel(ApiOperation operation) +{ + return string.IsNullOrWhiteSpace(operation.OperationId) + ? operation.Key + : $"{operation.Key} ({operation.OperationId})"; +} + +internal sealed record ApiOperation( + string Method, + string Path, + string? OperationId, + HashSet QueryParams, + HashSet PathParams, + string Source) +{ + public string Key => $"{Method} {Path}"; + public string StructuralKey => Regex.Replace(Key, @"\{[^}]+\}", "{}"); +} + +internal sealed record LoadedSpec(OpenApiDocument Document, string Source); + +internal sealed record MethodBlock(string Name, string Block); + +internal sealed record OperationPair(ApiOperation SpecOperation, ApiOperation SdkOperation); + +internal sealed record MissingQueryParams(ApiOperation SpecOperation, ApiOperation SdkOperation, List Missing); + +internal sealed class Options +{ + public string? SpecPath { get; private set; } + public string? SpecUrl { get; private set; } + public string ClientRoot { get; private set; } = Path.Combine(Directory.GetCurrentDirectory(), "mailinator-csharp-client"); + public bool FailOnDrift { get; private set; } + public string Format { get; private set; } = "text"; + public bool ShowHelp { get; private set; } + + public static Options Parse(string[] args) + { + var options = new Options(); + + for (var index = 0; index < args.Length; index++) + { + var arg = args[index]; + switch (arg) + { + case "--spec": + options.SpecPath = ReadValue(args, ref index, arg); + break; + case "--spec-url": + options.SpecUrl = ReadValue(args, ref index, arg); + break; + case "--client-root": + options.ClientRoot = ReadValue(args, ref index, arg); + break; + case "--fail-on-drift": + options.FailOnDrift = true; + break; + case "--format": + options.Format = ReadValue(args, ref index, arg); + if (options.Format is not ("text" or "markdown")) + { + throw new ArgumentException("--format must be text or markdown."); + } + break; + case "-h": + case "--help": + options.ShowHelp = true; + break; + default: + throw new ArgumentException($"Unknown argument: {arg}"); + } + } + + return options; + } + + public static void PrintHelp() + { + Console.WriteLine( + """ + Usage: dotnet run --project eng/OpenApiCoverageCheck -- [options] + + Options: + --spec PATH OpenAPI YAML file to compare against. + --spec-url URL OpenAPI YAML URL to compare against. + --client-root PATH C# client project root. Defaults to ./mailinator-csharp-client. + --fail-on-drift Exit non-zero when spec and SDK differ. + --format FORMAT Output format: text or markdown. + -h, --help Show help. + """); + } + + private static string ReadValue(string[] args, ref int index, string option) + { + if (index + 1 >= args.Length) + { + throw new ArgumentException($"{option} requires a value."); + } + + index++; + return args[index]; + } +} diff --git a/eng/README.md b/eng/README.md new file mode 100644 index 0000000..0f001c7 --- /dev/null +++ b/eng/README.md @@ -0,0 +1,21 @@ +# Engineering Tools + +## OpenAPI Coverage Check + +Compare the C# client request surface against the Mailinator OpenAPI specification: + +```sh +dotnet run --project eng/OpenApiCoverageCheck -- --spec path/to/mailinator-api.yaml +``` + +If `--spec` is omitted, the tool fetches the current Mailinator OpenAPI YAML from: + +```text +https://raw.githubusercontent.com/manybrain/mailinatordocs/main/openapi/mailinator-api.yaml +``` + +Use `--fail-on-drift` in CI once the SDK is expected to be in sync with the spec: + +```sh +dotnet run --project eng/OpenApiCoverageCheck -- --spec path/to/mailinator-api.yaml --fail-on-drift +``` diff --git a/mailinator-csharp-client-tests/MessagesEndpointTests.cs b/mailinator-csharp-client-tests/MessagesEndpointTests.cs index def2427..2ae60cd 100644 --- a/mailinator-csharp-client-tests/MessagesEndpointTests.cs +++ b/mailinator-csharp-client-tests/MessagesEndpointTests.cs @@ -133,6 +133,7 @@ public async Task FetchMessageWithDeleteQueueParamsAsync() { await mailinatorClient.MessagesClient.FetchMessageAsync(request); }); + Assert.AreEqual(System.Net.HttpStatusCode.NotFound, exception.HttpStatusCode); } [TestMethod, TestCategory("Messages.FetchMessageWhenMessageDoesNotExistAsync")] @@ -143,6 +144,7 @@ public async Task FetchMessageWhenMessageDoesNotExistAsync() { await mailinatorClient.MessagesClient.FetchMessageAsync(request); }); + Assert.AreEqual(System.Net.HttpStatusCode.NotFound, exception.HttpStatusCode); } [TestMethod, TestCategory("Messages.FetchInboxMessageAsync")] @@ -167,6 +169,7 @@ public async Task FetchInboxMessageWhenMessageDoesNotExistAsync() { await mailinatorClient.MessagesClient.FetchInboxMessageAsync(request); }); + Assert.AreEqual(System.Net.HttpStatusCode.NotFound, exception.HttpStatusCode); } [TestMethod, TestCategory("Messages.FetchSMSMessagesAsync")] diff --git a/mailinator-csharp-client-tests/TestBase.cs b/mailinator-csharp-client-tests/TestBase.cs index cf270c7..78ef528 100644 --- a/mailinator-csharp-client-tests/TestBase.cs +++ b/mailinator-csharp-client-tests/TestBase.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; @@ -19,7 +20,7 @@ namespace mailinator_csharp_client_tests [TestClass] public class TestBase { - protected readonly MailinatorClient mailinatorClient; + protected MailinatorClient mailinatorClient; private Domain domain; @@ -37,23 +38,39 @@ public class TestBase private const string ENV_WEBHOOK_INBOX = "MAILINATOR_TEST_WEBHOOK_INBOX"; private const string ENV_WEBHOOK_CUSTOMSERVICE = "MAILINATOR_TEST_WEBHOOK_CUSTOMSERVICE"; + private static readonly string ApiToken; + + static TestBase() + { + LoadDotEnv(); + ApiToken = GetEnvironmentVariable(ENV_API_TOKEN); + } + protected TestBase() { - PrivateDomain = ENV_DOMAIN_PRIVATE; - DeleteDomain = ENV_DELETE_DOMAIN; - PrivateInbox = ENV_INBOX; + PrivateDomain = GetEnvironmentVariable(ENV_DOMAIN_PRIVATE); + DeleteDomain = GetEnvironmentVariable(ENV_DELETE_DOMAIN); + PrivateInbox = GetEnvironmentVariable(ENV_INBOX); InboxAll = "*"; - MessageIdWithAttachment = ENV_MESSAGE_WITH_ATTACHMENT_ID; - TeamSMSNumber = ENV_PHONE_NUMBER; - AttachmentId = ENV_ATTACHMENT_ID; - WebhookTokenPrivateDomain = ENV_WEBHOOKTOKEN_PRIVATEDOMAIN; - WebhookTokenCustomService = ENV_WEBHOOKTOKEN_CUSTOMSERVICE; - AuthSecret = ENV_AUTH_SECRET; - AuthId = ENV_AUTH_ID; - WebhookInbox = ENV_WEBHOOK_INBOX; - WebhookCustomService = ENV_WEBHOOK_CUSTOMSERVICE; - - mailinatorClient = new MailinatorClient(ENV_API_TOKEN); + MessageIdWithAttachment = GetEnvironmentVariable(ENV_MESSAGE_WITH_ATTACHMENT_ID); + TeamSMSNumber = GetEnvironmentVariable(ENV_PHONE_NUMBER); + AttachmentId = GetEnvironmentVariable(ENV_ATTACHMENT_ID); + WebhookTokenPrivateDomain = GetEnvironmentVariable(ENV_WEBHOOKTOKEN_PRIVATEDOMAIN); + WebhookTokenCustomService = GetEnvironmentVariable(ENV_WEBHOOKTOKEN_CUSTOMSERVICE); + AuthSecret = GetEnvironmentVariable(ENV_AUTH_SECRET); + AuthId = GetEnvironmentVariable(ENV_AUTH_ID); + WebhookInbox = GetEnvironmentVariable(ENV_WEBHOOK_INBOX); + WebhookCustomService = GetEnvironmentVariable(ENV_WEBHOOK_CUSTOMSERVICE); + + } + + [TestInitialize] + public void RequireApiToken() + { + if (string.IsNullOrWhiteSpace(ApiToken)) + Assert.Inconclusive($"Skipping integration test: set {ENV_API_TOKEN} in the repository .env file or process environment."); + + mailinatorClient = new MailinatorClient(ApiToken); } protected Domain Domain @@ -140,5 +157,57 @@ public Task PostNewMessageAsync(string domain, string inbox var request = new PostMessageRequest() { Domain = domain, Inbox = inbox, Message = message }; return mailinatorClient.MessagesClient.PostMessageAsync(request); } + + private static string GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } + + private static void LoadDotEnv() + { + var dotEnvPath = FindDotEnv(Environment.CurrentDirectory) ?? FindDotEnv(AppDomain.CurrentDomain.BaseDirectory); + if (dotEnvPath == null) + return; + + foreach (var line in File.ReadAllLines(dotEnvPath)) + { + var trimmedLine = line.Trim(); + if (trimmedLine.Length == 0 || trimmedLine.StartsWith("#")) + continue; + + if (trimmedLine.StartsWith("export ")) + trimmedLine = trimmedLine.Substring("export ".Length).TrimStart(); + + var separatorIndex = trimmedLine.IndexOf('='); + if (separatorIndex <= 0) + continue; + + var name = trimmedLine.Substring(0, separatorIndex).Trim(); + var value = trimmedLine.Substring(separatorIndex + 1).Trim(); + if (value.Length >= 2 && ((value.StartsWith("\"") && value.EndsWith("\"")) || (value.StartsWith("'") && value.EndsWith("'")))) + value = value.Substring(1, value.Length - 2); + + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(name))) + Environment.SetEnvironmentVariable(name, value); + } + } + + private static string FindDotEnv(string startDirectory) + { + if (string.IsNullOrWhiteSpace(startDirectory)) + return null; + + var directory = new DirectoryInfo(startDirectory); + while (directory != null) + { + var path = Path.Combine(directory.FullName, ".env"); + if (File.Exists(path)) + return path; + + directory = directory.Parent; + } + + return null; + } } } diff --git a/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs b/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs new file mode 100644 index 0000000..9797527 --- /dev/null +++ b/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using mailinator_csharp_client.Clients.ApiClients.Domains; +using mailinator_csharp_client.Clients.ApiClients.Messages; +using mailinator_csharp_client.Clients.HttpClient; +using mailinator_csharp_client.Models.Domains.Requests; +using mailinator_csharp_client.Models.Domains.Responses; +using mailinator_csharp_client.Models.Messages.Entities; +using mailinator_csharp_client.Models.Messages.Requests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RestSharp; + +namespace mailinator_csharp_client_unit_tests +{ + [TestClass] + public class ApiClientRequestTests + { + [TestMethod] + public async Task GetDomainAsync_BuildsGetRequestWithDomainUrlSegment() + { + var httpClient = new RecordingHttpClient(); + var client = new DomainsClient(httpClient, "domains"); + + await client.GetDomainAsync(new GetDomainRequest { DomainId = "example.com" }); + + Assert.AreEqual(Method.Get, httpClient.Request.Method); + Assert.AreEqual("domains/{domain_id}", httpClient.Request.Resource); + Assert.AreEqual("example.com", ParameterValue(httpClient.Request, "domain_id")); + } + + [TestMethod] + public async Task FetchInboxAsync_BuildsRouteAndOptionalQueryParameters() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + var request = new FetchInboxRequest + { + Domain = "example.com", + Inbox = "orders", + Skip = 10, + Limit = 20, + Sort = Sort.asc, + DecodeSubject = true, + Cursor = "next-page", + Full = true, + Delete = "30s", + Wait = "10s" + }; + + await client.FetchInboxAsync(request); + + Assert.AreEqual(Method.Get, httpClient.Request.Method); + Assert.AreEqual("domains/{domain}/inboxes/{inbox}", httpClient.Request.Resource); + Assert.AreEqual("example.com", ParameterValue(httpClient.Request, "domain")); + Assert.AreEqual("orders", ParameterValue(httpClient.Request, "inbox")); + Assert.AreEqual("10", ParameterValue(httpClient.Request, "skip")); + Assert.AreEqual("20", ParameterValue(httpClient.Request, "limit")); + Assert.AreEqual("asc", ParameterValue(httpClient.Request, "sort")); + Assert.AreEqual("True", ParameterValue(httpClient.Request, "decode_subject")); + Assert.AreEqual("next-page", ParameterValue(httpClient.Request, "cursor")); + Assert.AreEqual("True", ParameterValue(httpClient.Request, "full")); + Assert.AreEqual("30s", ParameterValue(httpClient.Request, "delete")); + Assert.AreEqual("10s", ParameterValue(httpClient.Request, "wait")); + } + + [TestMethod] + public async Task PostMessageAsync_BuildsPostRequestWithJsonBody() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + var message = new MessageToPost { From = "sender@example.com", Subject = "Hello", Text = "Body" }; + + await client.PostMessageAsync(new PostMessageRequest { Domain = "example.com", Inbox = "orders", Message = message }); + + Assert.AreEqual(Method.Post, httpClient.Request.Method); + Assert.AreEqual("domains/{domain}/inboxes/{inbox}/messages", httpClient.Request.Resource); + Assert.AreEqual("example.com", ParameterValue(httpClient.Request, "domain")); + Assert.AreEqual("orders", ParameterValue(httpClient.Request, "inbox")); + Assert.AreSame(message, httpClient.Request.Parameters.Single(parameter => parameter.Type == ParameterType.RequestBody).Value); + } + + private static object ParameterValue(RestRequest request, string name) + { + return request.Parameters.Single(parameter => parameter.Name == name).Value; + } + + private sealed class RecordingHttpClient : IHttpClient + { + public RestRequest Request { get; private set; } + + public RestRequest GetRequest(string url, Method method) + { + return new RestRequest(url, method); + } + + public Task ExecuteAsync(RestRequest request) + { + Request = request; + return Task.FromResult(default(T)); + } + + public Task ExecuteAsync(RestRequest request, Func customDeserializationFunction) + { + Request = request; + return Task.FromResult(default(T)); + } + } + } +} diff --git a/mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj b/mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj new file mode 100644 index 0000000..2b84370 --- /dev/null +++ b/mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj @@ -0,0 +1,15 @@ + + + net8.0 + false + true + + + + + + + + + + diff --git a/mailinator-csharp-client.sln b/mailinator-csharp-client.sln index ef5b66d..41dea74 100644 --- a/mailinator-csharp-client.sln +++ b/mailinator-csharp-client.sln @@ -7,6 +7,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "mailinator-csharp-client", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mailinator-csharp-client-tests", "mailinator-csharp-client-tests\mailinator-csharp-client-tests.csproj", "{D59A67E5-DE4B-49AF-BD14-143091B46527}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenApiCoverageCheck", "eng\OpenApiCoverageCheck\OpenApiCoverageCheck.csproj", "{C95520B0-8396-4D8C-9317-A8A958296A47}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "mailinator-csharp-client-unit-tests", "mailinator-csharp-client-unit-tests\mailinator-csharp-client-unit-tests.csproj", "{6F9324B4-AB58-4E62-90E0-9AF8B6C23B6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +25,14 @@ Global {D59A67E5-DE4B-49AF-BD14-143091B46527}.Debug|Any CPU.Build.0 = Debug|Any CPU {D59A67E5-DE4B-49AF-BD14-143091B46527}.Release|Any CPU.ActiveCfg = Release|Any CPU {D59A67E5-DE4B-49AF-BD14-143091B46527}.Release|Any CPU.Build.0 = Release|Any CPU + {C95520B0-8396-4D8C-9317-A8A958296A47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C95520B0-8396-4D8C-9317-A8A958296A47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C95520B0-8396-4D8C-9317-A8A958296A47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C95520B0-8396-4D8C-9317-A8A958296A47}.Release|Any CPU.Build.0 = Release|Any CPU + {6F9324B4-AB58-4E62-90E0-9AF8B6C23B6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F9324B4-AB58-4E62-90E0-9AF8B6C23B6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F9324B4-AB58-4E62-90E0-9AF8B6C23B6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F9324B4-AB58-4E62-90E0-9AF8B6C23B6E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/mailinator-csharp-client/Clients/ApiClients/Domains/DomainsClient.cs b/mailinator-csharp-client/Clients/ApiClients/Domains/DomainsClient.cs index bef586a..1785fc9 100644 --- a/mailinator-csharp-client/Clients/ApiClients/Domains/DomainsClient.cs +++ b/mailinator-csharp-client/Clients/ApiClients/Domains/DomainsClient.cs @@ -2,6 +2,7 @@ using mailinator_csharp_client.Models.Domains.Requests; using mailinator_csharp_client.Models.Domains.Responses; using RestSharp; +using System; using System.Threading.Tasks; namespace mailinator_csharp_client.Clients.ApiClients.Domains @@ -53,6 +54,7 @@ public async Task GetDomainAsync(GetDomainRequest request) /// /// CreateDomainRequest object. /// + [Obsolete("Deprecated: Domain create/delete endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task CreateDomainAsync(CreateDomainRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}", Method.Post); @@ -67,6 +69,7 @@ public async Task CreateDomainAsync(CreateDomainRequest re /// /// DeleteDomainRequest object. /// + [Obsolete("Deprecated: Domain create/delete endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task DeleteDomainAsync(DeleteDomainRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}", Method.Delete); diff --git a/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs b/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs index 4c23bb5..81c2453 100644 --- a/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs +++ b/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs @@ -5,6 +5,7 @@ using RestSharp; using System.IO; using System.Threading.Tasks; +using System; namespace mailinator_csharp_client.Clients.ApiClients.Messages { @@ -379,6 +380,7 @@ public async Task FetchInboxMessageRawAsync(FetchI /// /// FetchLatestMessagesResponse object. /// + [Obsolete("Deprecated: Latest message wildcard endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task FetchLatestMessagesAsync(FetchLatestMessagesRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/messages/*", Method.Get); @@ -394,6 +396,7 @@ public async Task FetchLatestMessagesAsync(FetchLat /// /// FetchLatestInboxMessagesResponse object. /// + [Obsolete("Deprecated: Latest message wildcard endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task FetchLatestInboxMessagesAsync(FetchLatestInboxMessagesRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/inboxes/{inbox}/messages/*", Method.Get); diff --git a/mailinator-csharp-client/Clients/ApiClients/Rules/RulesClient.cs b/mailinator-csharp-client/Clients/ApiClients/Rules/RulesClient.cs index a875446..1b47226 100644 --- a/mailinator-csharp-client/Clients/ApiClients/Rules/RulesClient.cs +++ b/mailinator-csharp-client/Clients/ApiClients/Rules/RulesClient.cs @@ -3,6 +3,7 @@ using mailinator_csharp_client.Models.Rules.Requests; using mailinator_csharp_client.Models.Rules.Responses; using RestSharp; +using System; using System.Threading.Tasks; namespace mailinator_csharp_client.Clients.ApiClients.Rules @@ -30,6 +31,7 @@ public RulesClient(IHttpClient httpClient, string endpointUrl) /// /// CreateRuleRequest object. /// + [Obsolete("Deprecated: Rules endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task CreateRuleAsync(CreateRuleRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}/rules", Method.Post); @@ -46,6 +48,7 @@ public async Task CreateRuleAsync(CreateRuleRequest request) /// /// EnableRuleRequest object. /// + [Obsolete("Deprecated: Rules endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task EnableRuleAsync(EnableRuleRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}/rules/{ruleId}/enable", Method.Put); @@ -61,6 +64,7 @@ public async Task EnableRuleAsync(EnableRuleRequest request) /// /// DisableRuleRequest object. /// + [Obsolete("Deprecated: Rules endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task DisableRuleAsync(DisableRuleRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}/rules/{ruleId}/disable", Method.Put); @@ -76,6 +80,7 @@ public async Task DisableRuleAsync(DisableRuleRequest reque /// /// GetAllRulesRequest object. /// + [Obsolete("Deprecated: Rules endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task GetAllRulesAsync(GetAllRulesRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}/rules", Method.Get); @@ -90,6 +95,7 @@ public async Task GetAllRulesAsync(GetAllRulesRequest reque /// /// GetRuleRequest object. /// + [Obsolete("Deprecated: Rules endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task GetRuleAsync(GetRuleRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}/rules/{ruleId}", Method.Get); @@ -105,6 +111,7 @@ public async Task GetRuleAsync(GetRuleRequest request) /// /// DeleteRuleRequest object. /// + [Obsolete("Deprecated: Rules endpoints are not present in the current Mailinator OpenAPI spec. This method may be removed in a future release.")] public async Task DeleteRuleAsync(DeleteRuleRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "/{domain_id}/rules/{ruleId}", Method.Delete); diff --git a/mailinator-csharp-client/mailinator-csharp-client.csproj b/mailinator-csharp-client/mailinator-csharp-client.csproj index bc35154..4021225 100644 --- a/mailinator-csharp-client/mailinator-csharp-client.csproj +++ b/mailinator-csharp-client/mailinator-csharp-client.csproj @@ -8,16 +8,16 @@ https://github.com/manybrain/mailinator-csharp-client Client Library used to interact with the Mailinator API - © Manybrain 2025 + © Manybrain 2026 Marian Melnychuk Manybrain; ApiClient git MIT README.md MailinatorApiClient - 1.0.6 - 1.0.6 - 1.0.6 + 1.0.7 + 1.0.7 + 1.0.7 True