diff --git a/.env.example b/.env.example index 7f3f684..497b5d7 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ MAILINATOR_TEST_API_TOKEN= # Optional values used by endpoint-specific integration tests. MAILINATOR_TEST_DOMAIN_PRIVATE= MAILINATOR_TEST_INBOX= +# Existing email in MAILINATOR_TEST_DOMAIN_PRIVATE, used by header and summary retrieval tests. +MAILINATOR_TEST_MESSAGE_ID= MAILINATOR_TEST_PHONE_NUMBER= MAILINATOR_TEST_MESSAGE_WITH_ATTACHMENT_ID= MAILINATOR_TEST_ATTACHMENT_ID= @@ -13,4 +15,6 @@ MAILINATOR_TEST_WEBHOOKTOKEN_CUSTOMSERVICE= MAILINATOR_TEST_AUTH_SECRET= MAILINATOR_TEST_AUTH_ID= MAILINATOR_TEST_WEBHOOK_INBOX= +# Set to 1 to run domain webhook tests, which create four messages. +MAILINATOR_TEST_RUN_WEBHOOKS=0 MAILINATOR_TEST_WEBHOOK_CUSTOMSERVICE= diff --git a/CHANGELOG.md b/CHANGELOG.md index 70829a1..18c11e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ 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.8] - TBD + +### Added + +- Domain and inbox webhook injection via `PostWebhookMessageAsync` and `PostWebhookInboxMessageAsync`, supporting both webhook authentication forms, rich JSON payloads, offline unit tests, and opt-in integration tests. + +- `MessagesClient.GetMessageTextAsync`, `GetMessageTextPlainAsync`, and `GetMessageTextHtmlAsync` with request/response models, offline route and JSON deserialization tests, and opt-in read-only integration tests. + +- `MessagesClient.GetMessageSummaryAsync` with request/response models, offline request and deserialization tests, and an opt-in live test for an existing email. +- `MessagesClient.GetMessageHeadersAsync` with request/response models, offline request and deserialization tests, and an opt-in live test for an existing email. +- `MessagesClient.ListDomainMessagesAsync` and `ListDomainMessagesRequest` for domain-wide message listing, with optional inbox filtering and all documented listing parameters. Returns the existing `FetchInboxResponse` model. +- Offline tests for domain listing query parameters, default behavior, and wildcard filtering. + +### Security + +- Updated the transitive `System.Text.Json` dependency to `10.0.11` to remediate CVE-2024-43485. + +### Changed + +- Updated `Newtonsoft.Json` to `13.0.4`. +- Updated `RestSharp` to `114.0.0`. +- Updated the OpenAPI coverage tool's `Microsoft.OpenApi.Readers` dependency to `1.6.31`. +- Updated the offline unit-test stack to `Microsoft.NET.Test.Sdk` `18.9.0` and MSTest `4.4.0`. +- Migrated the live integration-test project to SDK-style `PackageReference` and the same current test stack. +- Updated integration-test exception assertions and binding redirects for MSTest 4 compatibility. +- Added NuGet package lock files. + +### Deprecated + +- `AuthenticatorsClient.GetAuthenticatorsAsync`, `GetAuthenticatorAsync`, and `GetAuthenticatorByIdAsync`; use `GetAuthenticatorsByIdAsync` for the documented stored-authenticator operation. + +### Fixed + +- Forwarded the optional `delete` query parameter in `FetchInboxMessageAsync`. + ## [1.0.7] - 2026-08-15 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..f860221 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/EXAMPLES.md b/EXAMPLES.md index 16341e9..0faafcd 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -43,7 +43,7 @@ var response = await client.MessagesClient.FetchInboxAsync(request); ## Authenticators -Instant TOTP code + list authenticators: +Instant TOTP code + get a stored authenticator: ```csharp using mailinator_csharp_client; @@ -54,8 +54,6 @@ 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" }); ``` @@ -78,6 +76,73 @@ var domain = await client.DomainsClient.GetDomainAsync( ## Messages +### List domain messages + +List messages across all inboxes in a domain: + +```csharp +using mailinator_csharp_client.Models.Messages.Requests; + +// Uses the authenticated client created in Setup. +var request = new ListDomainMessagesRequest +{ + Domain = "your_private_domain.com", + Limit = 20 +}; + +var response = await client.MessagesClient.ListDomainMessagesAsync(request); +var messages = response.Messages; + +// Fetch the next page only when the API supplies a cursor. +if (!string.IsNullOrEmpty(response.Cursor)) +{ + request.Cursor = response.Cursor; + var nextPage = await client.MessagesClient.ListDomainMessagesAsync(request); +} +``` + +`Inbox` is an optional query filter: omit it or set it to `"*"` for all inboxes, or supply an inbox name/prefix such as `"orders*"`. The request also supports `Skip`, `Limit`, `Sort`, `DecodeSubject`, `Cursor`, `Full`, `Wait`, and `Delete`. Set `Full = true` to request full message content. `Delete` schedules deletion after retrieval (for example, `"30s"`); it is omitted by default. + +The result is a `FetchInboxResponse`, sharing the inbox-listing response model and pagination cursor. + +### Get message summary + +```csharp +using mailinator_csharp_client.Models.Messages.Requests; + +// Uses the authenticated client created in Setup. +var response = await client.MessagesClient.GetMessageSummaryAsync( + new GetMessageSummaryRequest + { + Domain = "your_private_domain.com", + MessageId = "your-message-id" + }); + +var summary = response.Summary; +``` + +`Summary` reuses the `Message` model and contains the subject, domain, sender (`From`), message ID, recipient (`To`), and timestamp (`Time`). This endpoint does not return body or attachment content. + +### Get message headers + +```csharp +using mailinator_csharp_client.Models.Messages.Requests; + +// Uses the authenticated client created in Setup. +var response = await client.MessagesClient.GetMessageHeadersAsync( + new GetMessageHeadersRequest + { + Domain = "your_private_domain.com", + MessageId = "your-message-id" + }); + +var headers = response.Headers; +``` + +Use a message ID returned by inbox or domain listing. `Headers` is a `Dictionary` that preserves custom header names. Values can be strings or JSON arrays (for example, `received`), matching the existing full-message header model. + +### Post a message + Post (inject) a message: ```csharp @@ -218,3 +283,58 @@ var customServiceInboxWebhook = await client.WebhooksClient.PrivateCustomService - Ensure you’re using an API token from your Mailinator team settings. - For webhook injection, use webhook tokens (`whtoken`) instead of your API token. + +## Get message content + +Use a message ID returned by inbox or domain listing: + +```csharp +var extracted = await client.MessagesClient.GetMessageTextAsync( + new GetMessageTextRequest { Domain = "your-private-domain.com", MessageId = "your-message-id" }); +var plain = await client.MessagesClient.GetMessageTextPlainAsync( + new GetMessageTextPlainRequest { Domain = "your-private-domain.com", MessageId = "your-message-id" }); +var html = await client.MessagesClient.GetMessageTextHtmlAsync( + new GetMessageTextHtmlRequest { Domain = "your-private-domain.com", MessageId = "your-message-id" }); + +string extractedText = extracted.Text; +string plainText = plain.TextPlain; +string htmlBody = html.TextHtml; +``` + +These endpoints return JSON wrappers with `text`, `text/plain`, and `text/html` fields respectively. The SDK preserves their content, including HTML markup and any quoted-printable artifacts such as `=C2=A0` in extracted text. Empty strings are preserved. These operations do not delete the message. + +## Domain and inbox webhooks + +These endpoints authenticate with webhook tokens; no API token is needed. Request types are in `mailinator_csharp_client.Models.Webhooks.Requests` and `WebhookMessage` is in `mailinator_csharp_client.Models.Webhooks.Entities`. + +```csharp +var client = new MailinatorClient(); +var webhookToken = Environment.GetEnvironmentVariable("MAILINATOR_WEBHOOK_TOKEN"); +var payload = new WebhookMessage +{ + To = "orders", + From = "sender@example.com", + Subject = "Order notification", + Text = "Order received", + Html = "

Order received

