Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ Fetch sender IDs:
var senderIds = await client.SenderIds.GetAsync();
```

Send through the Number API:

```csharp
var numberMessage = await client.Numbers.SendAsync(new SendNumberMessageRequest
{
To = "2348012345678",
Sms = "Hello from a dedicated Termii number"
});
```

## Examples

Run the examples project after setting your Termii API key:
Expand All @@ -71,6 +81,14 @@ export TERMII_API_KEY="your-termii-api-key"
dotnet run --project examples/Termii.Examples
```

To send a live Number API example message, opt in explicitly:

```bash
export TERMII_SEND_NUMBER_MESSAGE="true"
export TERMII_EXAMPLE_PHONE_NUMBER="2348012345678"
dotnet run --project examples/Termii.Examples
```

## Tests

Run the local test suite:
Expand Down
8 changes: 4 additions & 4 deletions docs/API_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ The Termii docs describe a REST/JSON API and state that each account has its own
| Reusable fake HTTP test helpers and opt-in live test conventions | Implemented | #10, PR #20 |
| README endpoint examples | Planned | #12 |
| CI build/test/package validation | Implemented | #11, PR #22 |
| NuGet package metadata and publishing workflow | In progress | #8 |
| NuGet package metadata and publishing workflow | Implemented | #8, PR #23 |
| First GitHub Release and NuGet publish | Planned | #13 |

## Endpoint Coverage

| Area | Capability | Method | Path | Auth placement | SDK surface | SDK status | Tracking | Test status |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Messaging | Fetch sender IDs | GET | `/api/sender-id` | Query | `TermiiClient.SenderIds` | In progress | #3 | Unit tests added |
| Messaging | Request sender ID | POST | `/api/sender-id/request` | JSON body | `TermiiClient.SenderIds` | In progress | #3 | Unit tests added |
| Messaging | Fetch sender IDs | GET | `/api/sender-id` | Query | `TermiiClient.SenderIds` | Implemented | #3, PR #24 | Unit tests added |
| Messaging | Request sender ID | POST | `/api/sender-id/request` | JSON body | `TermiiClient.SenderIds` | Implemented | #3, PR #24 | Unit tests added |
| Messaging | Send SMS/channel message | POST | `/api/sms/send` | JSON body | `TermiiClient.Messaging` | Implemented | #7, PR #21 | Unit tests added |
| Messaging | Send WhatsApp conversational message | POST | `/api/sms/send` | JSON body | `TermiiClient.Messaging` | Implemented | #7, PR #21 | Unit tests added |
| Messaging | Send bulk message | POST | `/api/sms/send/bulk` | JSON body | `TermiiClient.Messaging` | Implemented | #7, PR #21 | Unit tests added |
| Messaging | Send via Number API | POST | `/api/sms/number/send` | Query or JSON body, docs/examples vary | `TermiiClient.Messaging` or `TermiiClient.Numbers` | Planned | #5 | Planned unit + optional integration |
| Messaging | Send via Number API | POST | `/api/sms/number/send` | JSON body | `TermiiClient.Numbers` | In progress | #5 | Unit tests added |
| Token | Send OTP token | POST | `/api/sms/otp/send` | JSON body | `TermiiClient.Token` | Planned | #4 | Planned unit + optional integration |
| Token | Verify OTP token | POST | `/api/sms/otp/verify` | JSON body | `TermiiClient.Token` | Planned | #4 | Planned unit + optional integration |
| Token | Generate in-app OTP token | POST | `/api/sms/otp/generate` | JSON body | `TermiiClient.Token` | Planned | #4 | Planned unit + optional integration |
Expand Down
20 changes: 20 additions & 0 deletions examples/Termii.Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,23 @@
});

Console.WriteLine($"Termii client configured for {client.Options.BaseUrl}.");

var phoneNumber = Environment.GetEnvironmentVariable("TERMII_EXAMPLE_PHONE_NUMBER");
var sendNumberMessage = string.Equals(
Environment.GetEnvironmentVariable("TERMII_SEND_NUMBER_MESSAGE"),
"true",
StringComparison.OrdinalIgnoreCase);

if (!sendNumberMessage || string.IsNullOrWhiteSpace(phoneNumber))
{
Console.WriteLine("Set TERMII_SEND_NUMBER_MESSAGE=true and TERMII_EXAMPLE_PHONE_NUMBER to send a Number API message.");
return;
}

var response = await client.Numbers.SendAsync(new SendNumberMessageRequest
{
To = phoneNumber,
Sms = "Hello from Termii.SDK",
});

Console.WriteLine($"Number API response: {response.Message ?? response.Code ?? "sent"}");
8 changes: 8 additions & 0 deletions src/Termii/Numbers/ITermiiNumberClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Termii;

