aere-cloud-sdk/csharp/Aere.Cloud.Tests/Program.cs
Aere Network 0fe1828f5b 1.7.0: identity and compliance verification in all five clients (identityVerify / complianceCheck; Go: IdentityVerify / ComplianceCheck with the presentation as its JSON bytes)
POST /v1/identity/verify: an AERE Identity presentation judged by the published command line -> VALID / PARTIAL / INVALID with every
row, the claims shown and the subject when no row failed, and the command that reproduces the verdict.
POST /v1/compliance/check: your policy judged on a presentation -> COMPLIANT / NOT_COMPLIANT with the reasons and, with record, the
decision as an AIP-23 envelope without personal data.
In Go the presentation, keys, lists and policy travel as json.RawMessage: a map or float64 would change numbers above 2^53 and the
canonical text the signatures cover. The .NET negative control now builds clean for each planted defect (an incremental build could
run the tests against the previous planted library). A negative control for the JavaScript and Python routes is added.

Tests: JavaScript 13, Python 14, Go ok, Java 10/10 (and the Java 8 API gate); negative controls Go 9/9, Java 10/10, .NET 12/12,
JavaScript and Python 4/4.
2026-09-30 12:10:59 +03:00

194 lines
12 KiB
C#

// Probele clientului .NET (Aere.Cloud 1.7.0), fara framework de probe: fiecare proba tipareste "--- PASS: Nume" sau "--- FAIL: Nume: motiv",
// iar codul de iesire e numarul de probe picate (0 = toate verzi). Aceleasi garzi ca la clientul Go: cheia obligatorie, cheia trimisa NUMAI
// unde trebuie, redirectarile NEurmate (dovedit pe un server local real, fiindca numai handlerul implicit al SDK-ului o poate arata),
// exportul jurnalului verificat pe octetii primiti, verificarea unei copii pastrate (tinuta / stearsa / eroare), webhook-ul in timp constant.
// Controlul negativ (control-negativ.sh) strica fiecare paznic intr-o COPIE a sursei si cere ca proba lui sa pice.
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Aere.Cloud;
const string Cheie = "ak2800.0x1111111111111111111111111111111111111111.secretproba";
int picate = 0, trecute = 0;
async Task Proba(string nume, Func<Task> f)
{
try { await f(); Console.WriteLine("--- PASS: " + nume); trecute++; }
catch (Exception e) { Console.WriteLine("--- FAIL: " + nume + ": " + e.Message.Split('\n')[0]); picate++; }
}
void Cer(bool c, string ce) { if (!c) throw new Exception("cerut: " + ce); }
string Sha(byte[] b) => "0x" + Convert.ToHexString(SHA256.HashData(b)).ToLowerInvariant();
(AereCloudClient, Ciot) Client(Func<HttpRequestMessage, HttpResponseMessage>? r = null)
{
var c = new Ciot { Raspunde = r ?? (_ => Json(200, "{}")) };
return (new AereCloudClient(Cheie, "https://ciot.invalid/v1", c), c);
}
HttpResponseMessage Json(int s, string corp) => new((HttpStatusCode)s) { Content = new StringContent(corp, Encoding.UTF8, "application/json") };
await Proba("CheiaEObligatorie", () =>
{
bool a = false, b = false;
try { _ = new AereCloudClient(""); } catch (ArgumentException) { a = true; }
try { _ = new AereCloudClient(null!); } catch (ArgumentException) { b = true; }
Cer(a && b, "o cheie goala sau lipsa e refuzata la constructie");
return Task.CompletedTask;
});
await Proba("ReadinessCheiaNumaiLaAttest", async () =>
{
var (c, ciot) = Client();
await c.ReadinessAsync("exemplu.test");
Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "fara attest cheia NU pleaca");
await c.ReadinessAsync("exemplu.test", attest: true);
Cer(ciot.Ultima.Antete.GetValueOrDefault("x-api-key") == Cheie, "cu attest cheia pleaca");
Cer(ciot.Ultima.Corp.Contains("\"attest\":true"), "corpul poarta attest");
await c.ReadinessReportAsync("exemplu.test"); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "raportul public fara cheie");
await c.PqFinalityAsync(); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "finalitatea AIP-21 fara cheie");
await c.HealthAsync(); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "health fara cheie");
await c.AccountAsync(); Cer(ciot.Ultima.Antete.GetValueOrDefault("x-api-key") == Cheie, "account cu cheie");
Cer(ciot.Ultima.Antete.GetValueOrDefault("user-agent", "").StartsWith("aere-cloud-csharp/1.7.0"), "user-agent-ul clientului");
});
await Proba("PerimetruFinalitateDovada", async () =>
{
var (c, ciot) = Client();
await c.ReadinessPerimeterAsync("exemplu.test", new PerimeterOptions(new[] { "www", "api" }, false, true));
using (var d = JsonDocument.Parse(ciot.Ultima.Corp))
{
var r = d.RootElement;
Cer(r.GetProperty("hosts").GetArrayLength() == 2 && !r.GetProperty("discover").GetBoolean() && r.GetProperty("attest").GetBoolean(), "corpul perimetrului");
}
Cer(ciot.Ultima.Antete.ContainsKey("x-api-key"), "perimetrul cere cheia");
await c.FinalityAsync(); Cer(ciot.Ultima.Url == "https://ciot.invalid/v1/data/finality" && ciot.Ultima.Metoda == "GET", "GET /data/finality");
await c.ProofAsync("0xab/c"); Cer(ciot.Ultima.Url.EndsWith("/proof/0xab%2Fc"), "hash-ul scapat in cale: " + ciot.Ultima.Url);
});
await Proba("TrustApiVerify", async () =>
{
var (c, ciot) = Client(_ => Json(200, "{\"verdict\":\"VALID\"}"));
var r = await c.VerifyAsync(new Dictionary<string, object> { ["kind"] = "data" }, 28001);
Cer(ciot.Ultima.Metoda == "POST" && ciot.Ultima.Url.EndsWith("/verify?chain=28001"), "POST /verify?chain=28001");
Cer(ciot.Ultima.Corp == "{\"kind\":\"data\"}", "plicul trimis ca atare: " + ciot.Ultima.Corp);
Cer(r.GetProperty("verdict").GetString() == "VALID", "verdictul intors");
await c.VerifyAsync(new Dictionary<string, object> { ["kind"] = "data" });
Cer(ciot.Ultima.Url.EndsWith("/verify"), "fara lant: /verify");
});
await Proba("IdentityCompliance", async () =>
{
var (c, ciot) = Client(_ => Json(200, "{\"verdict\":\"PARTIAL\"}"));
var r = await c.IdentityVerifyAsync(new Dictionary<string, object> { ["audience"] = "a", ["nonce"] = "n" });
Cer(ciot.Ultima.Metoda == "POST" && ciot.Ultima.Url.EndsWith("/v1/identity/verify") && ciot.Ultima.Antete.ContainsKey("x-api-key"), "POST /identity/verify cu cheie: " + ciot.Ultima.Url);
Cer(ciot.Ultima.Corp == "{\"audience\":\"a\",\"nonce\":\"n\"}", "cererea trimisa ca atare: " + ciot.Ultima.Corp);
Cer(r.GetProperty("verdict").GetString() == "PARTIAL", "verdictul intors");
await c.ComplianceCheckAsync(new Dictionary<string, object> { ["record"] = true });
Cer(ciot.Ultima.Url.EndsWith("/v1/compliance/check") && ciot.Ultima.Antete.ContainsKey("x-api-key") && ciot.Ultima.Corp == "{\"record\":true}", "POST /compliance/check: " + ciot.Ultima.Url + " " + ciot.Ultima.Corp);
});
await Proba("ExportulZilei", async () =>
{
var corp = Encoding.UTF8.GetBytes("{\"a\":1}\n{\"b\":2}\n");
string? digest = Sha(corp); string format = "jsonl";
var (c, _) = Client(_ =>
{
var m = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(corp) };
if (digest is not null) m.Headers.Add("x-aere-digest", digest);
m.Headers.Add("x-aere-day", "2026-09-27"); m.Headers.Add("x-aere-entries", "2");
return m;
});
var e = await c.AuditExportAsync("2026-09-27", format);
Cer(e.Verified == true && e.Entries == 2 && e.Day == "2026-09-27", "octetii se potrivesc cu amprenta declarata");
digest = "0x" + new string('0', 64);
e = await c.AuditExportAsync("2026-09-27", format); Cer(e.Verified == false, "o amprenta care nu e a octetilor: NEverificat");
digest = null;
e = await c.AuditExportAsync("2026-09-27", format); Cer(e.Verified == false, "fara amprenta: NEverificat");
e = await c.AuditExportAsync("2026-09-27", "cef"); Cer(e.Verified is null, "cef: nimic de verificat aici");
});
await Proba("ExportulPastrat", async () =>
{
var tinut = Encoding.UTF8.GetBytes("{\"x\":1}\n");
int stare = 200; string piatra = "";
var (c, _) = Client(_ => stare switch
{
200 => new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(tinut) },
410 => Json(410, piatra),
_ => Json(stare, "{\"error\":\"too many requests\"}"),
});
var k = await c.AuditVerifyKeptAsync("2026-09-01", tinut); Cer(k.State == "held" && k.Matches, "copia pastrata = ziua tinuta");
k = await c.AuditVerifyKeptAsync("2026-09-01", Encoding.UTF8.GetBytes("altceva")); Cer(k.State == "held" && !k.Matches, "o copie alterata NU se potriveste");
stare = 410; piatra = "{\"expired\":{\"digest\":\"" + Sha(tinut) + "\",\"deletedAt\":\"2026-09-20T00:00:00Z\"}}";
k = await c.AuditVerifyKeptAsync("2026-09-01", tinut); Cer(k.State == "expired" && k.Matches && k.DeletedAt == "2026-09-20T00:00:00Z", "ziua stearsa: piatra de mormant");
k = await c.AuditVerifyKeptAsync("2026-09-01", Encoding.UTF8.GetBytes("altceva")); Cer(k.State == "expired" && !k.Matches, "piatra de mormant nu se potriveste cu o copie alterata");
stare = 429; bool eroare = false;
try { await c.AuditVerifyKeptAsync("2026-09-01", tinut); } catch (AereCloudException x) { eroare = x.Status == 429; }
Cer(eroare, "un 429 e o EROARE, niciodata 'nu se potriveste'");
});
await Proba("RedirectareaNuEUrmata", async () =>
{
int Port() { var l = new TcpListener(IPAddress.Loopback, 0); l.Start(); var p = ((IPEndPoint)l.LocalEndpoint).Port; l.Stop(); return p; }
int pa = Port(), pb = Port(); int loviturib = 0; string? cheiaLaB = null;
using var a = new HttpListener(); a.Prefixes.Add($"http://127.0.0.1:{pa}/"); a.Start();
using var b = new HttpListener(); b.Prefixes.Add($"http://127.0.0.1:{pb}/"); b.Start();
var ta = Task.Run(async () => { while (a.IsListening) { HttpListenerContext x; try { x = await a.GetContextAsync(); } catch { break; } x.Response.StatusCode = 302; x.Response.RedirectLocation = $"http://127.0.0.1:{pb}/fura"; x.Response.Close(); } });
var tb = Task.Run(async () => { while (b.IsListening) { HttpListenerContext x; try { x = await b.GetContextAsync(); } catch { break; } loviturib++; cheiaLaB = x.Request.Headers["x-api-key"]; x.Response.StatusCode = 200; x.Response.Close(); } });
using var c = new AereCloudClient(Cheie, $"http://127.0.0.1:{pa}/v1"); // handlerul IMPLICIT al SDK-ului
int status = -1;
try { await c.AccountAsync(); } catch (AereCloudException x) { status = x.Status; }
await Task.Delay(200);
a.Stop(); b.Stop();
Cer(status == 302, "un 3xx e un raspuns (AereCloudException 302), nu un drum; primit " + status);
Cer(loviturib == 0 && cheiaLaB is null, "gazda din Location NU e contactata (lovituri " + loviturib + ")");
});
await Proba("VerifyWebhook", () =>
{
var corp = Encoding.UTF8.GetBytes("{\"event\":\"anchor\",\"height\":1}");
var sig = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes("secret"), corp)).ToLowerInvariant();
Cer(AereCloudClient.VerifyWebhook(corp, sig, "secret"), "semnatura buna trece");
var rea = sig[..^1] + (sig[^1] == '0' ? '1' : '0');
Cer(!AereCloudClient.VerifyWebhook(corp, rea, "secret"), "o cifra schimbata e refuzata");
Cer(!AereCloudClient.VerifyWebhook(corp, sig[..10], "secret"), "o semnatura scurta e refuzata");
Cer(!AereCloudClient.VerifyWebhook(corp, null, "secret"), "fara antet: refuzat");
Cer(!AereCloudClient.VerifyWebhook(corp, sig, "alt-secret"), "alt secret: refuzat");
return Task.CompletedTask;
});
await Proba("EroriSiBlockNumber", async () =>
{
var (c, _) = Client(r => r.RequestUri!.AbsolutePath.EndsWith("/rpc") ? Json(200, "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x10\"}") : Json(500, "boom"));
Cer(await c.BlockNumberAsync() == 16, "eth_blockNumber 0x10 = 16");
AereCloudException? e = null;
try { await c.DataHeadAsync(); } catch (AereCloudException x) { e = x; }
Cer(e is { Status: 500, Body: "boom", Path: "/data/head" }, "un 500 poarta statusul, corpul si calea");
var (c2, _) = Client(_ => throw new HttpRequestException("retea cazuta"));
AereCloudException? e2 = null;
try { await c2.HealthAsync(); } catch (AereCloudException x) { e2 = x; }
Cer(e2 is { Status: 0 } && !e2.Message.Contains(Cheie), "fara raspuns: status 0, iar mesajul nu poarta cheia");
var (c3, _) = Client(_ => Json(200, "{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-1}}"));
bool refuz = false; try { await c3.BlockNumberAsync(); } catch (AereCloudException x) { refuz = x.Status == 200; }
Cer(refuz, "un rezultat fara forma 0x e o eroare, nu un zero");
});
Console.WriteLine($"probe: {trecute} trecute, {picate} picate");
return picate;
sealed class Ciot : HttpMessageHandler
{
public sealed record Cerere(string Metoda, string Url, Dictionary<string, string> Antete, string Corp);
public List<Cerere> Cereri { get; } = new();
public Cerere Ultima => Cereri[^1];
public Func<HttpRequestMessage, HttpResponseMessage> Raspunde { get; set; } = _ => new HttpResponseMessage(HttpStatusCode.OK);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken ct)
{
var corp = r.Content is null ? "" : await r.Content.ReadAsStringAsync(ct);
var h = r.Headers.ToDictionary(x => x.Key.ToLowerInvariant(), x => string.Join(" ", x.Value));
Cereri.Add(new Cerere(r.Method.Method, r.RequestUri!.ToString(), h, corp));
return Raspunde(r);
}
}