183 lines
11 KiB
C#
183 lines
11 KiB
C#
// Probele clientului .NET (Aere.Cloud 1.6.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.6.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("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);
|
|
}
|
|
}
|