public interface ITermiiNumberClient
{
Task<TermiiMessageResponse> SendAsync(
SendNumberMessageRequest request,
CancellationToken cancellationToken = default);
}
18 changes: 18 additions & 0 deletions src/Termii/Numbers/SendNumberMessageRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;

namespace Termii;

public sealed class SendNumberMessageRequest
{
[JsonPropertyName("to")]
public string To { get; set; } = string.Empty;

[JsonPropertyName("sms")]
public string Sms { get; set; } = string.Empty;

internal void Validate()
{
TermiiRequestValidation.Required(To, nameof(To));
TermiiRequestValidation.Required(Sms, nameof(Sms));
}
}
30 changes: 30 additions & 0 deletions src/Termii/Numbers/TermiiNumberClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Termii;

internal sealed class TermiiNumberClient : ITermiiNumberClient
{
private readonly TermiiJsonHttpPipeline _pipeline;

public TermiiNumberClient(TermiiJsonHttpPipeline pipeline)
{
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
}

public Task<TermiiMessageResponse> SendAsync(
SendNumberMessageRequest request,
CancellationToken cancellationToken = default)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

request.Validate();

return _pipeline.SendJsonAsync<TermiiMessageResponse>(
HttpMethod.Post,
"/api/sms/number/send",
request,
TermiiAuthenticationLocation.Body,
cancellationToken);
}
}
3 changes: 3 additions & 0 deletions src/Termii/TermiiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ private TermiiClient(HttpClient httpClient, TermiiOptions options, bool ownsHttp
_pipeline = new TermiiJsonHttpPipeline(_httpClient, Options);
Messaging = new TermiiMessagingClient(_pipeline);
SenderIds = new TermiiSenderIdClient(_pipeline);
Numbers = new TermiiNumberClient(_pipeline);
}

public TermiiOptions Options { get; }
Expand All @@ -44,6 +45,8 @@ private TermiiClient(HttpClient httpClient, TermiiOptions options, bool ownsHttp

public ITermiiSenderIdClient SenderIds { get; }

public ITermiiNumberClient Numbers { get; }

public void Dispose()
{
if (_ownsHttpClient)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ public void CanCreateClientFromIntegrationEnvironment()
Assert.True(client.Options.BaseUrl.IsAbsoluteUri);
Assert.NotNull(client.Messaging);
Assert.NotNull(client.SenderIds);
Assert.NotNull(client.Numbers);
}
}
68 changes: 68 additions & 0 deletions tests/Termii.Tests/TermiiNumberClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Net;
using Termii;
using Termii.Tests.Infrastructure;
using Xunit;

namespace Termii.Tests;

public sealed class TermiiNumberClientTests
{
[Fact]
public async Task SendAsyncPostsNumberMessageBodyAndDeserializesResponse()
{
using var handler = new TestHttpMessageHandler(
HttpStatusCode.OK,
"""
{
"code": "ok",
"balance": "38.75",
"message_id": "number-12345",
"message": "Successfully Sent",
"user": "termii-user",
"message_id_str": "number-12345"
}
""");
var client = TestTermiiClientFactory.Create(handler);

var response = await client.Numbers.SendAsync(
new SendNumberMessageRequest
{
To = "2348012345678",
Sms = "Hello from the Number API",
},
CancellationToken.None);

var request = handler.LastRequest;
Assert.NotNull(request);
using var body = await request.ReadJsonBodyAsync(CancellationToken.None);

Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal("https://example.test/api/sms/number/send", request.RequestUri!.ToString());
Assert.Equal("test-api-key", body.RootElement.GetProperty("api_key").GetString());
Assert.Equal("2348012345678", body.RootElement.GetProperty("to").GetString());
Assert.Equal("Hello from the Number API", body.RootElement.GetProperty("sms").GetString());
Assert.False(body.RootElement.TryGetProperty("from", out _));

Assert.Equal("ok", response.Code);
Assert.Equal(38.75m, response.Balance);
Assert.Equal("number-12345", response.MessageId);
Assert.Equal("Successfully Sent", response.Message);
Assert.Equal("termii-user", response.User);
Assert.Equal("number-12345", response.MessageIdString);
}

[Fact]
public async Task SendAsyncRejectsMissingRequiredFields()
{
using var handler = new TestHttpMessageHandler();
var client = TestTermiiClientFactory.Create(handler);

await Assert.ThrowsAsync<ArgumentException>(() => client.Numbers.SendAsync(
new SendNumberMessageRequest
{
To = "2348012345678",
Sms = "",
},
CancellationToken.None));
}
}
Loading