" +}; + +var domainResult = await client.WebhooksClient.PostWebhookMessageAsync( + new PostWebhookMessageRequest + { + Domain = "your-private-domain.com", + WebhookToken = webhookToken, + Webhook = payload + }); + +var inboxResult = await client.WebhooksClient.PostWebhookInboxMessageAsync( + new PostWebhookInboxMessageRequest + { + Domain = "your-private-domain.com", + Inbox = "orders", + WebhookToken = webhookToken, + Webhook = payload + }); +``` + +Both methods also accept the webhook token in `Domain`; omit `WebhookToken` for that form. The payload's `To` field is required by the specification. `Headers` accepts a dictionary of string values, and `AdditionalProperties` accepts custom JSON fields. Both responses expose `Status` and `Id`. Existing private/custom-service webhook methods remain available. diff --git a/README.md b/README.md index 7266a6e..47f7ce0 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,16 @@ var response = await client.MessagesClient.FetchInboxAsync( All API operations are asynchronous and end in `Async`. Operations are grouped under `MessagesClient`, `DomainsClient`, `AuthenticatorsClient`, `StatsClient`, `WebhooksClient`, and `RulesClient`. +To list messages across a domain, use `MessagesClient.ListDomainMessagesAsync(new ListDomainMessagesRequest { Domain = "your-private-domain.com" })`. The optional `Inbox` query filter scopes results to an inbox; omit it to include all inboxes. See the [domain listing example](EXAMPLES.md#list-domain-messages) for pagination. + ## API reference +Retrieve message metadata without the body with `MessagesClient.GetMessageSummaryAsync(new GetMessageSummaryRequest { Domain = "your-private-domain.com", MessageId = "your-message-id" })`. See the [message summary example](EXAMPLES.md#get-message-summary). + +Retrieve message content with `MessagesClient.GetMessageTextAsync`, `GetMessageTextPlainAsync`, or `GetMessageTextHtmlAsync`. See the [message content examples](EXAMPLES.md#get-message-content). + +Retrieve SMTP headers for an existing message with `MessagesClient.GetMessageHeadersAsync(new GetMessageHeadersRequest { Domain = "your-private-domain.com", MessageId = "your-message-id" })`. See the [header retrieval example](EXAMPLES.md#get-message-headers). + - [Mailinator API reference](https://www.mailinator.com/documentation/docs/api/index.html) describes the REST API. - [REFERENCE.md](REFERENCE.md) lists the operations currently exposed by this SDK. - [EXAMPLES.md](EXAMPLES.md) contains examples for common SDK workflows. @@ -97,3 +105,5 @@ dotnet test mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-te The separate legacy integration suite calls the live Mailinator API and requires deliberate account configuration. Some tests create or delete remote resources. Read [TESTING.md](TESTING.md) before running it. To compare the SDK request surface with the OpenAPI specification, use the [OpenAPI coverage check](eng/README.md#openapi-coverage-check). + +Domain and inbox webhook injection is available through `WebhooksClient.PostWebhookMessageAsync` and `PostWebhookInboxMessageAsync`, using a webhook token with a tokenless `new MailinatorClient()`. See the [webhook examples](EXAMPLES.md#domain-and-inbox-webhooks). diff --git a/REFERENCE.md b/REFERENCE.md index 8f42743..d530dc0 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -18,6 +18,12 @@ An authenticated client exposes `MessagesClient`, `DomainsClient`, `Authenticato | Operation | Request | Response | | --- | --- | --- | | `FetchInboxAsync` | `FetchInboxRequest` | `FetchInboxResponse` | +| `ListDomainMessagesAsync` | `ListDomainMessagesRequest` | `FetchInboxResponse` | +| `GetMessageHeadersAsync` | `GetMessageHeadersRequest` | `GetMessageHeadersResponse` | +| `GetMessageSummaryAsync` | `GetMessageSummaryRequest` | `GetMessageSummaryResponse` | +| `GetMessageTextAsync` | `GetMessageTextRequest` | `GetMessageTextResponse` | +| `GetMessageTextPlainAsync` | `GetMessageTextPlainRequest` | `GetMessageTextPlainResponse` | +| `GetMessageTextHtmlAsync` | `GetMessageTextHtmlRequest` | `GetMessageTextHtmlResponse` | | `FetchInboxMessageAsync` | `FetchInboxMessageRequest` | `FetchInboxMessageResponse` | | `FetchMessageAsync` | `FetchMessageRequest` | `FetchMessageResponse` | | `FetchSMSMessagesAsync` | `FetchSMSMessagesRequest` | `FetchSMSMessagesResponse` | @@ -74,6 +80,8 @@ These operations use webhook tokens rather than the API token supplied to `Maili | Operation | Request | Response | | --- | --- | --- | +| `PostWebhookMessageAsync` | `PostWebhookMessageRequest` | `PostWebhookMessageResponse` | +| `PostWebhookInboxMessageAsync` | `PostWebhookInboxMessageRequest` | `PostWebhookMessageResponse` | | `PrivateWebhookAsync` | `PrivateWebhookRequest` | `PrivateWebhookResponse` | | `PrivateInboxWebhookAsync` | `PrivateInboxWebhookRequest` | `PrivateWebhookResponse` | | `PrivateCustomServiceWebhookAsync` | `PrivateCustomServiceWebhookRequest` | `PrivateCustomServiceWebhookResponse` | @@ -100,6 +108,7 @@ The following public methods are marked `[Obsolete]`. They remain in the current | --- | --- | --- | | `MessagesClient` | `FetchLatestMessagesAsync`, `FetchLatestInboxMessagesAsync` | The wildcard “latest” endpoints are not in the current OpenAPI specification. | | `DomainsClient` | `CreateDomainAsync`, `DeleteDomainAsync` | Domain create/delete endpoints are not in the current OpenAPI specification. | +| `AuthenticatorsClient` | `GetAuthenticatorsAsync`, `GetAuthenticatorAsync`, `GetAuthenticatorByIdAsync` | These list/get endpoints are not in the current OpenAPI specification. | | `RulesClient` | All operations | Rules endpoints are not in the current OpenAPI specification. | See [ROADMAP.md](ROADMAP.md) for known specification gaps and planned alignment work. diff --git a/ROADMAP.md b/ROADMAP.md index c1e1f6b..86eec10 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,52 +13,24 @@ This document is a living roadmap for the Mailinator C# client. It’s intention - 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. +- Known gaps / bugs: missing spec endpoints and documented SDK-only compatibility operations. Path parameter-name differences and the SMS convenience alias are intentional and do not change the resulting HTTP route. ## Dependency Maintenance -Audit refreshed: 2026-08-10. +Dependencies were audited on 2026-09-04. All projects have a clean NuGet vulnerability audit, and committed package lock files make restores reproducible. -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) +## Gap Analysis (2026-09-08) 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 operations: 51 +- Exact matches: 29 +- Missing from SDK: 2 - SDK-only (no spec match): 17 - SDK aliases / convenience wrappers: 1 - Path parameter-name mismatches: 4 -- Operations with missing query params: 1 +- Operations with missing query params: 0 Re-run locally: @@ -70,52 +42,44 @@ Re-run locally: 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”. +These SDK operations do not have a matching operation in the current OpenAPI spec. All remaining cases now have a compatibility decision recorded below. + +- **Webhooks** private/custom-service endpoints (`POST /api/v2/domains/private/...`) are intentional, supported compatibility APIs shared with the JavaScript client. Keep them documented and do not deprecate them solely because they are absent from the current 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/...`) +### Resolved compatibility decisions + +The following operations are already deprecated. Retain them for source compatibility and skip further spec-alignment work; remove them only in a future breaking major release. + +- **Rules** — 6 operations under `/api/v2/domains/{domain_id}/rules...` +- **Domains** — create/delete (`POST`/`DELETE /api/v2/domains/{domain_id}`) +- **Authenticators** — the unsupported list/get variants under `/api/v2/authenticator...` and `/api/v2/authenticators` are deprecated; retain them only until a future breaking major release. +- **Messages** — “latest” wildcard endpoints (`GET .../messages/*`) ### Work Items (spec alignment) -Path template parameter names differ from the spec (non-breaking, but worth aligning for clarity and consistency): +Path template parameter names differ from the spec. These are intentional, non-functional differences; the resulting HTTP routes are the same, so no SDK change is planned: - 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. +`FetchSMSMessagesAsync` is also an intentional convenience alias for inbox retrieval using the team SMS number. `FetchInboxAsync` remains available when callers need the full inbox-listing parameter set. ## 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. +- Improve test coverage. - Add more ergonomic APIs / helpers while keeping the low-level request mapping. ## Long-Term diff --git a/TESTING.md b/TESTING.md index b829c7d..e50e4a7 100644 --- a/TESTING.md +++ b/TESTING.md @@ -4,7 +4,7 @@ The repository has two distinct MSTest suites: fast offline unit tests and legac ## Offline unit tests -The .NET 8 unit-test project verifies request construction without making network calls: +The .NET 8 unit-test project verifies request construction for every public async SDK operation without making network calls. It asserts each operation's HTTP method, route, path/query parameters, and JSON body where applicable: ```sh dotnet test mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj @@ -33,6 +33,7 @@ The `.env` file is excluded from Git. Process environment variables take precede | `MAILINATOR_TEST_API_TOKEN` | API token used by authenticated integration tests. | | `MAILINATOR_TEST_DOMAIN_PRIVATE` | Private domain used by domain and message tests. | | `MAILINATOR_TEST_INBOX` | Existing inbox in the configured private domain. | +| `MAILINATOR_TEST_MESSAGE_ID` | Existing email in the configured private domain, for headers, summary, and content retrieval. Content tests require nonempty plain-text and HTML MIME parts; header retrieval requires SMTP headers. | | `MAILINATOR_TEST_PHONE_NUMBER` | Team SMS number whose messages can be fetched. | | `MAILINATOR_TEST_MESSAGE_WITH_ATTACHMENT_ID` | ID of an existing message that has an attachment. | | `MAILINATOR_TEST_ATTACHMENT_ID` | Attachment ID belonging to the configured message. | @@ -51,3 +52,55 @@ dotnet test mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj ``` Prefer a test filter when validating a specific endpoint, especially for operations that mutate remote state. + +To test header retrieval only, configure `MAILINATOR_TEST_API_TOKEN`, `MAILINATOR_TEST_DOMAIN_PRIVATE`, and `MAILINATOR_TEST_MESSAGE_ID`, then run: + +```sh +dotnet test mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj --filter "FullyQualifiedName=mailinator_csharp_client_tests.MessagesEndpointTests.GetMessageHeadersAsync" +``` + +Use an existing email received through SMTP with headers; an attachment is not required. This test only reads the message headers and does not create or delete messages. Missing configuration marks the test inconclusive. + +Summary retrieval uses the same three environment variables and reads the existing message without creating or deleting data: + +```sh +dotnet test mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj --filter "FullyQualifiedName=mailinator_csharp_client_tests.MessagesEndpointTests.GetMessageSummaryAsync" +``` + +### Message content endpoints + +The three tests in `MessageContentEndpointTests` only read an existing email. Configure `MAILINATOR_TEST_API_TOKEN`, `MAILINATOR_TEST_DOMAIN_PRIVATE`, and `MAILINATOR_TEST_MESSAGE_ID`. Use an SMTP email containing both nonempty `text/plain` and `text/html` MIME parts. Missing configuration marks tests inconclusive; an unsuitable or expired fixture fails the content assertions. + +Run the focused offline tests: + +```sh +dotnet test mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj --filter "FullyQualifiedName~MessageContentTests" +``` + +Run all offline tests using the unfiltered command in the first section, then run the new read-only integration tests on a compatible Windows machine: + +```sh +dotnet test mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj --filter "FullyQualifiedName~MessageContentEndpointTests" +``` + +To also validate the existing header and summary endpoints without selecting other message tests: + +```sh +dotnet test mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj --filter "FullyQualifiedName~MessageContentEndpointTests|FullyQualifiedName=mailinator_csharp_client_tests.MessagesEndpointTests.GetMessageHeadersAsync|FullyQualifiedName=mailinator_csharp_client_tests.MessagesEndpointTests.GetMessageSummaryAsync" +``` + +### Domain and inbox webhooks + +Offline tests cover both authentication forms, request bodies, optional token queries, custom payload fields, and response deserialization: + +```sh +dotnet test mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj --filter "FullyQualifiedName~WebhookMessageTests" +``` + +`DomainWebhookEndpointTests` loads the repository `.env` with process environment variables taking precedence. It does not require an API token. Set `MAILINATOR_TEST_DOMAIN_PRIVATE`, `MAILINATOR_TEST_WEBHOOKTOKEN_PRIVATEDOMAIN`, and `MAILINATOR_TEST_WEBHOOK_INBOX` for a test domain. Set `MAILINATOR_TEST_RUN_WEBHOOKS=1` to opt in. Without the opt-in or required configuration, tests are inconclusive. + +```sh +dotnet test mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj --filter "FullyQualifiedName~DomainWebhookEndpointTests" +``` + +This runs four cases: domain and inbox injection with query-token and path-token authentication. Each creates one message and checks the acceptance status and message ID; it does not verify subsequent delivery or delete messages. diff --git a/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj b/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj index 8a4f8a9..7178212 100644 --- a/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj +++ b/eng/OpenApiCoverageCheck/OpenApiCoverageCheck.csproj @@ -8,7 +8,7 @@ - + diff --git a/eng/OpenApiCoverageCheck/packages.lock.json b/eng/OpenApiCoverageCheck/packages.lock.json new file mode 100644 index 0000000..a717b22 --- /dev/null +++ b/eng/OpenApiCoverageCheck/packages.lock.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Microsoft.OpenApi.Readers": { + "type": "Direct", + "requested": "[1.6.31, )", + "resolved": "1.6.31", + "contentHash": "MbtHhR2vYs11EE71ywvmyppzp0noC3XHKzdI6m/9mwKRzw4IVCGuHHWDnmjr1BX90CIOOxbz6WszBJt3a+WGBg==", + "dependencies": { + "Microsoft.OpenApi": "1.6.31", + "SharpYaml": "2.1.5" + } + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "1.6.31", + "contentHash": "58ggc22i0BKvHuVoj5oVVQ7EbMkEMY0FvGeoQ/yBemk36pUbroDoj1XxRTnCi+fsgDTDuf+YZmigHkEzs5B2wA==" + }, + "SharpYaml": { + "type": "Transitive", + "resolved": "2.1.5", + "contentHash": "1pNSpfXxNAgCQ9sacKKpEWO2DxTefoLEjw0v1nWMvyJScUWacoHvjXr4LfcUkhm0qYgNYripFFiUeEzW4tdXIQ==" + } + } + } +} \ No newline at end of file diff --git a/mailinator-csharp-client-tests/DomainWebhookEndpointTests.cs b/mailinator-csharp-client-tests/DomainWebhookEndpointTests.cs new file mode 100644 index 0000000..081dade --- /dev/null +++ b/mailinator-csharp-client-tests/DomainWebhookEndpointTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Threading.Tasks; +using mailinator_csharp_client; +using mailinator_csharp_client.Helpers; +using mailinator_csharp_client.Models.Webhooks.Entities; +using mailinator_csharp_client.Models.Webhooks.Requests; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace mailinator_csharp_client_tests +{ + // Explicitly opt in: each test case creates one message and does not delete it. + [TestClass] + public class DomainWebhookEndpointTests + { + [TestInitialize] + public void LoadConfiguration() + { + TestEnvironment.LoadDotEnv(); + } + + [TestMethod, TestCategory("Webhooks.PostWebhookMessageAsync")] + [DataRow(false)] + [DataRow(true)] + public async Task PostWebhookMessageAsync(bool tokenInPath) + { + if (Environment.GetEnvironmentVariable("MAILINATOR_TEST_RUN_WEBHOOKS") != "1") + Assert.Inconclusive("Set MAILINATOR_TEST_RUN_WEBHOOKS=1 to authorize creating test messages."); + var domain = Environment.GetEnvironmentVariable("MAILINATOR_TEST_DOMAIN_PRIVATE"); + var token = Environment.GetEnvironmentVariable("MAILINATOR_TEST_WEBHOOKTOKEN_PRIVATEDOMAIN"); + var inbox = Environment.GetEnvironmentVariable("MAILINATOR_TEST_WEBHOOK_INBOX"); + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(inbox) || + (!tokenInPath && string.IsNullOrWhiteSpace(domain))) + Assert.Inconclusive("Configure the private domain, webhook token, and webhook inbox process environment variables."); + + var client = new MailinatorClient(); + try + { + var response = await client.WebhooksClient.PostWebhookMessageAsync(new PostWebhookMessageRequest + { + Domain = tokenInPath ? token : domain, + WebhookToken = tokenInPath ? null : token, + Webhook = new WebhookMessage + { + To = inbox, From = "sdk-test@example.com", + Subject = "C# webhook test " + Guid.NewGuid().ToString("N"), + Text = "Webhook integration test", Html = "

Webhook integration test

" + } + }); + Assert.IsNotNull(response); + Assert.IsTrue(response.Status == "ok", "The webhook should be accepted."); + Assert.IsFalse(string.IsNullOrWhiteSpace(response.Id), "The response should include a message ID."); + } + catch (ApiException exception) + { + // Never include token-bearing URLs or API response bodies in test output. + Assert.Fail($"Webhook injection failed with HTTP status {(int)exception.HttpStatusCode}."); + } + } + + [TestMethod, TestCategory("Webhooks.PostWebhookInboxMessageAsync")] + [DataRow(false)] + [DataRow(true)] + public async Task PostWebhookInboxMessageAsync(bool tokenInPath) + { + if (Environment.GetEnvironmentVariable("MAILINATOR_TEST_RUN_WEBHOOKS") != "1") + Assert.Inconclusive("Set MAILINATOR_TEST_RUN_WEBHOOKS=1 to authorize creating test messages."); + var domain = Environment.GetEnvironmentVariable("MAILINATOR_TEST_DOMAIN_PRIVATE"); + var token = Environment.GetEnvironmentVariable("MAILINATOR_TEST_WEBHOOKTOKEN_PRIVATEDOMAIN"); + var inbox = Environment.GetEnvironmentVariable("MAILINATOR_TEST_WEBHOOK_INBOX"); + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(inbox) || + (!tokenInPath && string.IsNullOrWhiteSpace(domain))) + Assert.Inconclusive("Configure the private domain, webhook token, and webhook inbox process environment variables."); + + var client = new MailinatorClient(); + try + { + var response = await client.WebhooksClient.PostWebhookInboxMessageAsync(new PostWebhookInboxMessageRequest + { + Domain = tokenInPath ? token : domain, + WebhookToken = tokenInPath ? null : token, + Inbox = inbox, + Webhook = new WebhookMessage + { + To = inbox, From = "sdk-test@example.com", + Subject = "C# webhook test " + Guid.NewGuid().ToString("N"), + Text = "Webhook integration test", Html = "

Webhook integration test

" + } + }); + Assert.IsNotNull(response); + Assert.IsTrue(response.Status == "ok", "The webhook should be accepted."); + Assert.IsFalse(string.IsNullOrWhiteSpace(response.Id), "The response should include a message ID."); + } + catch (ApiException exception) + { + // Never include token-bearing URLs or API response bodies in test output. + Assert.Fail($"Webhook injection failed with HTTP status {(int)exception.HttpStatusCode}."); + } + } + + } +} diff --git a/mailinator-csharp-client-tests/MessageContentEndpointTests.cs b/mailinator-csharp-client-tests/MessageContentEndpointTests.cs new file mode 100644 index 0000000..0e44cff --- /dev/null +++ b/mailinator-csharp-client-tests/MessageContentEndpointTests.cs @@ -0,0 +1,79 @@ +using System.Threading.Tasks; +using mailinator_csharp_client.Helpers; +using mailinator_csharp_client.Models.Messages.Requests; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace mailinator_csharp_client_tests +{ + // Read-only tests against an existing SMTP email with plain-text and HTML parts. + [TestClass] + public class MessageContentEndpointTests : TestBase + { + [TestMethod, TestCategory("Messages.GetMessageTextAsync")] + public async Task GetMessageTextAsync() + { + if (string.IsNullOrWhiteSpace(PrivateDomain) || string.IsNullOrWhiteSpace(MessageId)) + Assert.Inconclusive("Set MAILINATOR_TEST_DOMAIN_PRIVATE and MAILINATOR_TEST_MESSAGE_ID for an existing multipart email."); + + try + { + var response = await mailinatorClient.MessagesClient.GetMessageTextAsync( + new GetMessageTextRequest { Domain = PrivateDomain, MessageId = MessageId }); + + Assert.IsNotNull(response); + Assert.IsFalse(string.IsNullOrWhiteSpace(response.Text), + "The configured email must contain nonempty text content."); + } + catch (ApiException exception) + { + // Do not expose message content or API error bodies in test output. + Assert.Fail($"Message content retrieval failed with HTTP status {(int)exception.HttpStatusCode}."); + } + } + + [TestMethod, TestCategory("Messages.GetMessageTextPlainAsync")] + public async Task GetMessageTextPlainAsync() + { + if (string.IsNullOrWhiteSpace(PrivateDomain) || string.IsNullOrWhiteSpace(MessageId)) + Assert.Inconclusive("Set MAILINATOR_TEST_DOMAIN_PRIVATE and MAILINATOR_TEST_MESSAGE_ID for an existing multipart email."); + + try + { + var response = await mailinatorClient.MessagesClient.GetMessageTextPlainAsync( + new GetMessageTextPlainRequest { Domain = PrivateDomain, MessageId = MessageId }); + + Assert.IsNotNull(response); + Assert.IsFalse(string.IsNullOrWhiteSpace(response.TextPlain), + "The configured email must contain nonempty textplain content."); + } + catch (ApiException exception) + { + // Do not expose message content or API error bodies in test output. + Assert.Fail($"Message content retrieval failed with HTTP status {(int)exception.HttpStatusCode}."); + } + } + + [TestMethod, TestCategory("Messages.GetMessageTextHtmlAsync")] + public async Task GetMessageTextHtmlAsync() + { + if (string.IsNullOrWhiteSpace(PrivateDomain) || string.IsNullOrWhiteSpace(MessageId)) + Assert.Inconclusive("Set MAILINATOR_TEST_DOMAIN_PRIVATE and MAILINATOR_TEST_MESSAGE_ID for an existing multipart email."); + + try + { + var response = await mailinatorClient.MessagesClient.GetMessageTextHtmlAsync( + new GetMessageTextHtmlRequest { Domain = PrivateDomain, MessageId = MessageId }); + + Assert.IsNotNull(response); + Assert.IsFalse(string.IsNullOrWhiteSpace(response.TextHtml), + "The configured email must contain nonempty texthtml content."); + } + catch (ApiException exception) + { + // Do not expose message content or API error bodies in test output. + Assert.Fail($"Message content retrieval failed with HTTP status {(int)exception.HttpStatusCode}."); + } + } + + } +} diff --git a/mailinator-csharp-client-tests/MessagesEndpointTests.cs b/mailinator-csharp-client-tests/MessagesEndpointTests.cs index 2ae60cd..1ada1b7 100644 --- a/mailinator-csharp-client-tests/MessagesEndpointTests.cs +++ b/mailinator-csharp-client-tests/MessagesEndpointTests.cs @@ -12,6 +12,83 @@ namespace mailinator_csharp_client_tests [TestClass] public class MessagesEndpointTests : TestBase { + [TestMethod, TestCategory("Messages.GetMessageSummaryAsync")] + public async Task GetMessageSummaryAsync() + { + if (string.IsNullOrWhiteSpace(PrivateDomain) || string.IsNullOrWhiteSpace(MessageId)) + Assert.Inconclusive("Set MAILINATOR_TEST_DOMAIN_PRIVATE and MAILINATOR_TEST_MESSAGE_ID for an existing email."); + + mailinator_csharp_client.Models.Responses.GetMessageSummaryResponse response; + try + { + response = await mailinatorClient.MessagesClient.GetMessageSummaryAsync( + new GetMessageSummaryRequest { Domain = PrivateDomain, MessageId = MessageId }); + } + catch (ApiException exception) + { + // Do not include potentially sensitive response bodies in test output. + Assert.Fail($"Message summary retrieval failed with HTTP status {(int)exception.HttpStatusCode}."); + return; + } + + Assert.IsNotNull(response); + Assert.IsNotNull(response.Summary); + Assert.IsTrue(response.Summary.Id == MessageId, "The summary should identify the requested message."); + // Domain assertion deferred: the live API currently reports "public" for private-domain messages. + // Assert.IsTrue(response.Summary.Domain == PrivateDomain, "The summary should identify the requested domain."); + Assert.IsNull(response.Summary.Parts); + Assert.IsNull(response.Summary.Text); + } + + [TestMethod, TestCategory("Messages.GetMessageHeadersAsync")] + public async Task GetMessageHeadersAsync() + { + if (string.IsNullOrWhiteSpace(PrivateDomain) || string.IsNullOrWhiteSpace(MessageId)) + Assert.Inconclusive("Set MAILINATOR_TEST_DOMAIN_PRIVATE and MAILINATOR_TEST_MESSAGE_ID for an existing email."); + + mailinator_csharp_client.Models.Responses.GetMessageHeadersResponse response; + try + { + response = await mailinatorClient.MessagesClient.GetMessageHeadersAsync( + new GetMessageHeadersRequest { Domain = PrivateDomain, MessageId = MessageId }); + } + catch (ApiException exception) + { + // Do not include potentially sensitive response bodies in test output. + Assert.Fail($"Message headers retrieval failed with HTTP status {(int)exception.HttpStatusCode}."); + return; + } + + Assert.IsNotNull(response); + Assert.IsNotNull(response.Headers); + Assert.IsTrue(response.Headers.Count > 0, "The configured email should have SMTP headers."); + } + + [TestMethod, TestCategory("Messages.ListDomainMessagesAsync")] + public async Task ListDomainMessagesAsync() + { + if (string.IsNullOrWhiteSpace(PrivateDomain)) + Assert.Inconclusive("Set MAILINATOR_TEST_DOMAIN_PRIVATE to run the domain listing integration test."); + + var request = new ListDomainMessagesRequest { Domain = PrivateDomain, Limit = 2 }; + mailinator_csharp_client.Models.Responses.FetchInboxResponse response; + try + { + response = await mailinatorClient.MessagesClient.ListDomainMessagesAsync(request); + } + catch (ApiException exception) + { + // API error bodies can contain private message data; report only the status. + Assert.Fail($"Domain listing failed with HTTP status {(int)exception.HttpStatusCode}."); + return; + } + + Assert.IsNotNull(response); + Assert.IsTrue(response.Domain == PrivateDomain, "The response should identify the requested domain."); + Assert.IsNotNull(response.Messages); + Assert.IsTrue(response.Messages.Count <= request.Limit, "The result should respect the requested limit."); + } + [TestMethod, TestCategory("Messages.PostMessageAsync")] public async Task PostMessageAsync() { @@ -129,7 +206,7 @@ public async Task FetchMessageWithDeleteQueueParamsAsync() Thread.Sleep(45 * 1000); - var exception = await Assert.ThrowsExceptionAsync(async () => + var exception = await Assert.ThrowsExactlyAsync(async () => { await mailinatorClient.MessagesClient.FetchMessageAsync(request); }); @@ -140,7 +217,7 @@ public async Task FetchMessageWithDeleteQueueParamsAsync() public async Task FetchMessageWhenMessageDoesNotExistAsync() { var request = new FetchMessageRequest() { Domain = PrivateDomain, MessageId = DateTime.UtcNow.Ticks.ToString() }; - var exception = await Assert.ThrowsExceptionAsync(async () => + var exception = await Assert.ThrowsExactlyAsync(async () => { await mailinatorClient.MessagesClient.FetchMessageAsync(request); }); @@ -165,7 +242,7 @@ public async Task FetchInboxMessageAsync() public async Task FetchInboxMessageWhenMessageDoesNotExistAsync() { var request = new FetchInboxMessageRequest() { Domain = PrivateDomain, Inbox = PrivateInbox, MessageId = DateTime.UtcNow.Ticks.ToString() }; - var exception = await Assert.ThrowsExceptionAsync(async () => + var exception = await Assert.ThrowsExactlyAsync(async () => { await mailinatorClient.MessagesClient.FetchInboxMessageAsync(request); }); diff --git a/mailinator-csharp-client-tests/TestBase.cs b/mailinator-csharp-client-tests/TestBase.cs index 78ef528..f53eae7 100644 --- a/mailinator-csharp-client-tests/TestBase.cs +++ b/mailinator-csharp-client-tests/TestBase.cs @@ -1,4 +1,4 @@ -using mailinator_csharp_client; +using mailinator_csharp_client; using mailinator_csharp_client.Models.Domains.Entities; using mailinator_csharp_client.Models.Domains.Requests; using mailinator_csharp_client.Models.Domains.Responses; @@ -17,7 +17,6 @@ namespace mailinator_csharp_client_tests { - [TestClass] public class TestBase { protected MailinatorClient mailinatorClient; @@ -27,6 +26,7 @@ public class TestBase private const string ENV_API_TOKEN = "MAILINATOR_TEST_API_TOKEN"; private const string ENV_DOMAIN_PRIVATE = "MAILINATOR_TEST_DOMAIN_PRIVATE"; private const string ENV_INBOX = "MAILINATOR_TEST_INBOX"; + private const string ENV_MESSAGE_ID = "MAILINATOR_TEST_MESSAGE_ID"; private const string ENV_PHONE_NUMBER = "MAILINATOR_TEST_PHONE_NUMBER"; private const string ENV_MESSAGE_WITH_ATTACHMENT_ID = "MAILINATOR_TEST_MESSAGE_WITH_ATTACHMENT_ID"; private const string ENV_ATTACHMENT_ID = "MAILINATOR_TEST_ATTACHMENT_ID"; @@ -42,7 +42,7 @@ public class TestBase static TestBase() { - LoadDotEnv(); + TestEnvironment.LoadDotEnv(); ApiToken = GetEnvironmentVariable(ENV_API_TOKEN); } @@ -51,6 +51,7 @@ protected TestBase() PrivateDomain = GetEnvironmentVariable(ENV_DOMAIN_PRIVATE); DeleteDomain = GetEnvironmentVariable(ENV_DELETE_DOMAIN); PrivateInbox = GetEnvironmentVariable(ENV_INBOX); + MessageId = GetEnvironmentVariable(ENV_MESSAGE_ID); InboxAll = "*"; MessageIdWithAttachment = GetEnvironmentVariable(ENV_MESSAGE_WITH_ATTACHMENT_ID); TeamSMSNumber = GetEnvironmentVariable(ENV_PHONE_NUMBER); @@ -82,6 +83,7 @@ protected Domain Domain } protected string PrivateInbox { get; } + protected string MessageId { get; } protected string PrivateDomain { get; } protected string DeleteDomain { get; } protected string InboxAll { get; } @@ -163,51 +165,5 @@ 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-tests/TestEnvironment.cs b/mailinator-csharp-client-tests/TestEnvironment.cs new file mode 100644 index 0000000..87195f7 --- /dev/null +++ b/mailinator-csharp-client-tests/TestEnvironment.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; + +namespace mailinator_csharp_client_tests +{ + internal static class TestEnvironment + { + internal 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-tests/app.config b/mailinator-csharp-client-tests/app.config index f10b4ac..437e2f6 100644 --- a/mailinator-csharp-client-tests/app.config +++ b/mailinator-csharp-client-tests/app.config @@ -4,8 +4,8 @@ - + - \ No newline at end of file + diff --git a/mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj b/mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj index 0706128..305c93f 100644 --- a/mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj +++ b/mailinator-csharp-client-tests/mailinator-csharp-client-tests.csproj @@ -1,145 +1,24 @@ - - - - - - - + + - Debug - AnyCPU - {D59A67E5-DE4B-49AF-BD14-143091B46527} - Library - Properties + net472 mailinator_csharp_client_tests mailinator-csharp-client-tests - v4.7.2 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 15.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - + false + true + false + true + true - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Microsoft.ApplicationInsights.2.22.0\lib\net46\Microsoft.ApplicationInsights.dll - - - - ..\packages\Microsoft.Testing.Extensions.Telemetry.1.3.2\lib\netstandard2.0\Microsoft.Testing.Extensions.Telemetry.dll - - - ..\packages\Microsoft.Testing.Extensions.TrxReport.Abstractions.1.3.2\lib\netstandard2.0\Microsoft.Testing.Extensions.TrxReport.Abstractions.dll - - - ..\packages\Microsoft.Testing.Extensions.VSTestBridge.1.3.2\lib\netstandard2.0\Microsoft.Testing.Extensions.VSTestBridge.dll - - - ..\packages\Microsoft.Testing.Platform.1.3.2\lib\netstandard2.0\Microsoft.Testing.Platform.dll - - - ..\packages\Microsoft.Testing.Platform.MSBuild.1.3.2\lib\netstandard2.0\Microsoft.Testing.Platform.MSBuild.dll - - - ..\packages\Microsoft.TestPlatform.ObjectModel.17.10.0\lib\net462\Microsoft.TestPlatform.CoreUtilities.dll - - - ..\packages\Microsoft.TestPlatform.ObjectModel.17.10.0\lib\net462\Microsoft.TestPlatform.PlatformAbstractions.dll - - - ..\packages\Microsoft.TestPlatform.ObjectModel.17.10.0\lib\net462\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll - - - ..\packages\MSTest.TestFramework.3.5.2\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - - - ..\packages\MSTest.TestFramework.3.5.2\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll - - - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll - - - - - ..\packages\System.Diagnostics.DiagnosticSource.5.0.0\lib\net46\System.Diagnostics.DiagnosticSource.dll - - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll - - - - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Reflection.Metadata.1.6.0\lib\netstandard2.0\System.Reflection.Metadata.dll - - - - ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll - - - - - - - - - - - - - - + - - + + + + - - {7ab3b7cd-7633-4de4-8b8b-4a3b3924e085} - mailinator-csharp-client - + - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - - \ No newline at end of file + + diff --git a/mailinator-csharp-client-tests/packages.config b/mailinator-csharp-client-tests/packages.config deleted file mode 100644 index f8adf0b..0000000 --- a/mailinator-csharp-client-tests/packages.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mailinator-csharp-client-tests/packages.lock.json b/mailinator-csharp-client-tests/packages.lock.json new file mode 100644 index 0000000..6fc2828 --- /dev/null +++ b/mailinator-csharp-client-tests/packages.lock.json @@ -0,0 +1,228 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.7.2": { + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.9.0, )", + "resolved": "18.9.0", + "contentHash": "xIzVXa/VpKXqDWzyC/5Hw8JbFY5u9k3Jc53CpcjBiOXvkQGio484LD9FNwOiGUSMDcYL3Rcw9sNgGi5WrswlPQ==", + "dependencies": { + "Microsoft.CodeCoverage": "18.9.0" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "xZdlCr2r/6XFOo67jcemJsfm46tTgXdqtnISFIuDUAJf5Yxk07XpTC9eMUErLdlYD8QM9zw07eOPre7+nTayUg==", + "dependencies": { + "MSTest.TestFramework": "4.4.0", + "Microsoft.TestPlatform.ObjectModel": "18.9.0", + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "sfJzj3ntdU/GahJd2n+WQLgKqbV0RRMJVXtjRwRQLhblevlzLSDAOocOKANhfMaCEQ5dMt6348ztbVq/Ap60xA==", + "dependencies": { + "MSTest.Analyzers": "4.4.0" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "5.0.0" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "pUkYI1f99hY9giu/MRaEVN53RO7skXwR7FGtVvFJCK5ezdg5i0W+z1yj/91X5aKv07xGuSB5n1g/Zt2P4Ooq6g==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.9.0", + "contentHash": "MtegCIKGuG0r/LCdIjoR1HgzCGIUBhR3aNdbtQpts5vMXQuqBDZ2Jsd2uFKoHFM2XrQV6ZrDf3F5oLi3SLTp8w==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.9.0", + "contentHash": "Fz/qXC52VXXopzHj7v2reCKcCqSxkTDCkEDfkNAH0vni/7qK/KLMiEYtRmnUanxO2F2g2zZZ60qCpxF0AF7URA==", + "dependencies": { + "System.Collections.Immutable": "10.0.0", + "System.Reflection.Metadata": "8.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "8g2KL2LjsXXVywvQb0uWQBANOONVnRf6cTSbON9ar6gIw08oSBY7Z3PJlC8sHbqOVvCr7srrcWiKzb12SUoV2w==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "RestSharp": { + "type": "Transitive", + "resolved": "114.0.0", + "contentHash": "8pDO4q+K8nIqXbvGCb9ciKcngj36pCXGRDzOYPEPoS2IvBIL+mejmUMAtSNALt/NkDc9LQmHvM95kdniLSbGtg==", + "dependencies": { + "System.Text.Json": "10.0.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "BHo23kBFvTFQa0tuDFXcb3Q8QInjm1Xrq+If/xuV8iMlxOOsylsa6sgRK25n5dlcMk8G2f0O/t5AdjZrdVBOXA==", + "dependencies": { + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", + "dependencies": { + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rg8LPOZ62quw3aTnsQNh9rssncaMs69WEM1DiJdDkV+o4Llb2s5Pasy5Bulfm8zL+pE4gDcSQo7V2zW2jvxwYg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "R7H2/Oqr3onFff/JKDpfRjwxcQZlocF/eQ0ce8go/OTjqz+6LEp/fZK8j1YnDobtysChATEg7krVq4hQJ3IoeQ==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "X2rXQy7g8KEUvWVbAz6vOk6byI1eMgmzFeXbKxq4BfZCfFy5S91K+K+hd4ZBlSp0wL1Y8FyGRXs6+hvABTjwQw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.11", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.11", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.11", + "System.Threading.Tasks.Extensions": "4.6.3", + "System.ValueTuple": "4.6.2" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.2", + "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" + }, + "MailinatorApiClient": { + "type": "Project", + "dependencies": { + "Newtonsoft.Json": "[13.0.4, )", + "RestSharp": "[114.0.0, )", + "System.Text.Json": "[10.0.11, )" + } + } + } + } +} \ No newline at end of file diff --git a/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs b/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs index 9797527..dc1f874 100644 --- a/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs +++ b/mailinator-csharp-client-unit-tests/ApiClientRequestTests.cs @@ -2,13 +2,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using mailinator_csharp_client.Clients.ApiClients.Authenticators; using mailinator_csharp_client.Clients.ApiClients.Domains; using mailinator_csharp_client.Clients.ApiClients.Messages; +using mailinator_csharp_client.Clients.ApiClients.Rules; +using mailinator_csharp_client.Clients.ApiClients.Stats; +using mailinator_csharp_client.Clients.ApiClients.Webhooks; using mailinator_csharp_client.Clients.HttpClient; +using mailinator_csharp_client.Models.Authenticators.Requests; 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 mailinator_csharp_client.Models.Rules.Requests; +using mailinator_csharp_client.Models.Webhooks.Entities; +using mailinator_csharp_client.Models.Webhooks.Requests; using Microsoft.VisualStudio.TestTools.UnitTesting; using RestSharp; @@ -17,6 +25,53 @@ namespace mailinator_csharp_client_unit_tests [TestClass] public class ApiClientRequestTests { + [TestMethod] + public async Task ListDomainMessagesAsync_BuildsDomainRouteAndAllQueryParameters() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + + await client.ListDomainMessagesAsync(new ListDomainMessagesRequest + { + Domain = "example.com", Inbox = "orders*", Skip = 10, Limit = 20, + Sort = Sort.asc, DecodeSubject = true, Cursor = "next+page=", + Full = true, Delete = "30s", Wait = "10s" + }); + + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes", + "domain", "example.com", "inbox", "orders*", "skip", "10", "limit", "20", + "sort", "asc", "decode_subject", "True", "cursor", "next+page=", + "full", "True", "delete", "30s", "wait", "10s"); + Assert.AreEqual(9, httpClient.Request.Parameters.Count(p => p.Type == ParameterType.QueryString)); + Assert.AreEqual(ParameterType.QueryString, httpClient.Request.Parameters.Single(p => p.Name == "inbox").Type); + Assert.AreEqual(ParameterType.UrlSegment, httpClient.Request.Parameters.Single(p => p.Name == "domain").Type); + } + + [TestMethod] + public async Task ListDomainMessagesAsync_DefaultsToEntireDomainWithoutOptionalFilters() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + + await client.ListDomainMessagesAsync(new ListDomainMessagesRequest()); + + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes", + "domain", "private", "skip", "0", "limit", "50", "sort", "desc", "decode_subject", "False"); + CollectionAssert.AreEquivalent(new[] { "skip", "limit", "sort", "decode_subject" }, + httpClient.Request.Parameters.Where(p => p.Type == ParameterType.QueryString).Select(p => p.Name).ToArray()); + } + + [TestMethod] + public async Task ListDomainMessagesAsync_PreservesWildcardAndExplicitFalse() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + + await client.ListDomainMessagesAsync(new ListDomainMessagesRequest { Inbox = "*", Full = false }); + + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes", "inbox", "*", "full", "False"); + } + [TestMethod] public async Task GetDomainAsync_BuildsGetRequestWithDomainUrlSegment() { @@ -65,6 +120,25 @@ public async Task FetchInboxAsync_BuildsRouteAndOptionalQueryParameters() Assert.AreEqual("10s", ParameterValue(httpClient.Request, "wait")); } + [TestMethod] + public async Task FetchInboxMessageAsync_ForwardsDeleteQueryParameter() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + + await client.FetchInboxMessageAsync(new FetchInboxMessageRequest + { + Domain = "example.com", + Inbox = "orders", + MessageId = "message-123", + Delete = "30s" + }); + + Assert.AreEqual(Method.Get, httpClient.Request.Method); + Assert.AreEqual("domains/{domain}/inboxes/{inbox}/messages/{messageId}", httpClient.Request.Resource); + Assert.AreEqual("30s", ParameterValue(httpClient.Request, "delete")); + } + [TestMethod] public async Task PostMessageAsync_BuildsPostRequestWithJsonBody() { @@ -81,11 +155,181 @@ public async Task PostMessageAsync_BuildsPostRequestWithJsonBody() Assert.AreSame(message, httpClient.Request.Parameters.Single(parameter => parameter.Type == ParameterType.RequestBody).Value); } + [TestMethod] + public async Task AuthenticatorsClient_BuildsRequestsForEveryOperation() + { + var httpClient = new RecordingHttpClient(); + var client = new AuthenticatorsClient(httpClient, ""); + + await client.InstantTOTP2FACodeAsync(new InstantTOTP2FACodeRequest { TotpSecretKey = "secret" }); + AssertRequest(httpClient, Method.Get, "totp/{totp_secret_key}", "totp_secret_key", "secret"); + +#pragma warning disable CS0618 + await client.GetAuthenticatorsAsync(); + AssertRequest(httpClient, Method.Get, "authenticators"); +#pragma warning restore CS0618 + + await client.GetAuthenticatorsByIdAsync(new GetAuthenticatorsByIdRequest { Id = "auth-1" }); + AssertRequest(httpClient, Method.Get, "authenticators/{auth_id}", "auth_id", "auth-1"); + +#pragma warning disable CS0618 + await client.GetAuthenticatorAsync(); + AssertRequest(httpClient, Method.Get, "authenticator"); + await client.GetAuthenticatorByIdAsync(new GetAuthenticatorByIdRequest { Id = "auth-1" }); + AssertRequest(httpClient, Method.Get, "authenticator/{auth_id}", "auth_id", "auth-1"); +#pragma warning restore CS0618 + } + + [TestMethod] + public async Task DomainsClient_BuildsRequestsForEveryOperation() + { + var httpClient = new RecordingHttpClient(); + var client = new DomainsClient(httpClient, "domains"); + + await client.GetAllDomainsAsync(); + AssertRequest(httpClient, Method.Get, "domains/"); + +#pragma warning disable CS0618 + await client.CreateDomainAsync(new CreateDomainRequest { Name = "example.com" }); + AssertRequest(httpClient, Method.Post, "domains/{domain_id}", "domain_id", "example.com"); + await client.DeleteDomainAsync(new DeleteDomainRequest { DomainId = "example.com" }); + AssertRequest(httpClient, Method.Delete, "domains/{domain_id}", "domain_id", "example.com"); +#pragma warning restore CS0618 + } + + [TestMethod] + public async Task MessagesClient_BuildsRequestsForMessageRetrievalOperations() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + + await client.FetchMessageAsync(new FetchMessageRequest { Domain = "example.com", MessageId = "message-1", Delete = "1m" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}", "domain", "example.com", "messageId", "message-1", "delete", "1m"); + await client.FetchSMSMessagesAsync(new FetchSMSMessagesRequest { Domain = "example.com", TeamSMSNumber = "15551234567" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{teamSMSNumber}", "domain", "example.com", "teamSMSNumber", "15551234567"); + await client.FetchInboxMessageAttachmentsAsync(new FetchInboxMessageAttachmentsRequest { Domain = "example.com", Inbox = "orders", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{inbox}/messages/{messageId}/attachments", "domain", "example.com", "inbox", "orders", "messageId", "message-1"); + await client.FetchMessageAttachmentsAsync(new FetchMessageAttachmentsRequest { Domain = "example.com", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}/attachments", "domain", "example.com", "messageId", "message-1"); + await client.FetchInboxMessageAttachmentAsync(new FetchInboxMessageAttachmentRequest { Domain = "example.com", Inbox = "orders", MessageId = "message-1", AttachmentId = "file.pdf" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{inbox}/messages/{messageId}/attachments/{attachmentId}", "domain", "example.com", "inbox", "orders", "messageId", "message-1", "attachmentId", "file.pdf"); + await client.FetchMessageAttachmentAsync(new FetchMessageAttachmentRequest { Domain = "example.com", MessageId = "message-1", AttachmentId = "file.pdf" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}/attachments/{attachmentId}", "domain", "example.com", "messageId", "message-1", "attachmentId", "file.pdf"); + await client.FetchMessageLinksAsync(new FetchMessageLinksRequest { Domain = "example.com", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}/links", "domain", "example.com", "messageId", "message-1"); + await client.FetchInboxMessageLinksAsync(new FetchInboxMessageLinksRequest { Domain = "example.com", Inbox = "orders", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{inbox}/messages/{messageId}/links", "domain", "example.com", "inbox", "orders", "messageId", "message-1"); + await client.FetchMessageLinksFullAsync(new FetchMessageLinksFullRequest { Domain = "example.com", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}/linksfull", "domain", "example.com", "messageId", "message-1"); + } + + [TestMethod] + public async Task MessagesClient_BuildsRequestsForDeletionAndContentOperations() + { + var httpClient = new RecordingHttpClient(); + var client = new MessagesClient(httpClient, "domains"); + + await client.DeleteAllDomainMessagesAsync(new DeleteAllDomainMessagesRequest { Domain = "example.com" }); + AssertRequest(httpClient, Method.Delete, "domains/{domain}/inboxes", "domain", "example.com"); + await client.DeleteAllInboxMessagesAsync(new DeleteAllInboxMessagesRequest { Domain = "example.com", Inbox = "orders" }); + AssertRequest(httpClient, Method.Delete, "domains/{domain}/inboxes/{inbox}", "domain", "example.com", "inbox", "orders"); + await client.DeleteMessageAsync(new DeleteMessageRequest { Domain = "example.com", Inbox = "orders", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Delete, "domains/{domain}/inboxes/{inbox}/messages/{messageId}", "domain", "example.com", "inbox", "orders", "messageId", "message-1"); + await client.FetchMessageSmtpLogAsync(new FetchMessageSmtpLogRequest { Domain = "example.com", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}/smtplog", "domain", "example.com", "messageId", "message-1"); + await client.FetchInboxMessageSmtpLogAsync(new FetchInboxMessageSmtpLogRequest { Domain = "example.com", Inbox = "orders", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{inbox}/messages/{messageId}/smtplog", "domain", "example.com", "inbox", "orders", "messageId", "message-1"); + await client.FetchMessageRawAsync(new FetchMessageRawRequest { Domain = "example.com", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/{messageId}/raw", "domain", "example.com", "messageId", "message-1"); + await client.FetchInboxMessageRawAsync(new FetchInboxMessageRawRequest { Domain = "example.com", Inbox = "orders", MessageId = "message-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{inbox}/messages/{messageId}/raw", "domain", "example.com", "inbox", "orders", "messageId", "message-1"); + +#pragma warning disable CS0618 + await client.FetchLatestMessagesAsync(new FetchLatestMessagesRequest { Domain = "example.com" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/messages/*", "domain", "example.com"); + await client.FetchLatestInboxMessagesAsync(new FetchLatestInboxMessagesRequest { Domain = "example.com", Inbox = "orders" }); + AssertRequest(httpClient, Method.Get, "domains/{domain}/inboxes/{inbox}/messages/*", "domain", "example.com", "inbox", "orders"); +#pragma warning restore CS0618 + } + + [TestMethod] + public async Task RulesClient_BuildsRequestsForEveryOperation() + { + var httpClient = new RecordingHttpClient(); + var client = new RulesClient(httpClient, "domains"); + var createRequest = new CreateRuleRequest { DomainId = "example.com" }; + +#pragma warning disable CS0618 + await client.CreateRuleAsync(createRequest); + AssertRequest(httpClient, Method.Post, "domains/{domain_id}/rules", "domain_id", "example.com"); + Assert.AreSame(createRequest.Rule, BodyValue(httpClient.Request)); + await client.EnableRuleAsync(new EnableRuleRequest { DomainId = "example.com", RuleId = "rule-1" }); + AssertRequest(httpClient, Method.Put, "domains/{domain_id}/rules/{ruleId}/enable", "domain_id", "example.com", "ruleId", "rule-1"); + await client.DisableRuleAsync(new DisableRuleRequest { DomainId = "example.com", RuleId = "rule-1" }); + AssertRequest(httpClient, Method.Put, "domains/{domain_id}/rules/{ruleId}/disable", "domain_id", "example.com", "ruleId", "rule-1"); + await client.GetAllRulesAsync(new GetAllRulesRequest { DomainId = "example.com" }); + AssertRequest(httpClient, Method.Get, "domains/{domain_id}/rules", "domain_id", "example.com"); + await client.GetRuleAsync(new GetRuleRequest { DomainId = "example.com", RuleId = "rule-1" }); + AssertRequest(httpClient, Method.Get, "domains/{domain_id}/rules/{ruleId}", "domain_id", "example.com", "ruleId", "rule-1"); + await client.DeleteRuleAsync(new DeleteRuleRequest { DomainId = "example.com", RuleId = "rule-1" }); + AssertRequest(httpClient, Method.Delete, "domains/{domain_id}/rules/{ruleId}", "domain_id", "example.com", "ruleId", "rule-1"); +#pragma warning restore CS0618 + } + + [TestMethod] + public async Task StatsClient_BuildsRequestsForEveryOperation() + { + var httpClient = new RecordingHttpClient(); + var client = new StatsClient(httpClient, "stats"); + + await client.GetTeamStatsAsync(); + AssertRequest(httpClient, Method.Get, "statsteam/stats"); + await client.GetTeamAsync(); + AssertRequest(httpClient, Method.Get, "stats/team"); + await client.GetTeamInfoAsync(); + AssertRequest(httpClient, Method.Get, "stats/teaminfo"); + } + + [TestMethod] + public async Task WebhooksClient_BuildsRequestsForEveryOperation() + { + var httpClient = new RecordingHttpClient(); + var client = new WebhooksClient(httpClient, "webhooks"); + var webhook = new Webhook { From = "sender@example.com", Subject = "Hello" }; + + await client.PrivateWebhookAsync(new PrivateWebhookRequest { WebhookToken = "token", Webhook = webhook }); + AssertRequest(httpClient, Method.Post, "webhooks/private/webhook", "whtoken", "token"); + Assert.AreSame(webhook, BodyValue(httpClient.Request)); + await client.PrivateInboxWebhookAsync(new PrivateInboxWebhookRequest { WebhookToken = "token", Inbox = "orders", Webhook = webhook }); + AssertRequest(httpClient, Method.Post, "webhooks/private/webhook/{inbox}", "whtoken", "token", "inbox", "orders"); + await client.PrivateCustomServiceWebhookAsync(new PrivateCustomServiceWebhookRequest { WebhookToken = "token", CustomService = "twilio", Webhook = webhook }); + AssertRequest(httpClient, Method.Post, "webhooks/private/{customService}", "whtoken", "token", "customService", "twilio"); + await client.PrivateCustomServiceInboxWebhookAsync(new PrivateCustomServiceInboxWebhookRequest { WebhookToken = "token", CustomService = "twilio", Inbox = "orders", Webhook = webhook }); + AssertRequest(httpClient, Method.Post, "webhooks/private/{customService}/{inbox}", "whtoken", "token", "customService", "twilio", "inbox", "orders"); + } + private static object ParameterValue(RestRequest request, string name) { return request.Parameters.Single(parameter => parameter.Name == name).Value; } + private static object BodyValue(RestRequest request) + { + return request.Parameters.Single(parameter => parameter.Type == ParameterType.RequestBody).Value; + } + + private static void AssertRequest(RecordingHttpClient httpClient, Method method, string resource, params string[] parameters) + { + Assert.AreEqual(method, httpClient.Request.Method); + Assert.AreEqual(resource, httpClient.Request.Resource); + Assert.AreEqual(0, parameters.Length % 2, "Expected parameter names and values in pairs."); + + for (var index = 0; index < parameters.Length; index += 2) + { + Assert.AreEqual(parameters[index + 1], ParameterValue(httpClient.Request, parameters[index])?.ToString()); + } + } + private sealed class RecordingHttpClient : IHttpClient { public RestRequest Request { get; private set; } diff --git a/mailinator-csharp-client-unit-tests/MessageContentTests.cs b/mailinator-csharp-client-unit-tests/MessageContentTests.cs new file mode 100644 index 0000000..c3e908e --- /dev/null +++ b/mailinator-csharp-client-unit-tests/MessageContentTests.cs @@ -0,0 +1,129 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using mailinator_csharp_client.Clients.ApiClients.Messages; +using mailinator_csharp_client.Clients.HttpClient; +using mailinator_csharp_client.Models.Messages.Requests; +using mailinator_csharp_client.Models.Responses; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RestSharp; + +namespace mailinator_csharp_client_unit_tests +{ + [TestClass] + public class MessageContentTests + { + [TestMethod] + public async Task GetMessageTextAsync_BuildsGetRequestAndReturnsResponse() + { + var expected = new GetMessageTextResponse { Text = "content" }; + var http = new ContentHttpClient(expected); + var client = new MessagesClient(http, "domains"); + + var response = await client.GetMessageTextAsync(new GetMessageTextRequest + { + Domain = "example.com", MessageId = "message+123/part" + }); + + Assert.AreEqual(Method.Get, http.Request.Method); + Assert.AreEqual("domains/{domain}/messages/{messageId}/text", http.Request.Resource); + Assert.AreEqual(2, http.Request.Parameters.Count()); + Assert.IsTrue(http.Request.Parameters.All(p => p.Type == ParameterType.UrlSegment)); + Assert.AreEqual("example.com", http.Request.Parameters.Single(p => p.Name == "domain").Value); + Assert.AreEqual("message+123/part", http.Request.Parameters.Single(p => p.Name == "messageId").Value); + Assert.AreSame(expected, response); + } + + [TestMethod] + [DataRow("")] + [DataRow("Hello\r\nWorld =C2=A0 café

HTML & text

")] + public void GetMessageTextResponse_DeserializesContentWithoutTransformingIt(string content) + { + var json = new JObject { ["text"] = content }.ToString(); + var response = JsonConvert.DeserializeObject(json); + + Assert.AreEqual(content, response.Text); + } + + [TestMethod] + public async Task GetMessageTextPlainAsync_BuildsGetRequestAndReturnsResponse() + { + var expected = new GetMessageTextPlainResponse { TextPlain = "content" }; + var http = new ContentHttpClient(expected); + var client = new MessagesClient(http, "domains"); + + var response = await client.GetMessageTextPlainAsync(new GetMessageTextPlainRequest + { + Domain = "example.com", MessageId = "message+123/part" + }); + + Assert.AreEqual(Method.Get, http.Request.Method); + Assert.AreEqual("domains/{domain}/messages/{messageId}/textplain", http.Request.Resource); + Assert.AreEqual(2, http.Request.Parameters.Count()); + Assert.IsTrue(http.Request.Parameters.All(p => p.Type == ParameterType.UrlSegment)); + Assert.AreEqual("example.com", http.Request.Parameters.Single(p => p.Name == "domain").Value); + Assert.AreEqual("message+123/part", http.Request.Parameters.Single(p => p.Name == "messageId").Value); + Assert.AreSame(expected, response); + } + + [TestMethod] + [DataRow("")] + [DataRow("Hello\r\nWorld =C2=A0 café

HTML & text

")] + public void GetMessageTextPlainResponse_DeserializesContentWithoutTransformingIt(string content) + { + var json = new JObject { ["text/plain"] = content }.ToString(); + var response = JsonConvert.DeserializeObject(json); + + Assert.AreEqual(content, response.TextPlain); + } + + [TestMethod] + public async Task GetMessageTextHtmlAsync_BuildsGetRequestAndReturnsResponse() + { + var expected = new GetMessageTextHtmlResponse { TextHtml = "content" }; + var http = new ContentHttpClient(expected); + var client = new MessagesClient(http, "domains"); + + var response = await client.GetMessageTextHtmlAsync(new GetMessageTextHtmlRequest + { + Domain = "example.com", MessageId = "message+123/part" + }); + + Assert.AreEqual(Method.Get, http.Request.Method); + Assert.AreEqual("domains/{domain}/messages/{messageId}/texthtml", http.Request.Resource); + Assert.AreEqual(2, http.Request.Parameters.Count()); + Assert.IsTrue(http.Request.Parameters.All(p => p.Type == ParameterType.UrlSegment)); + Assert.AreEqual("example.com", http.Request.Parameters.Single(p => p.Name == "domain").Value); + Assert.AreEqual("message+123/part", http.Request.Parameters.Single(p => p.Name == "messageId").Value); + Assert.AreSame(expected, response); + } + + [TestMethod] + [DataRow("")] + [DataRow("Hello\r\nWorld =C2=A0 café

HTML & text

")] + public void GetMessageTextHtmlResponse_DeserializesContentWithoutTransformingIt(string content) + { + var json = new JObject { ["text/html"] = content }.ToString(); + var response = JsonConvert.DeserializeObject(json); + + Assert.AreEqual(content, response.TextHtml); + } + + private sealed class ContentHttpClient : IHttpClient + { + private readonly object response; + public RestRequest Request { get; private set; } + public ContentHttpClient(object response) => this.response = response; + public RestRequest GetRequest(string url, Method method) => new RestRequest(url, method); + public Task ExecuteAsync(RestRequest request) + { + Request = request; + return Task.FromResult((T)response); + } + public Task ExecuteAsync(RestRequest request, Func customDeserializationFunction) + => throw new NotSupportedException(); + } + } +} diff --git a/mailinator-csharp-client-unit-tests/MessageHeadersTests.cs b/mailinator-csharp-client-unit-tests/MessageHeadersTests.cs new file mode 100644 index 0000000..a7e4834 --- /dev/null +++ b/mailinator-csharp-client-unit-tests/MessageHeadersTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using mailinator_csharp_client.Clients.ApiClients.Messages; +using mailinator_csharp_client.Clients.HttpClient; +using mailinator_csharp_client.Models.Messages.Requests; +using mailinator_csharp_client.Models.Responses; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RestSharp; + +namespace mailinator_csharp_client_unit_tests +{ + [TestClass] + public class MessageHeadersTests + { + [TestMethod] + public async Task GetMessageHeadersAsync_BuildsGetRequestAndReturnsResponse() + { + var http = new HeadersHttpClient(); + var client = new MessagesClient(http, "domains"); + + var response = await client.GetMessageHeadersAsync(new GetMessageHeadersRequest + { + Domain = "example.com", MessageId = "message+123" + }); + + Assert.AreEqual(Method.Get, http.Request.Method); + Assert.AreEqual("domains/{domain}/messages/{messageId}/headers", http.Request.Resource); + Assert.AreEqual(2, http.Request.Parameters.Count()); + Assert.IsTrue(http.Request.Parameters.All(p => p.Type == ParameterType.UrlSegment)); + Assert.AreEqual("example.com", http.Request.Parameters.Single(p => p.Name == "domain").Value); + Assert.AreEqual("message+123", http.Request.Parameters.Single(p => p.Name == "messageId").Value); + Assert.AreSame(http.Response, response); + } + + [TestMethod] + public void GetMessageHeadersResponse_DeserializesStringsArraysAndCustomHeaders() + { + var response = JsonConvert.DeserializeObject( + "{\"headers\":{\"subject\":\"Test\",\"received\":[\"hop one\",\"hop two\"],\"x-custom\":\"custom value\"}}"); + + Assert.AreEqual("Test", response.Headers["subject"]); + CollectionAssert.AreEqual(new[] { "hop one", "hop two" }, ((JArray)response.Headers["received"]).ToObject()); + Assert.AreEqual("custom value", response.Headers["x-custom"]); + } + + [TestMethod] + public void GetMessageHeadersResponse_DeserializesEmptyHeaders() + { + var response = JsonConvert.DeserializeObject("{\"headers\":{}}"); + Assert.IsNotNull(response.Headers); + Assert.AreEqual(0, response.Headers.Count); + } + + private sealed class HeadersHttpClient : IHttpClient + { + public RestRequest Request { get; private set; } + public GetMessageHeadersResponse Response { get; } = new GetMessageHeadersResponse(); + public RestRequest GetRequest(string url, Method method) => new RestRequest(url, method); + public Task ExecuteAsync(RestRequest request) + { + Request = request; + return Task.FromResult((T)(object)Response); + } + public Task ExecuteAsync(RestRequest request, Func customDeserializationFunction) + => throw new NotSupportedException(); + } + } +} diff --git a/mailinator-csharp-client-unit-tests/MessageSummaryTests.cs b/mailinator-csharp-client-unit-tests/MessageSummaryTests.cs new file mode 100644 index 0000000..7c391e8 --- /dev/null +++ b/mailinator-csharp-client-unit-tests/MessageSummaryTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using mailinator_csharp_client.Clients.ApiClients.Messages; +using mailinator_csharp_client.Clients.HttpClient; +using mailinator_csharp_client.Models.Messages.Requests; +using mailinator_csharp_client.Models.Responses; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using RestSharp; + +namespace mailinator_csharp_client_unit_tests +{ + [TestClass] + public class MessageSummaryTests + { + [TestMethod] + public async Task GetMessageSummaryAsync_BuildsGetRequestAndReturnsResponse() + { + var http = new SummaryHttpClient(); + var client = new MessagesClient(http, "domains"); + + var response = await client.GetMessageSummaryAsync(new GetMessageSummaryRequest + { + Domain = "example.com", MessageId = "message+123" + }); + + Assert.AreEqual(Method.Get, http.Request.Method); + Assert.AreEqual("domains/{domain}/messages/{messageId}/summary", http.Request.Resource); + Assert.AreEqual(2, http.Request.Parameters.Count()); + Assert.IsTrue(http.Request.Parameters.All(p => p.Type == ParameterType.UrlSegment)); + Assert.AreEqual("example.com", http.Request.Parameters.Single(p => p.Name == "domain").Value); + Assert.AreEqual("message+123", http.Request.Parameters.Single(p => p.Name == "messageId").Value); + Assert.AreSame(http.Response, response); + } + + [TestMethod] + public void GetMessageSummaryResponse_DeserializesWrappedMetadataAnd64BitTime() + { + var response = JsonConvert.DeserializeObject( + "{\"summary\":{\"subject\":\"Test\",\"domain\":\"example.com\",\"from\":\"sender@example.com\",\"id\":\"message-123\",\"to\":\"orders\",\"time\":1788909009000}}"); + + Assert.IsNotNull(response.Summary); + Assert.AreEqual("Test", response.Summary.Subject); + Assert.AreEqual("example.com", response.Summary.Domain); + Assert.AreEqual("sender@example.com", response.Summary.From); + Assert.AreEqual("message-123", response.Summary.Id); + Assert.AreEqual("orders", response.Summary.To); + Assert.AreEqual(1788909009000L, response.Summary.Time); + Assert.IsNull(response.Summary.Parts); + Assert.IsNull(response.Summary.Text); + } + private sealed class SummaryHttpClient : IHttpClient + { + public RestRequest Request { get; private set; } + public GetMessageSummaryResponse Response { get; } = new GetMessageSummaryResponse(); + public RestRequest GetRequest(string url, Method method) => new RestRequest(url, method); + public Task ExecuteAsync(RestRequest request) + { + Request = request; + return Task.FromResult((T)(object)Response); + } + public Task ExecuteAsync(RestRequest request, Func customDeserializationFunction) + => throw new NotSupportedException(); + } + } +} + diff --git a/mailinator-csharp-client-unit-tests/WebhookMessageTests.cs b/mailinator-csharp-client-unit-tests/WebhookMessageTests.cs new file mode 100644 index 0000000..302ea58 --- /dev/null +++ b/mailinator-csharp-client-unit-tests/WebhookMessageTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using mailinator_csharp_client.Clients.ApiClients.Webhooks; +using mailinator_csharp_client.Clients.HttpClient; +using mailinator_csharp_client.Models.Webhooks.Entities; +using mailinator_csharp_client.Models.Webhooks.Requests; +using mailinator_csharp_client.Models.Webhooks.Responses; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RestSharp; + +namespace mailinator_csharp_client_unit_tests +{ + [TestClass] + public class WebhookMessageTests + { + [TestMethod] + [DataRow("example.com", "test-token+123")] + [DataRow("test-token+123", null)] + public async Task PostWebhookMessageAsync_BuildsRequestForBothAuthenticationForms(string domain, string token) + { + var http = new RecordingHttpClient(); + var client = new WebhooksClient(http, "domains"); + var body = new WebhookMessage { To = "orders", Text = "Hello" }; + var response = await client.PostWebhookMessageAsync(new PostWebhookMessageRequest + { + Domain = domain, WebhookToken = token, Webhook = body + }); + Assert.AreEqual(Method.Post, http.Request.Method); + Assert.AreEqual("domains/{domain}/webhook", http.Request.Resource); + Assert.AreEqual(domain, http.Request.Parameters.Single(p => p.Name == "domain" && p.Type == ParameterType.UrlSegment).Value); + + var query = http.Request.Parameters.Where(p => p.Type == ParameterType.QueryString).ToArray(); + Assert.AreEqual(token == null ? 0 : 1, query.Length); + if (token != null) + { + Assert.AreEqual("whtoken", query[0].Name); + Assert.AreEqual(token, query[0].Value); + } + Assert.AreSame(body, http.Request.Parameters.Single(p => p.Type == ParameterType.RequestBody).Value); + Assert.IsFalse(http.Request.Parameters.Any(p => p.Name == "token")); + Assert.AreSame(http.Response, response); + } + + [TestMethod] + [DataRow("example.com", "test-token+123")] + [DataRow("test-token+123", null)] + public async Task PostWebhookInboxMessageAsync_BuildsRequestForBothAuthenticationForms(string domain, string token) + { + var http = new RecordingHttpClient(); + var client = new WebhooksClient(http, "domains"); + var body = new WebhookMessage { To = "orders", Text = "Hello" }; + var response = await client.PostWebhookInboxMessageAsync(new PostWebhookInboxMessageRequest + { + Domain = domain, WebhookToken = token, Webhook = body, Inbox = "orders+test" + }); + Assert.AreEqual(Method.Post, http.Request.Method); + Assert.AreEqual("domains/{domain}/webhook/{inbox}", http.Request.Resource); + Assert.AreEqual(domain, http.Request.Parameters.Single(p => p.Name == "domain" && p.Type == ParameterType.UrlSegment).Value); + Assert.AreEqual("orders+test", http.Request.Parameters.Single(p => p.Name == "inbox" && p.Type == ParameterType.UrlSegment).Value); + var query = http.Request.Parameters.Where(p => p.Type == ParameterType.QueryString).ToArray(); + Assert.AreEqual(token == null ? 0 : 1, query.Length); + if (token != null) + { + Assert.AreEqual("whtoken", query[0].Name); + Assert.AreEqual(token, query[0].Value); + } + Assert.AreSame(body, http.Request.Parameters.Single(p => p.Type == ParameterType.RequestBody).Value); + Assert.IsFalse(http.Request.Parameters.Any(p => p.Name == "token")); + Assert.AreSame(http.Response, response); + } + + [TestMethod] + public void WebhookMessage_PreservesDocumentedAndCustomPayloadFields() + { + var body = JsonConvert.DeserializeObject( + "{\"from\":\"sender\",\"to\":\"orders\",\"subject\":\"Test\",\"text\":\"plain\",\"html\":\"

HTML

\",\"headers\":{\"X-Test\":\"value\"},\"custom\":{\"count\":2}}"); + Assert.AreEqual("

HTML

", body.Html); + Assert.AreEqual("value", body.Headers["X-Test"]); + var json = JObject.Parse(JsonConvert.SerializeObject(body)); + Assert.AreEqual("sender", (string)json["from"]); + Assert.AreEqual("orders", (string)json["to"]); + Assert.AreEqual("Test", (string)json["subject"]); + Assert.AreEqual("plain", (string)json["text"]); + Assert.AreEqual(2, (int)json["custom"]["count"]); + } + + [TestMethod] + public void PostWebhookMessageResponse_DeserializesStatusAndId() + { + var response = JsonConvert.DeserializeObject("{\"status\":\"ok\",\"id\":\"message-123\"}"); + Assert.AreEqual("ok", response.Status); + Assert.AreEqual("message-123", response.Id); + } + + private sealed class RecordingHttpClient : IHttpClient + { + public RestRequest Request { get; private set; } + public PostWebhookMessageResponse Response { get; } = new PostWebhookMessageResponse(); + public RestRequest GetRequest(string url, Method method) => new RestRequest(url, method); + public Task ExecuteAsync(RestRequest request) + { + Request = request; + return Task.FromResult((T)(object)Response); + } + public Task ExecuteAsync(RestRequest request, Func customDeserializationFunction) + => throw new NotSupportedException(); + } + } +} 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 index 2b84370..246d6b2 100644 --- a/mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj +++ b/mailinator-csharp-client-unit-tests/mailinator-csharp-client-unit-tests.csproj @@ -5,9 +5,9 @@ true - - - + + + diff --git a/mailinator-csharp-client-unit-tests/packages.lock.json b/mailinator-csharp-client-unit-tests/packages.lock.json new file mode 100644 index 0000000..9b3780c --- /dev/null +++ b/mailinator-csharp-client-unit-tests/packages.lock.json @@ -0,0 +1,142 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.9.0, )", + "resolved": "18.9.0", + "contentHash": "xIzVXa/VpKXqDWzyC/5Hw8JbFY5u9k3Jc53CpcjBiOXvkQGio484LD9FNwOiGUSMDcYL3Rcw9sNgGi5WrswlPQ==", + "dependencies": { + "Microsoft.CodeCoverage": "18.9.0", + "Microsoft.TestPlatform.TestHost": "18.9.0" + } + }, + "MSTest.TestAdapter": { + "type": "Direct", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "xZdlCr2r/6XFOo67jcemJsfm46tTgXdqtnISFIuDUAJf5Yxk07XpTC9eMUErLdlYD8QM9zw07eOPre7+nTayUg==", + "dependencies": { + "MSTest.TestFramework": "4.4.0", + "Microsoft.TestPlatform.ObjectModel": "18.9.0", + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0" + } + }, + "MSTest.TestFramework": { + "type": "Direct", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "sfJzj3ntdU/GahJd2n+WQLgKqbV0RRMJVXtjRwRQLhblevlzLSDAOocOKANhfMaCEQ5dMt6348ztbVq/Ap60xA==", + "dependencies": { + "MSTest.Analyzers": "4.4.0" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "5.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.9.0", + "contentHash": "MtegCIKGuG0r/LCdIjoR1HgzCGIUBhR3aNdbtQpts5vMXQuqBDZ2Jsd2uFKoHFM2XrQV6ZrDf3F5oLi3SLTp8w==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.9.0", + "contentHash": "Fz/qXC52VXXopzHj7v2reCKcCqSxkTDCkEDfkNAH0vni/7qK/KLMiEYtRmnUanxO2F2g2zZZ60qCpxF0AF7URA==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.9.0", + "contentHash": "Oq+Pma/J86aZL72t71bcESvDszDsTjUfl6PIsuDdPoKTHZfNMhKamCfLD5fKx51W/u0KozXtJo2/3fnt0sgPxg==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.9.0" + } + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "8g2KL2LjsXXVywvQb0uWQBANOONVnRf6cTSbON9ar6gIw08oSBY7Z3PJlC8sHbqOVvCr7srrcWiKzb12SUoV2w==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "RestSharp": { + "type": "Transitive", + "resolved": "114.0.0", + "contentHash": "8pDO4q+K8nIqXbvGCb9ciKcngj36pCXGRDzOYPEPoS2IvBIL+mejmUMAtSNALt/NkDc9LQmHvM95kdniLSbGtg==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rg8LPOZ62quw3aTnsQNh9rssncaMs69WEM1DiJdDkV+o4Llb2s5Pasy5Bulfm8zL+pE4gDcSQo7V2zW2jvxwYg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "R7H2/Oqr3onFff/JKDpfRjwxcQZlocF/eQ0ce8go/OTjqz+6LEp/fZK8j1YnDobtysChATEg7krVq4hQJ3IoeQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "X2rXQy7g8KEUvWVbAz6vOk6byI1eMgmzFeXbKxq4BfZCfFy5S91K+K+hd4ZBlSp0wL1Y8FyGRXs6+hvABTjwQw==", + "dependencies": { + "System.IO.Pipelines": "10.0.11", + "System.Text.Encodings.Web": "10.0.11" + } + }, + "MailinatorApiClient": { + "type": "Project", + "dependencies": { + "Newtonsoft.Json": "[13.0.4, )", + "RestSharp": "[114.0.0, )", + "System.Text.Json": "[10.0.11, )" + } + } + } + } +} \ No newline at end of file diff --git a/mailinator-csharp-client/Clients/ApiClients/Authenticators/AuthenticatorsClient.cs b/mailinator-csharp-client/Clients/ApiClients/Authenticators/AuthenticatorsClient.cs index b12427e..e991949 100644 --- a/mailinator-csharp-client/Clients/ApiClients/Authenticators/AuthenticatorsClient.cs +++ b/mailinator-csharp-client/Clients/ApiClients/Authenticators/AuthenticatorsClient.cs @@ -41,6 +41,7 @@ public async Task InstantTOTP2FACodeAsync(InstantTOT /// Fetch Authenticators /// /// + [System.Obsolete("Deprecated: This authenticator list endpoint is not present in the current Mailinator OpenAPI spec. Use GetAuthenticatorsByIdAsync for the documented stored-authenticator operation. This method may be removed in a future major release.")] public async Task GetAuthenticatorsAsync() { var requestObject = httpClient.GetRequest(endpointUrl + "authenticators", Method.Get); @@ -69,6 +70,7 @@ public async Task GetAuthenticatorsByIdAsync(GetA /// Fetch Authenticator /// /// + [System.Obsolete("Deprecated: This authenticator endpoint is not present in the current Mailinator OpenAPI spec. Use GetAuthenticatorsByIdAsync for the documented stored-authenticator operation. This method may be removed in a future major release.")] public async Task GetAuthenticatorAsync() { var requestObject = httpClient.GetRequest(endpointUrl + "authenticator", Method.Get); @@ -82,6 +84,7 @@ public async Task GetAuthenticatorAsync() /// /// GetAuthenticatorByIdRequest object. /// + [System.Obsolete("Deprecated: This authenticator endpoint is not present in the current Mailinator OpenAPI spec. Use GetAuthenticatorsByIdAsync for the documented stored-authenticator operation. This method may be removed in a future major release.")] public async Task GetAuthenticatorByIdAsync(GetAuthenticatorByIdRequest request) { var requestObject = httpClient.GetRequest(endpointUrl + "authenticator/{auth_id}", Method.Get); diff --git a/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs b/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs index 81c2453..75f9cdd 100644 --- a/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs +++ b/mailinator-csharp-client/Clients/ApiClients/Messages/MessagesClient.cs @@ -23,6 +23,104 @@ public MessagesClient(IHttpClient httpClient, string endpointUrl) this.endpointUrl = endpointUrl; } + /// + /// Retrieves text content for a message in a domain. + /// + /// The domain and message ID. + /// Extracted message text, preserving any quoted-printable artifacts returned by the API. + public async Task GetMessageTextAsync(GetMessageTextRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/messages/{messageId}/text", Method.Get); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddUrlSegment("messageId", request.MessageId); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + + /// + /// Retrieves textplain content for a message in a domain. + /// + /// The domain and message ID. + /// The text/plain message body. + public async Task GetMessageTextPlainAsync(GetMessageTextPlainRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/messages/{messageId}/textplain", Method.Get); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddUrlSegment("messageId", request.MessageId); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + + /// + /// Retrieves texthtml content for a message in a domain. + /// + /// The domain and message ID. + /// The text/html message body, preserving HTML markup. + public async Task GetMessageTextHtmlAsync(GetMessageTextHtmlRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/messages/{messageId}/texthtml", Method.Get); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddUrlSegment("messageId", request.MessageId); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + + /// + /// Retrieves message metadata without body content. + /// + /// The domain and message ID. + /// The message summary. + public async Task GetMessageSummaryAsync(GetMessageSummaryRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/messages/{messageId}/summary", Method.Get); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddUrlSegment("messageId", request.MessageId); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + + /// + /// Retrieves SMTP headers for a message in a domain. + /// + /// The domain and message ID. + /// The message's headers. + public async Task GetMessageHeadersAsync(GetMessageHeadersRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/messages/{messageId}/headers", Method.Get); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddUrlSegment("messageId", request.MessageId); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + + /// + /// Retrieves message summaries across a domain, optionally filtered by inbox. + /// + /// Domain, optional inbox filter, and listing options. + /// Message summaries (or full messages when requested) and a pagination cursor. + public async Task ListDomainMessagesAsync(ListDomainMessagesRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/inboxes", Method.Get); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddSafeQueryParameter("inbox", request.Inbox); + requestObject.AddSafeQueryParameter("skip", request.Skip.ToString()); + requestObject.AddSafeQueryParameter("limit", request.Limit.ToString()); + requestObject.AddSafeQueryParameter("sort", request.Sort.ToString()); + requestObject.AddSafeQueryParameter("decode_subject", request.DecodeSubject.ToString()); + requestObject.AddSafeQueryParameter("cursor", request.Cursor); + requestObject.AddSafeQueryParameter("full", request.Full?.ToString()); + requestObject.AddSafeQueryParameter("delete", request.Delete); + requestObject.AddSafeQueryParameter("wait", request.Wait); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + /// /// This endpoint retrieves a list of messages summaries. You can retreive a list by inbox, inboxes, or entire domain. /// :domain @@ -66,6 +164,7 @@ public async Task FetchInboxMessageAsync(FetchInboxMe requestObject.AddUrlSegment("domain", request.Domain); requestObject.AddUrlSegment("inbox", request.Inbox); requestObject.AddUrlSegment("messageId", request.MessageId); + requestObject.AddSafeQueryParameter("delete", request.Delete?.ToString()); var response = await httpClient.ExecuteAsync(requestObject); return response; diff --git a/mailinator-csharp-client/Clients/ApiClients/Webhooks/WebhooksClient.cs b/mailinator-csharp-client/Clients/ApiClients/Webhooks/WebhooksClient.cs index 2f88c31..58baa0b 100644 --- a/mailinator-csharp-client/Clients/ApiClients/Webhooks/WebhooksClient.cs +++ b/mailinator-csharp-client/Clients/ApiClients/Webhooks/WebhooksClient.cs @@ -25,6 +25,31 @@ public WebhooksClient(IHttpClient httpClient, string endpointUrl) this.endpointUrl = endpointUrl; } + /// Injects a message using a webhook token in the domain segment or whtoken query parameter. + public async Task PostWebhookMessageAsync(PostWebhookMessageRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/webhook", Method.Post); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddSafeQueryParameter("whtoken", request.WebhookToken); + requestObject.AddJsonBody(request.Webhook); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + + /// Injects a message using a webhook token in the domain segment or whtoken query parameter. + public async Task PostWebhookInboxMessageAsync(PostWebhookInboxMessageRequest request) + { + var requestObject = httpClient.GetRequest(endpointUrl + "/{domain}/webhook/{inbox}", Method.Post); + requestObject.AddUrlSegment("domain", request.Domain); + requestObject.AddUrlSegment("inbox", request.Inbox); + requestObject.AddSafeQueryParameter("whtoken", request.WebhookToken); + requestObject.AddJsonBody(request.Webhook); + + var response = await httpClient.ExecuteAsync(requestObject); + return response; + } + /// /// This command will Webhook messages into your Private Domain /// The incoming Webhook will arrive in the inbox designated by the "to" field in the incoming request payload. diff --git a/mailinator-csharp-client/Models/Messages/Requests/GetMessageHeadersRequest.cs b/mailinator-csharp-client/Models/Messages/Requests/GetMessageHeadersRequest.cs new file mode 100644 index 0000000..5821a75 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Requests/GetMessageHeadersRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Messages.Requests +{ + public class GetMessageHeadersRequest + { + /// The domain containing the message. + [JsonProperty("domain")] + public string Domain { get; set; } + + /// The message ID returned by a message listing. + [JsonProperty("message_id")] + public string MessageId { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Requests/GetMessageSummaryRequest.cs b/mailinator-csharp-client/Models/Messages/Requests/GetMessageSummaryRequest.cs new file mode 100644 index 0000000..59651f1 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Requests/GetMessageSummaryRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Messages.Requests +{ + public class GetMessageSummaryRequest + { + /// The domain containing the message. + [JsonProperty("domain")] + public string Domain { get; set; } + + /// The message ID returned by a message listing. + [JsonProperty("message_id")] + public string MessageId { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextHtmlRequest.cs b/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextHtmlRequest.cs new file mode 100644 index 0000000..ad76c27 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextHtmlRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Messages.Requests +{ + public class GetMessageTextHtmlRequest + { + /// The domain containing the message. + [JsonProperty("domain")] + public string Domain { get; set; } + + /// The message ID returned by a message listing. + [JsonProperty("message_id")] + public string MessageId { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextPlainRequest.cs b/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextPlainRequest.cs new file mode 100644 index 0000000..68dab8d --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextPlainRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Messages.Requests +{ + public class GetMessageTextPlainRequest + { + /// The domain containing the message. + [JsonProperty("domain")] + public string Domain { get; set; } + + /// The message ID returned by a message listing. + [JsonProperty("message_id")] + public string MessageId { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextRequest.cs b/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextRequest.cs new file mode 100644 index 0000000..ba50e94 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Requests/GetMessageTextRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Messages.Requests +{ + public class GetMessageTextRequest + { + /// The domain containing the message. + [JsonProperty("domain")] + public string Domain { get; set; } + + /// The message ID returned by a message listing. + [JsonProperty("message_id")] + public string MessageId { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Requests/ListDomainMessagesRequest.cs b/mailinator-csharp-client/Models/Messages/Requests/ListDomainMessagesRequest.cs new file mode 100644 index 0000000..63d66ec --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Requests/ListDomainMessagesRequest.cs @@ -0,0 +1,73 @@ +using mailinator_csharp_client.Models.Messages.Entities; +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Messages.Requests +{ + public class ListDomainMessagesRequest + { + /// + /// public - Fetch Message Summaries from the Public Mailinator System + /// private - Fetch Message Summaries from all Your Private Domains + /// [your_private_domain.com] - Fetch Message Summaries from a specific Private Domain + /// + [JsonProperty("domain")] + public string Domain { get; set; } = "private"; + + /// + /// Omitted - fetch message summaries for the entire domain + /// * - Fetch All Messages summaries for an entire domain + /// [inbox_name] - Fetch All Messages summaries for a given Inbox + /// [inbox_name*] - Fetch All Messages summaries for a given Inbox Prefix + /// + [JsonProperty("inbox")] + public string Inbox { get; set; } + + /// + /// Skip this many emails in your Private Domain. Default Value 0. Required - no + /// + [JsonProperty("skip")] + public int Skip { get; set; } = 0; + + /// + /// Number of emails to fetch from your Private Domain. Default Value 50. Required - no + /// + [JsonProperty("limit")] + public int Limit { get; set; } = 50; + + /// + /// Sort results by ascending or descending. Default Value 'descending'. Required - no + /// + [JsonProperty("sort")] + public Sort Sort { get; set; } = Sort.desc; + + /// + /// true: decode encoded subjects. Default Value 'false'. Required - no + /// + [JsonProperty("decode_subject")] + public bool DecodeSubject { get; set; } = false; + + /// + /// Pagination cursor for large result sets (obtained from previous response). Required - no + /// + [JsonProperty("cursor")] + public string Cursor { get; set; } + + /// + /// Return full email content with body/attachments (true) or just metadata (false). Default: false. Required - no + /// + [JsonProperty("full")] + public bool? Full { get; set; } + + /// + /// Auto-delete message after retrieval (e.g., "10s" = 10 seconds, "5m" = 5 minutes). Required - no + /// + [JsonProperty("delete")] + public string Delete { get; set; } + + /// + /// Maximum time to wait for new messages (e.g., "30s" = 30 seconds). Required - no + /// + [JsonProperty("wait")] + public string Wait { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Responses/GetMessageHeadersResponse.cs b/mailinator-csharp-client/Models/Messages/Responses/GetMessageHeadersResponse.cs new file mode 100644 index 0000000..850e01c --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Responses/GetMessageHeadersResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Responses +{ + public class GetMessageHeadersResponse + { + /// SMTP headers, including custom headers and multi-valued headers such as received. + [JsonProperty("headers")] + public Dictionary Headers; + } +} diff --git a/mailinator-csharp-client/Models/Messages/Responses/GetMessageSummaryResponse.cs b/mailinator-csharp-client/Models/Messages/Responses/GetMessageSummaryResponse.cs new file mode 100644 index 0000000..c1fc6f5 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Responses/GetMessageSummaryResponse.cs @@ -0,0 +1,12 @@ +using mailinator_csharp_client.Models.Messages.Entities; +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Responses +{ + public class GetMessageSummaryResponse + { + /// Message metadata. Body and attachment content are not returned by this endpoint. + [JsonProperty("summary")] + public Message Summary; + } +} diff --git a/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextHtmlResponse.cs b/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextHtmlResponse.cs new file mode 100644 index 0000000..1155711 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextHtmlResponse.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Responses +{ + public class GetMessageTextHtmlResponse + { + /// The text/html message body, preserving HTML markup. + [JsonProperty("text/html")] + public string TextHtml { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextPlainResponse.cs b/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextPlainResponse.cs new file mode 100644 index 0000000..e270e42 --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextPlainResponse.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Responses +{ + public class GetMessageTextPlainResponse + { + /// The text/plain message body. + [JsonProperty("text/plain")] + public string TextPlain { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextResponse.cs b/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextResponse.cs new file mode 100644 index 0000000..70f8c1b --- /dev/null +++ b/mailinator-csharp-client/Models/Messages/Responses/GetMessageTextResponse.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Responses +{ + public class GetMessageTextResponse + { + /// Extracted message text, preserving any quoted-printable artifacts returned by the API. + [JsonProperty("text")] + public string Text { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Webhooks/Entities/WebhookMessage.cs b/mailinator-csharp-client/Models/Webhooks/Entities/WebhookMessage.cs new file mode 100644 index 0000000..8708af3 --- /dev/null +++ b/mailinator-csharp-client/Models/Webhooks/Entities/WebhookMessage.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace mailinator_csharp_client.Models.Webhooks.Entities +{ + /// Payload for domain and inbox webhook injection. + public class WebhookMessage : Webhook + { + [JsonProperty("html")] + public string Html { get; set; } + + [JsonProperty("headers")] + public Dictionary Headers { get; set; } + + [JsonExtensionData] + public IDictionary AdditionalProperties { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Webhooks/Requests/PostWebhookInboxMessageRequest.cs b/mailinator-csharp-client/Models/Webhooks/Requests/PostWebhookInboxMessageRequest.cs new file mode 100644 index 0000000..0867eb5 --- /dev/null +++ b/mailinator-csharp-client/Models/Webhooks/Requests/PostWebhookInboxMessageRequest.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Webhooks.Requests +{ + public class PostWebhookInboxMessageRequest : PostWebhookMessageRequest + { + [JsonProperty("inbox")] + public string Inbox { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Webhooks/Requests/PostWebhookMessageRequest.cs b/mailinator-csharp-client/Models/Webhooks/Requests/PostWebhookMessageRequest.cs new file mode 100644 index 0000000..d1a233f --- /dev/null +++ b/mailinator-csharp-client/Models/Webhooks/Requests/PostWebhookMessageRequest.cs @@ -0,0 +1,20 @@ +using mailinator_csharp_client.Models.Webhooks.Entities; +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Webhooks.Requests +{ + public class PostWebhookMessageRequest + { + /// Private domain name, or a webhook token when WebhookToken is omitted. + [JsonProperty("domain")] + public string Domain { get; set; } + + /// Optional webhook token sent as whtoken when Domain is a domain name. + [JsonProperty("whtoken")] + public string WebhookToken { get; set; } + + /// Message payload. Set To to the recipient inbox. + [JsonIgnore] + public WebhookMessage Webhook { get; set; } + } +} diff --git a/mailinator-csharp-client/Models/Webhooks/Responses/PostWebhookMessageResponse.cs b/mailinator-csharp-client/Models/Webhooks/Responses/PostWebhookMessageResponse.cs new file mode 100644 index 0000000..9d40d28 --- /dev/null +++ b/mailinator-csharp-client/Models/Webhooks/Responses/PostWebhookMessageResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace mailinator_csharp_client.Models.Webhooks.Responses +{ + public class PostWebhookMessageResponse + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + } +} diff --git a/mailinator-csharp-client/mailinator-csharp-client.csproj b/mailinator-csharp-client/mailinator-csharp-client.csproj index 4021225..a09c66c 100644 --- a/mailinator-csharp-client/mailinator-csharp-client.csproj +++ b/mailinator-csharp-client/mailinator-csharp-client.csproj @@ -15,15 +15,16 @@ MIT README.md MailinatorApiClient - 1.0.7 - 1.0.7 - 1.0.7 + 1.0.8 + 1.0.8 + 1.0.8 True - - + + + diff --git a/mailinator-csharp-client/packages.lock.json b/mailinator-csharp-client/packages.lock.json new file mode 100644 index 0000000..8533912 --- /dev/null +++ b/mailinator-csharp-client/packages.lock.json @@ -0,0 +1,225 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.7.1": { + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net471": "1.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.4, )", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "RestSharp": { + "type": "Direct", + "requested": "[114.0.0, )", + "resolved": "114.0.0", + "contentHash": "8pDO4q+K8nIqXbvGCb9ciKcngj36pCXGRDzOYPEPoS2IvBIL+mejmUMAtSNALt/NkDc9LQmHvM95kdniLSbGtg==", + "dependencies": { + "System.Text.Json": "10.0.0" + } + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "X2rXQy7g8KEUvWVbAz6vOk6byI1eMgmzFeXbKxq4BfZCfFy5S91K+K+hd4ZBlSp0wL1Y8FyGRXs6+hvABTjwQw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.11", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.11", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.11", + "System.Threading.Tasks.Extensions": "4.6.3", + "System.ValueTuple": "4.6.2" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "pUkYI1f99hY9giu/MRaEVN53RO7skXwR7FGtVvFJCK5ezdg5i0W+z1yj/91X5aKv07xGuSB5n1g/Zt2P4Ooq6g==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Microsoft.NETFramework.ReferenceAssemblies.net471": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "Kf3vusy+mwHzn+0osmJAMrZNG00/UkwywjwfFKLUWSYEXiIMD6SEMGP9bG3sH1B54IiydxAneLubS5/Ajjn/Tw==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rg8LPOZ62quw3aTnsQNh9rssncaMs69WEM1DiJdDkV+o4Llb2s5Pasy5Bulfm8zL+pE4gDcSQo7V2zW2jvxwYg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "R7H2/Oqr3onFff/JKDpfRjwxcQZlocF/eQ0ce8go/OTjqz+6LEp/fZK8j1YnDobtysChATEg7krVq4hQJ3IoeQ==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.2", + "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" + } + }, + ".NETStandard,Version=v2.0": { + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.4, )", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "RestSharp": { + "type": "Direct", + "requested": "[114.0.0, )", + "resolved": "114.0.0", + "contentHash": "8pDO4q+K8nIqXbvGCb9ciKcngj36pCXGRDzOYPEPoS2IvBIL+mejmUMAtSNALt/NkDc9LQmHvM95kdniLSbGtg==", + "dependencies": { + "System.Text.Json": "10.0.0" + } + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "X2rXQy7g8KEUvWVbAz6vOk6byI1eMgmzFeXbKxq4BfZCfFy5S91K+K+hd4ZBlSp0wL1Y8FyGRXs6+hvABTjwQw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.11", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.11", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.11", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "pUkYI1f99hY9giu/MRaEVN53RO7skXwR7FGtVvFJCK5ezdg5i0W+z1yj/91X5aKv07xGuSB5n1g/Zt2P4Ooq6g==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rg8LPOZ62quw3aTnsQNh9rssncaMs69WEM1DiJdDkV+o4Llb2s5Pasy5Bulfm8zL+pE4gDcSQo7V2zW2jvxwYg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "R7H2/Oqr3onFff/JKDpfRjwxcQZlocF/eQ0ce8go/OTjqz+6LEp/fZK8j1YnDobtysChATEg7krVq4hQJ3IoeQ==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + } + } + } +} \ No newline at end of file