diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9a1c81b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --configuration Release --no-build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb36e42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +bin/ +obj/ +.vs/ +.vscode/ +.idea/ +TestResults/ +coverage/ +*.user +*.suo +*.nupkg +*.snupkg +artifacts/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..bdc37b5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,14 @@ + + + true + enable + enable + latest + https://github.com/teesofttech/Termii.SDK + git + https://github.com/teesofttech/Termii.SDK + MIT + Teesoft Tech + Teesoft Tech + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e02b29 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Termii .NET SDK + +A .NET SDK for the Termii messaging, token, and insights APIs. + +> This SDK is in early development. The first milestones establish the client, tests, examples, API coverage matrix, and package structure before endpoint support is added feature by feature. + +## Current Status + +The initial repository setup includes: + +- A `Termii` SDK project targeting `netstandard2.0` and `net8.0`. +- A lightweight unit test project. +- An examples project for developer-facing usage. +- An API coverage matrix in `docs/API_COVERAGE.md`. + +## Usage + +```csharp +using Termii; + +var client = new TermiiClient(new TermiiOptions +{ + ApiKey = "your-termii-api-key" +}); +``` + +The default base URL is `https://api.ng.termii.com`. + +## Examples + +Run the examples project after setting your Termii API key: + +```bash +export TERMII_API_KEY="your-termii-api-key" +dotnet run --project examples/Termii.Examples +``` + +## Tests + +Run the local test suite: + +```bash +dotnet test +``` + +Integration tests are opt-in and must not call live Termii endpoints unless the required environment variables are present: + +```bash +export TERMII_API_KEY="your-termii-api-key" +export TERMII_BASE_URL="https://api.ng.termii.com" +dotnet test tests/Termii.IntegrationTests +``` + +As endpoint support is added, integration tests should remain credential-gated so normal CI can run without network access or Termii credits. + +## Roadmap + +Development is tracked through GitHub issues: + +- SDK foundation and HTTP pipeline. +- Messaging APIs. +- Sender ID and number APIs. +- Token/OTP APIs. +- Insights APIs. +- CI, documentation, NuGet packaging, and GitHub Releases. + +Official Termii documentation: https://developer.termii.com/ diff --git a/Termii.SDK.sln b/Termii.SDK.sln new file mode 100644 index 0000000..ad4a40f --- /dev/null +++ b/Termii.SDK.sln @@ -0,0 +1,88 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Termii", "src\Termii\Termii.csproj", "{2C2E40AF-504E-4A61-B46A-84A1BC352ED5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Termii.Tests", "tests\Termii.Tests\Termii.Tests.csproj", "{6AE7CBA2-1987-477E-B14D-C34F50A5DD48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Termii.IntegrationTests", "tests\Termii.IntegrationTests\Termii.IntegrationTests.csproj", "{73C4DC47-8035-4F23-8C46-1AAFE5E7D449}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Termii.Examples", "examples\Termii.Examples\Termii.Examples.csproj", "{063DC3E1-23F8-4D66-918D-8FB1C61B648D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Debug|x64.Build.0 = Debug|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Debug|x86.Build.0 = Debug|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Release|Any CPU.Build.0 = Release|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Release|x64.ActiveCfg = Release|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Release|x64.Build.0 = Release|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Release|x86.ActiveCfg = Release|Any CPU + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5}.Release|x86.Build.0 = Release|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Debug|x64.Build.0 = Debug|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Debug|x86.Build.0 = Debug|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Release|Any CPU.Build.0 = Release|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Release|x64.ActiveCfg = Release|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Release|x64.Build.0 = Release|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Release|x86.ActiveCfg = Release|Any CPU + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48}.Release|x86.Build.0 = Release|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Debug|x64.ActiveCfg = Debug|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Debug|x64.Build.0 = Debug|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Debug|x86.ActiveCfg = Debug|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Debug|x86.Build.0 = Debug|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Release|Any CPU.Build.0 = Release|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Release|x64.ActiveCfg = Release|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Release|x64.Build.0 = Release|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Release|x86.ActiveCfg = Release|Any CPU + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449}.Release|x86.Build.0 = Release|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Debug|x64.ActiveCfg = Debug|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Debug|x64.Build.0 = Debug|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Debug|x86.ActiveCfg = Debug|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Debug|x86.Build.0 = Debug|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Release|Any CPU.Build.0 = Release|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Release|x64.ActiveCfg = Release|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Release|x64.Build.0 = Release|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Release|x86.ActiveCfg = Release|Any CPU + {063DC3E1-23F8-4D66-918D-8FB1C61B648D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2C2E40AF-504E-4A61-B46A-84A1BC352ED5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {6AE7CBA2-1987-477E-B14D-C34F50A5DD48} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {73C4DC47-8035-4F23-8C46-1AAFE5E7D449} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {063DC3E1-23F8-4D66-918D-8FB1C61B648D} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} + EndGlobalSection +EndGlobal diff --git a/docs/API_COVERAGE.md b/docs/API_COVERAGE.md new file mode 100644 index 0000000..44234db --- /dev/null +++ b/docs/API_COVERAGE.md @@ -0,0 +1,21 @@ +# Termii API Coverage + +This matrix tracks the Termii API surface against SDK implementation work. + +| Area | Capability | SDK status | Issue | +| --- | --- | --- | --- | +| Foundation | Solution, projects, examples, basic tests | In progress | #1 | +| Foundation | Client configuration, authentication, HTTP pipeline | Planned | #2 | +| Foundation | Error handling and validation | Planned | #6 | +| Messaging | Send SMS and channel messages | Planned | #7 | +| Messaging | Sender ID APIs | Planned | #3 | +| Messaging | Number API messaging | Planned | #5 | +| Token | Send, verify, and generate OTP | Planned | #4 | +| Insights | Balance, DND, number status, message history | Planned | #9 | +| Release | NuGet package metadata and publishing workflow | Planned | #8 | +| Release | GitHub Release and NuGet publication | Planned | #13 | + +References: + +- Termii developer docs: https://developer.termii.com/ +- Postman collection: https://termii.s3.us-west-1.amazonaws.com/upload/files/UozvGXj5czYEeY4OmE2f.json diff --git a/examples/Termii.Examples/Program.cs b/examples/Termii.Examples/Program.cs new file mode 100644 index 0000000..31b4108 --- /dev/null +++ b/examples/Termii.Examples/Program.cs @@ -0,0 +1,16 @@ +using Termii; + +var apiKey = Environment.GetEnvironmentVariable("TERMII_API_KEY"); + +if (string.IsNullOrWhiteSpace(apiKey)) +{ + Console.WriteLine("Set TERMII_API_KEY to run the Termii examples."); + return; +} + +var client = new TermiiClient(new TermiiOptions +{ + ApiKey = apiKey, +}); + +Console.WriteLine($"Termii client configured for {client.Options.BaseUrl}."); diff --git a/examples/Termii.Examples/Termii.Examples.csproj b/examples/Termii.Examples/Termii.Examples.csproj new file mode 100644 index 0000000..387485f --- /dev/null +++ b/examples/Termii.Examples/Termii.Examples.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + false + + + + + + diff --git a/src/Termii/Termii.csproj b/src/Termii/Termii.csproj new file mode 100644 index 0000000..cc7e907 --- /dev/null +++ b/src/Termii/Termii.csproj @@ -0,0 +1,12 @@ + + + netstandard2.0;net8.0 + Termii + Termii + Termii + A .NET SDK for the Termii messaging, token, and insights APIs. + termii;sms;otp;messaging;dotnet;sdk + true + $(NoWarn);1591 + + diff --git a/src/Termii/TermiiClient.cs b/src/Termii/TermiiClient.cs new file mode 100644 index 0000000..dfe9b6f --- /dev/null +++ b/src/Termii/TermiiClient.cs @@ -0,0 +1,15 @@ +namespace Termii; + +/// +/// Main entry point for the Termii SDK. +/// +public sealed class TermiiClient +{ + public TermiiClient(TermiiOptions options) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + Options.Validate(); + } + + public TermiiOptions Options { get; } +} diff --git a/src/Termii/TermiiOptions.cs b/src/Termii/TermiiOptions.cs new file mode 100644 index 0000000..6e3ea21 --- /dev/null +++ b/src/Termii/TermiiOptions.cs @@ -0,0 +1,26 @@ +namespace Termii; + +/// +/// Configuration required to call the Termii API. +/// +public sealed class TermiiOptions +{ + public const string DefaultBaseUrl = "https://api.ng.termii.com"; + + public string ApiKey { get; set; } = string.Empty; + + public Uri BaseUrl { get; set; } = new(DefaultBaseUrl); + + internal void Validate() + { + if (string.IsNullOrWhiteSpace(ApiKey)) + { + throw new InvalidOperationException("A Termii API key is required."); + } + + if (!BaseUrl.IsAbsoluteUri) + { + throw new InvalidOperationException("The Termii base URL must be absolute."); + } + } +} diff --git a/tests/Termii.IntegrationTests/Termii.IntegrationTests.csproj b/tests/Termii.IntegrationTests/Termii.IntegrationTests.csproj new file mode 100644 index 0000000..219cfb4 --- /dev/null +++ b/tests/Termii.IntegrationTests/Termii.IntegrationTests.csproj @@ -0,0 +1,19 @@ + + + net8.0 + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/tests/Termii.IntegrationTests/TermiiClientIntegrationTests.cs b/tests/Termii.IntegrationTests/TermiiClientIntegrationTests.cs new file mode 100644 index 0000000..df2c1d8 --- /dev/null +++ b/tests/Termii.IntegrationTests/TermiiClientIntegrationTests.cs @@ -0,0 +1,34 @@ +using Termii; +using Xunit; + +namespace Termii.IntegrationTests; + +public sealed class TermiiClientIntegrationTests +{ + [Fact] + public void CanCreateClientFromIntegrationEnvironment() + { + var apiKey = Environment.GetEnvironmentVariable("TERMII_API_KEY"); + + if (string.IsNullOrWhiteSpace(apiKey)) + { + return; + } + + var baseUrl = Environment.GetEnvironmentVariable("TERMII_BASE_URL"); + var options = new TermiiOptions + { + ApiKey = apiKey, + }; + + if (!string.IsNullOrWhiteSpace(baseUrl)) + { + options.BaseUrl = new Uri(baseUrl); + } + + var client = new TermiiClient(options); + + Assert.False(string.IsNullOrWhiteSpace(client.Options.ApiKey)); + Assert.True(client.Options.BaseUrl.IsAbsoluteUri); + } +} diff --git a/tests/Termii.Tests/Termii.Tests.csproj b/tests/Termii.Tests/Termii.Tests.csproj new file mode 100644 index 0000000..219cfb4 --- /dev/null +++ b/tests/Termii.Tests/Termii.Tests.csproj @@ -0,0 +1,19 @@ + + + net8.0 + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/tests/Termii.Tests/TermiiClientTests.cs b/tests/Termii.Tests/TermiiClientTests.cs new file mode 100644 index 0000000..46efaab --- /dev/null +++ b/tests/Termii.Tests/TermiiClientTests.cs @@ -0,0 +1,29 @@ +using Termii; +using Xunit; + +namespace Termii.Tests; + +public sealed class TermiiClientTests +{ + [Fact] + public void ConstructorRejectsMissingApiKey() + { + var exception = Assert.Throws(() => new TermiiClient(new TermiiOptions())); + + Assert.Contains("API key", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void ConstructorAcceptsValidOptions() + { + var options = new TermiiOptions + { + ApiKey = "test-api-key", + }; + + var client = new TermiiClient(options); + + Assert.Equal("test-api-key", client.Options.ApiKey); + Assert.Equal(TermiiOptions.DefaultBaseUrl, client.Options.BaseUrl.ToString().TrimEnd('/')); + } +}