From e1d76b8798a82f592cce1cfc324e65c9224e740a Mon Sep 17 00:00:00 2001 From: Kusp00110110 <113149818+Kusp00110110@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:18:52 +0200 Subject: [PATCH 1/3] chore: clean build outputs --- .gitignore | 3 + AGENTS.md | 9 ++ AliExpressDotnetSdk.sln | 34 +++++ .../AliExpressSdk.Tests.csproj | 27 ++++ dotnet/AliExpressSdk.Tests/UnitTest1.cs | 31 ++++ dotnet/AliExpressSdk/AliExpressSdk.csproj | 9 ++ dotnet/AliExpressSdk/Clients/AEBaseClient.cs | 135 ++++++++++++++++++ .../AliExpressSdk/Clients/AESystemClient.cs | 25 ++++ .../AliExpressSdk/Clients/AffiliateClient.cs | 49 +++++++ dotnet/AliExpressSdk/Models/Result.cs | 12 ++ 10 files changed, 334 insertions(+) create mode 100644 AGENTS.md create mode 100644 AliExpressDotnetSdk.sln create mode 100644 dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj create mode 100644 dotnet/AliExpressSdk.Tests/UnitTest1.cs create mode 100644 dotnet/AliExpressSdk/AliExpressSdk.csproj create mode 100644 dotnet/AliExpressSdk/Clients/AEBaseClient.cs create mode 100644 dotnet/AliExpressSdk/Clients/AESystemClient.cs create mode 100644 dotnet/AliExpressSdk/Clients/AffiliateClient.cs create mode 100644 dotnet/AliExpressSdk/Models/Result.cs diff --git a/.gitignore b/.gitignore index f06235c..bdb46ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules dist + +dotnet/**/bin +dotnet/**/obj diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3971fc7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# Repository Purpose + +This repository contains a TypeScript implementation of the AliExpress SDK and its .NET port. It serves as a blueprint and reference for building AliExpress integrations in different ecosystems. + +# Development Guidelines + +- Ensure that every change builds and tests the .NET solution. +- Run `dotnet build` and `dotnet test` before committing. + diff --git a/AliExpressDotnetSdk.sln b/AliExpressDotnetSdk.sln new file mode 100644 index 0000000..4693177 --- /dev/null +++ b/AliExpressDotnetSdk.sln @@ -0,0 +1,34 @@ + +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}") = "dotnet", "dotnet", "{1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk", "dotnet\AliExpressSdk\AliExpressSdk.csproj", "{BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk.Tests", "dotnet\AliExpressSdk.Tests\AliExpressSdk.Tests.csproj", "{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C}.Release|Any CPU.Build.0 = Release|Any CPU + {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224} + {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224} + EndGlobalSection +EndGlobal diff --git a/dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj b/dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj new file mode 100644 index 0000000..789b27a --- /dev/null +++ b/dotnet/AliExpressSdk.Tests/AliExpressSdk.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/dotnet/AliExpressSdk.Tests/UnitTest1.cs b/dotnet/AliExpressSdk.Tests/UnitTest1.cs new file mode 100644 index 0000000..98ec673 --- /dev/null +++ b/dotnet/AliExpressSdk.Tests/UnitTest1.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using AliExpressSdk.Clients; +using Xunit; + +namespace AliExpressSdk.Tests; + +public class SigningTests +{ + private class TestClient : AEBaseClient + { + public TestClient() : base("test", "secret", "session") { } + public string DoSign(IDictionary p) => Sign(p); + } + + [Fact] + public void Sign_ComputesExpectedHash() + { + var client = new TestClient(); + var parameters = new Dictionary + { + ["method"] = "/auth/token/create", + ["app_key"] = "test", + ["session"] = "session", + ["timestamp"] = "12345", + ["simplify"] = "true", + ["sign_method"] = "sha256" + }; + var sign = client.DoSign(parameters); + Assert.Equal("083482F9A0CE8559B567E46222AA1401B09BBDACC409D0BDA77A9A385A0BD31C", sign); + } +} diff --git a/dotnet/AliExpressSdk/AliExpressSdk.csproj b/dotnet/AliExpressSdk/AliExpressSdk.csproj new file mode 100644 index 0000000..bb23fb7 --- /dev/null +++ b/dotnet/AliExpressSdk/AliExpressSdk.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/dotnet/AliExpressSdk/Clients/AEBaseClient.cs b/dotnet/AliExpressSdk/Clients/AEBaseClient.cs new file mode 100644 index 0000000..3580a13 --- /dev/null +++ b/dotnet/AliExpressSdk/Clients/AEBaseClient.cs @@ -0,0 +1,135 @@ +using System.Linq; +using System.Globalization; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using AliExpressSdk.Models; + +namespace AliExpressSdk.Clients; + +public class AEBaseClient +{ + private readonly HttpClient _httpClient; + public string AppKey { get; } + public string AppSecret { get; } + public string Session { get; } + + private const string TopApiUrl = "https://api-sg.aliexpress.com/sync"; + private const string OpApiUrl = "https://api-sg.aliexpress.com/rest"; + private const string SignMethod = "sha256"; + + public AEBaseClient(string appKey, string appSecret, string session, HttpClient? httpClient = null) + { + AppKey = appKey; + AppSecret = appSecret; + Session = session; + _httpClient = httpClient ?? new HttpClient(); + } + + protected string Sign(IDictionary parameters) + { + var p = new Dictionary(parameters); + var baseString = string.Empty; + if (p.TryGetValue("method", out var method) && method.Contains('/')) + { + baseString = method; + p.Remove("method"); + } + + foreach (var kv in p.Where(kv => kv.Value != null).OrderBy(kv => kv.Key)) + { + baseString += kv.Key + kv.Value; + } + + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(AppSecret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(baseString)); + return string.Concat(hash.Select(b => b.ToString("X2", CultureInfo.InvariantCulture))); + } + + protected string Assemble(IDictionary parameters) + { + var p = new Dictionary(parameters); + var baseUrl = p["method"].Contains('/') ? $"{OpApiUrl}{p["method"]}" : TopApiUrl; + if (p["method"].Contains('/')) + { + p.Remove("method"); + } + + var query = string.Join("&", p + .Where(kv => kv.Value != null) + .OrderBy(kv => kv.Key) + .Select((kv, idx) => + { + var prefix = idx == 0 ? "?" : "&"; + return prefix + Uri.EscapeDataString(kv.Key) + "=" + Uri.EscapeDataString(kv.Value); + })); + + return baseUrl + query; + } + + protected async Task> Call(IDictionary parameters) + { + var url = Assemble(parameters); + try + { + var response = await _httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + return new Result { Ok = false, Message = $"HTTP Error: {(int)response.StatusCode} {response.ReasonPhrase}" }; + } + var content = await response.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(content); + var root = doc.RootElement; + if (root.TryGetProperty("error_response", out var error)) + { + return new Result + { + Ok = false, + Message = "Bad request", + ErrorResponse = error, + RequestId = error.GetProperty("request_id").GetString() + }; + } + return new Result { Ok = true, Data = root }; + } + catch (Exception ex) + { + return new Result { Ok = false, Message = ex.Message }; + } + } + + protected async Task> Execute(string method, IDictionary parameters) + { + var p = new Dictionary(parameters) + { + ["method"] = method, + ["session"] = Session, + ["app_key"] = AppKey, + ["simplify"] = "true", + ["sign_method"] = SignMethod, + ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture) + }; + p["sign"] = Sign(p); + return await Call(p); + } + + public Task> CallApiDirectly(string method, IDictionary parameters) + { + if (string.IsNullOrWhiteSpace(method)) + { + return Task.FromResult(new Result { Ok = false, Message = "Method parameter is required" }); + } + var p = new Dictionary(parameters) + { + ["method"] = method, + ["session"] = Session, + ["app_key"] = AppKey, + ["simplify"] = "true", + ["sign_method"] = SignMethod, + ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture) + }; + p["sign"] = Sign(p); + return Call(p); + } +} diff --git a/dotnet/AliExpressSdk/Clients/AESystemClient.cs b/dotnet/AliExpressSdk/Clients/AESystemClient.cs new file mode 100644 index 0000000..5565eda --- /dev/null +++ b/dotnet/AliExpressSdk/Clients/AESystemClient.cs @@ -0,0 +1,25 @@ +using System.Net.Http; +using System.Text.Json; +using AliExpressSdk.Models; + +namespace AliExpressSdk.Clients; + +public class AESystemClient : AEBaseClient +{ + public AESystemClient(string appKey, string appSecret, string session, HttpClient? httpClient = null) + : base(appKey, appSecret, session, httpClient) + { + } + + public Task> GenerateSecurityToken(IDictionary args) + => Execute("/auth/token/security/create", args); + + public Task> GenerateToken(IDictionary args) + => Execute("/auth/token/create", args); + + public Task> RefreshSecurityToken(IDictionary args) + => Execute("/auth/token/security/refresh", args); + + public Task> RefreshToken(IDictionary args) + => Execute("/auth/token/refresh", args); +} diff --git a/dotnet/AliExpressSdk/Clients/AffiliateClient.cs b/dotnet/AliExpressSdk/Clients/AffiliateClient.cs new file mode 100644 index 0000000..9ddb6c8 --- /dev/null +++ b/dotnet/AliExpressSdk/Clients/AffiliateClient.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using System.Text.Json; +using AliExpressSdk.Models; + +namespace AliExpressSdk.Clients; + +public class AffiliateClient : AESystemClient +{ + public AffiliateClient(string appKey, string appSecret, string session, HttpClient? httpClient = null) + : base(appKey, appSecret, session, httpClient) + { + } + + public Task> GenerateAffiliateLinks(IDictionary args) + => Execute("aliexpress.affiliate.link.generate", args); + + public Task> GetCategories(IDictionary args) + => Execute("aliexpress.affiliate.category.get", args); + + public Task> FeaturedPromoInfo(IDictionary args) + => Execute("aliexpress.affiliate.featuredpromo.get", args); + + public Task> FeaturedPromoProducts(IDictionary args) + => Execute("aliexpress.affiliate.featuredpromo.products.get", args); + + public Task> GetHotProductsDownload(IDictionary args) + => Execute("aliexpress.affiliate.hotproduct.download", args); + + public Task> GetHotProducts(IDictionary args) + => Execute("aliexpress.affiliate.hotproduct.query", args); + + public Task> OrderInfo(IDictionary args) + => Execute("aliexpress.affiliate.order.get", args); + + public Task> OrdersList(IDictionary args) + => Execute("aliexpress.affiliate.order.list", args); + + public Task> OrdersListByIndex(IDictionary args) + => Execute("aliexpress.affiliate.order.listbyindex", args); + + public Task> ProductDetails(IDictionary args) + => Execute("aliexpress.affiliate.productdetail.get", args); + + public Task> QueryProducts(IDictionary args) + => Execute("aliexpress.affiliate.product.query", args); + + public Task> SmartMatchProducts(IDictionary args) + => Execute("aliexpress.affiliate.product.smartmatch", args); +} diff --git a/dotnet/AliExpressSdk/Models/Result.cs b/dotnet/AliExpressSdk/Models/Result.cs new file mode 100644 index 0000000..5e8536d --- /dev/null +++ b/dotnet/AliExpressSdk/Models/Result.cs @@ -0,0 +1,12 @@ +using System.Text.Json; + +namespace AliExpressSdk.Models; + +public class Result +{ + public bool Ok { get; set; } + public string? Message { get; set; } + public T? Data { get; set; } + public JsonElement? ErrorResponse { get; set; } + public string? RequestId { get; set; } +} From c34e6b0473cfe4dfe5c3ce5764110ea9b4bad4ec Mon Sep 17 00:00:00 2001 From: Kusp00110110 <113149818+Kusp00110110@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:51:14 +0200 Subject: [PATCH 2/3] Add NopCommerce plugin planning checklist --- AGENTS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 3971fc7..7976ae3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,15 @@ This repository contains a TypeScript implementation of the AliExpress SDK and i - Ensure that every change builds and tests the .NET solution. - Run `dotnet build` and `dotnet test` before committing. +## NopCommerce Plugin Implementation Plan +- [ ] Review the provided documentation on writing NopCommerce plugins and the Visual Studio template structure to capture required conventions and extension points. +- [ ] Define the plugin's high-level architecture, including catalog synchronization flow, AliExpress API integration touchpoints, configuration settings, and background tasks. +- [ ] Scaffold a new plugin project in a dedicated root-level folder using the official template as a baseline, adapting namespaces and project metadata for AliExpress drop shipping. +- [ ] Integrate the existing AliExpress SDK into the plugin solution, establishing service abstractions for product search, catalog import, and order fulfillment. +- [ ] Implement catalog import features, including UI components for searching AliExpress products, mapping product data, and persisting selections into the NopCommerce catalog. +- [ ] Implement drop-shipping order automation, handling order placement to AliExpress, tracking statuses, and synchronizing fulfillment updates back to NopCommerce. +- [ ] Add administrative configuration pages, localization resources, and permission management required for managing the plugin within NopCommerce. +- [ ] Create automated tests, sample configuration instructions, and deployment documentation to validate and support the plugin. + +Next item to action: Review the documentation resources above and mark the first step as completed once finished. + From 66bff0b71101b6b43b1e64f7f9e8e6a16fd2a042 Mon Sep 17 00:00:00 2001 From: Kusp00110110 <113149818+Kusp00110110@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:32:51 +0200 Subject: [PATCH 3/3] Add .NET console harness and Azure DevOps pipelines --- AGENTS.md | 1 + AliExpressDotnetSdk.sln | 7 + README.md | 46 ++++- azure-pipelines-coverage.yml | 39 ++++ azure-pipelines.yml | 47 +++++ .../AliExpressSdk.ConsoleHarness.csproj | 14 ++ .../AliExpressSdk.ConsoleHarness/Program.cs | 178 ++++++++++++++++++ 7 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 azure-pipelines-coverage.yml create mode 100644 azure-pipelines.yml create mode 100644 dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj create mode 100644 dotnet/AliExpressSdk.ConsoleHarness/Program.cs diff --git a/AGENTS.md b/AGENTS.md index 7976ae3..e95d7c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ This repository contains a TypeScript implementation of the AliExpress SDK and i - Ensure that every change builds and tests the .NET solution. - Run `dotnet build` and `dotnet test` before committing. +- After making changes, add or update unit tests to maintain and improve coverage over time. ## NopCommerce Plugin Implementation Plan - [ ] Review the provided documentation on writing NopCommerce plugins and the Visual Studio template structure to capture required conventions and extension points. diff --git a/AliExpressDotnetSdk.sln b/AliExpressDotnetSdk.sln index 4693177..f3f7938 100644 --- a/AliExpressDotnetSdk.sln +++ b/AliExpressDotnetSdk.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk", "dotnet\Ali EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk.Tests", "dotnet\AliExpressSdk.Tests\AliExpressSdk.Tests.csproj", "{5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliExpressSdk.ConsoleHarness", "dotnet\AliExpressSdk.ConsoleHarness\AliExpressSdk.ConsoleHarness.csproj", "{DB6E8D4D-D753-435F-9B6D-9783966C9F3C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,9 +28,14 @@ Global {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2}.Release|Any CPU.Build.0 = Release|Any CPU + {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB6E8D4D-D753-435F-9B6D-9783966C9F3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {BA27C6F7-3935-43A5-8EDB-F26F43ED0A2C} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224} {5C6E9E68-3619-4C76-A976-6A6BB38BC4F2} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224} + {DB6E8D4D-D753-435F-9B6D-9783966C9F3C} = {1CA53CC0-F2D2-4468-82AB-2C4BEB1B9224} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 817486c..d15ab8b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AliExpress SDK ![npm version](https://img.shields.io/npm/v/ae_sdk?label=) +# AliExpress SDK (.NET Port) ![npm version](https://img.shields.io/npm/v/ae_sdk?label=) ![Typescript](https://img.shields.io/badge/-TypeScript-007ACC?style=flat-square&logo=typescript&logoColor=white) ![CI](https://img.shields.io/github/actions/workflow/status/moh3a/ae_sdk/main.yml?logo=githubactions&logoColor=white&label=CI) @@ -9,11 +9,24 @@ A simple, lightweight, and fully type-safe SDK for the AliExpress Open Platform APIs. Supports System Authentication, Dropshipping, and Affiliate APIs. +This repository contains the original TypeScript implementation published to npm **and** a .NET 8 port that mirrors the same API surface for C# developers. Both implementations share the same intent: provide a pragmatic, well-tested toolkit for working with the AliExpress Open Platform. + +## 🧱 Project Structure + +| Folder | Description | +| ------ | ----------- | +| `src/` | TypeScript source for the npm package. | +| `dotnet/AliExpressSdk` | Core .NET SDK that ports the TypeScript features to C#. | +| `dotnet/AliExpressSdk.Tests` | xUnit test suite with Coverlet coverage collection. | +| `dotnet/AliExpressSdk.ConsoleHarness` | Console-based harness for manual and integration testing against live AliExpress APIs. | + +The solution file `AliExpressDotnetSdk.sln` ties the .NET projects together for local development, automated tests, and packaging. + ## 📖 Overview AliExpress has completely migrated their services from the legacy [Taobao Open Platform](https://developers.aliexpress.com) to the new [Open Platform for International Developers](https://openservice.aliexpress.com). While this update brought significant improvements, [they have yet to release an official Node.js SDK](https://openservice.aliexpress.com/doc/doc.htm?nodeId=27493&docId=118729#/?docId=1371) for the new platform. -This unofficial SDK bridges that gap by providing a simple, consistent interface for Node.js developers to interact with AliExpress APIs. +This unofficial SDK bridges that gap by providing a simple, consistent interface for Node.js developers **and** a faithful .NET port so that teams can choose whichever stack best suits their integration requirements. ## ✨ Features @@ -138,6 +151,35 @@ All API methods return a consistent response structure: } ``` +## 🧪 .NET Integration Harness + +Use the console harness to exercise live API calls without having to write a bespoke application. The harness reads credentials from environment variables and accepts request parameters via command-line arguments or a JSON payload file. + +```bash +export AE_APP_KEY="your_app_key" +export AE_APP_SECRET="your_app_secret" +export AE_SESSION="active_session_token" + +dotnet run --project dotnet/AliExpressSdk.ConsoleHarness /auth/token/refresh refresh_token=xxxx +# or supply a payload file +dotnet run --project dotnet/AliExpressSdk.ConsoleHarness /auth/token/security/create --payload payload.json +``` + +Arguments passed as `key=value` are converted into request parameters. When `--payload` is supplied, the harness expects a JSON object whose properties are merged into the request. + +## 🛠️ Build, Test, and Packaging + +- `dotnet build AliExpressDotnetSdk.sln` builds the .NET port. +- `dotnet test AliExpressDotnetSdk.sln --collect "XPlat Code Coverage"` executes the xUnit suite and collects coverage. +- `dotnet pack dotnet/AliExpressSdk/AliExpressSdk.csproj -c Release` produces a NuGet package locally. + +For automated builds, the repository includes Azure DevOps pipeline definitions: + +| File | Purpose | +| ---- | ------- | +| `azure-pipelines.yml` | Restores, builds, tests, and packs the SDK, then publishes the NuGet package to the build artifact staging directory. | +| `azure-pipelines-coverage.yml` | Runs the test suite with default coverage settings and publishes both the test results and Cobertura coverage report. | + ## 📚 API Examples ### System Client diff --git a/azure-pipelines-coverage.yml b/azure-pipelines-coverage.yml new file mode 100644 index 0000000..93c2daa --- /dev/null +++ b/azure-pipelines-coverage.yml @@ -0,0 +1,39 @@ +trigger: none + +pool: + vmImage: 'windows-latest' + +variables: + buildConfiguration: 'Debug' + dotnetSdkVersion: '8.0.x' + solution: 'AliExpressDotnetSdk.sln' + +steps: +- task: UseDotNet@2 + displayName: 'Install .NET SDK' + inputs: + packageType: 'sdk' + version: '$(dotnetSdkVersion)' + +- script: dotnet restore $(solution) + displayName: 'Restore solution' + +- script: dotnet test $(solution) --configuration $(buildConfiguration) --logger "trx;LogFileName=TestResults.trx" --collect "XPlat Code Coverage" + displayName: 'Run tests with coverage' + +- task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '**/TestResults.trx' + searchFolder: '$(System.DefaultWorkingDirectory)' + testRunTitle: 'Coverage Tests' + failTaskOnFailedTests: true + +- task: PublishCodeCoverageResults@1 + displayName: 'Publish coverage report' + inputs: + codeCoverageTool: 'Cobertura' + summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.cobertura.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)' + failIfCoverageEmpty: true diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..d35228d --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,47 @@ +trigger: +- main + +pool: + vmImage: 'windows-latest' + +variables: + buildConfiguration: 'Release' + dotnetSdkVersion: '8.0.x' + packageProject: 'dotnet/AliExpressSdk/AliExpressSdk.csproj' + solution: 'AliExpressDotnetSdk.sln' + artifactName: 'nuget-package' + +steps: +- task: UseDotNet@2 + displayName: 'Install .NET SDK' + inputs: + packageType: 'sdk' + version: '$(dotnetSdkVersion)' + +- script: dotnet restore $(solution) + displayName: 'Restore solution' + +- script: dotnet build $(solution) --configuration $(buildConfiguration) --no-restore + displayName: 'Build solution' + +- script: dotnet test $(solution) --configuration $(buildConfiguration) --no-build --logger "trx;LogFileName=TestResults.trx" + displayName: 'Run tests' + +- task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'VSTest' + testResultsFiles: '**/TestResults.trx' + searchFolder: '$(System.DefaultWorkingDirectory)' + testRunTitle: 'Unit Tests' + failTaskOnFailedTests: true + +- script: dotnet pack $(packageProject) --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/nuget + displayName: 'Pack NuGet package' + +- task: PublishBuildArtifacts@1 + displayName: 'Publish NuGet package artifact' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)/nuget' + ArtifactName: '$(artifactName)' + publishLocation: 'Container' diff --git a/dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj b/dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj new file mode 100644 index 0000000..a8ebd3e --- /dev/null +++ b/dotnet/AliExpressSdk.ConsoleHarness/AliExpressSdk.ConsoleHarness.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/dotnet/AliExpressSdk.ConsoleHarness/Program.cs b/dotnet/AliExpressSdk.ConsoleHarness/Program.cs new file mode 100644 index 0000000..424c2a1 --- /dev/null +++ b/dotnet/AliExpressSdk.ConsoleHarness/Program.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using AliExpressSdk.Clients; + +if (args.Length == 0) +{ + PrintUsage(); + return; +} + +var configuration = ReadConfiguration(); +if (configuration == null) +{ + Console.Error.WriteLine("Required environment variables AE_APP_KEY, AE_APP_SECRET, or AE_SESSION are missing."); + Console.Error.WriteLine("Set them before running the harness."); + Environment.ExitCode = 1; + return; +} + +var (method, remainingArgs) = ParseMethod(args); +if (string.IsNullOrWhiteSpace(method)) +{ + Console.Error.WriteLine("No API method supplied."); + PrintUsage(); + Environment.ExitCode = 1; + return; +} + +var payloadFile = GetOptionValue("--payload", ref remainingArgs); +Dictionary parameters; +try +{ + parameters = BuildParameters(remainingArgs, payloadFile); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Failed to read parameters: {ex.Message}"); + Environment.ExitCode = 1; + return; +} + +var client = new AEBaseClient(configuration.Value.AppKey, configuration.Value.AppSecret, configuration.Value.Session); +var result = await client.CallApiDirectly(method!, parameters); + +if (result.Ok && result.Data is { } data) +{ + Console.WriteLine("Request succeeded."); + Console.WriteLine(JsonSerializer.Serialize(data, new JsonSerializerOptions + { + WriteIndented = true + })); +} +else +{ + Console.Error.WriteLine("Request failed."); + if (!string.IsNullOrWhiteSpace(result.Message)) + { + Console.Error.WriteLine($"Message: {result.Message}"); + } + + if (result.ErrorResponse is { } error) + { + Console.Error.WriteLine("AliExpress error response:"); + Console.Error.WriteLine(JsonSerializer.Serialize(error, new JsonSerializerOptions + { + WriteIndented = true + })); + } +} + +static (string? Method, string[] RemainingArgs) ParseMethod(string[] args) +{ + if (args.Length == 0) + { + return (null, Array.Empty()); + } + + return (args[0], args.Skip(1).ToArray()); +} + +static string? GetOptionValue(string optionName, ref string[] args) +{ + var remaining = new List(); + string? value = null; + + for (var i = 0; i < args.Length; i++) + { + if (args[i] == optionName) + { + if (i + 1 >= args.Length) + { + throw new ArgumentException($"The option {optionName} requires a value."); + } + + value = args[i + 1]; + i++; // Skip value + } + else + { + remaining.Add(args[i]); + } + } + + args = remaining.ToArray(); + return value; +} + +static Dictionary BuildParameters(IEnumerable args, string? payloadFile) +{ + var parameters = new Dictionary(StringComparer.Ordinal); + + if (!string.IsNullOrWhiteSpace(payloadFile)) + { + if (!File.Exists(payloadFile)) + { + throw new FileNotFoundException("Payload file not found.", payloadFile); + } + + using var payloadStream = File.OpenRead(payloadFile); + var payload = JsonSerializer.Deserialize>(payloadStream); + if (payload != null) + { + foreach (var kvp in payload) + { + parameters[kvp.Key] = kvp.Value; + } + } + } + + foreach (var argument in args) + { + var separatorIndex = argument.IndexOf('='); + if (separatorIndex < 1 || separatorIndex == argument.Length - 1) + { + throw new ArgumentException($"Could not parse parameter '{argument}'. Use the format key=value."); + } + + var key = argument[..separatorIndex]; + var value = argument[(separatorIndex + 1)..]; + parameters[key] = value; + } + + return parameters; +} + +static (string AppKey, string AppSecret, string Session)? ReadConfiguration() +{ + var appKey = Environment.GetEnvironmentVariable("AE_APP_KEY"); + var appSecret = Environment.GetEnvironmentVariable("AE_APP_SECRET"); + var session = Environment.GetEnvironmentVariable("AE_SESSION"); + + if (string.IsNullOrWhiteSpace(appKey) || string.IsNullOrWhiteSpace(appSecret) || string.IsNullOrWhiteSpace(session)) + { + return null; + } + + return (appKey, appSecret, session); +} + +static void PrintUsage() +{ + Console.WriteLine("AliExpress SDK Console Harness"); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(" dotnet run --project dotnet/AliExpressSdk.ConsoleHarness [key=value ...] [--payload payload.json]"); + Console.WriteLine(); + Console.WriteLine("Environment variables:"); + Console.WriteLine(" AE_APP_KEY - Your AliExpress app key."); + Console.WriteLine(" AE_APP_SECRET - Your AliExpress app secret."); + Console.WriteLine(" AE_SESSION - Active session token."); + Console.WriteLine(); + Console.WriteLine("Arguments:"); + Console.WriteLine(" method - API method path (e.g. /auth/token/create or aliexpress.solution.something)."); + Console.WriteLine(" key=value - Additional request parameters."); + Console.WriteLine(" --payload - Optional path to JSON file with additional parameters."); +}