aere-cloud-sdk/go/aerecloud_test.go

286 lines
11 KiB
Go

package aerecloud
// Probele clientului Go, pe un server httptest LOCAL: ce trimite clientul (metoda, cale, cheia, corpul, user-agent-ul) si ce
// face cu fiecare raspuns. Fiecare proba are perechea ei negativa; controlul negativ pe paznici e control-negativ.mjs.
// go test ./...
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
type vazut struct{ Method, Path, Key, Body, UA string }
type raspunsProba struct {
status int
body []byte
headers map[string]string
}
type server struct {
mu sync.Mutex
vazute []vazut
raspunsuri map[string]raspunsProba
srv *httptest.Server
}
func porneste(t *testing.T) (*server, *Client) {
s := &server{raspunsuri: map[string]raspunsProba{}}
s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
p := r.URL.EscapedPath()
if r.URL.RawQuery != "" {
p += "?" + r.URL.RawQuery
}
s.mu.Lock()
s.vazute = append(s.vazute, vazut{r.Method, p, r.Header.Get("x-api-key"), string(b), r.Header.Get("user-agent")})
rp, ok := s.raspunsuri[r.URL.EscapedPath()]
s.mu.Unlock()
if !ok {
rp = raspunsProba{200, []byte("{}"), nil}
}
for k, v := range rp.headers {
w.Header().Set(k, v)
}
w.WriteHeader(rp.status)
w.Write(rp.body)
}))
t.Cleanup(s.srv.Close)
c, err := NewWithBase("ak2800.0xAB.SINTETIC", s.srv.URL+"/v1", nil)
if err != nil {
t.Fatal(err)
}
return s, c
}
func (s *server) ultima() vazut { s.mu.Lock(); defer s.mu.Unlock(); return s.vazute[len(s.vazute)-1] }
func (s *server) pune(p string, st int, body []byte, h map[string]string) {
s.mu.Lock()
s.raspunsuri[p] = raspunsProba{st, body, h}
s.mu.Unlock()
}
var ctx = context.Background()
func TestCheiaEObligatorie(t *testing.T) {
if _, err := New(""); err == nil {
t.Fatal("o cheie goala nu a fost refuzata")
}
}
func TestReadinessCheiaNumaiLaAttest(t *testing.T) {
s, c := porneste(t)
if _, err := c.Readiness(ctx, "example.com", false, false); err != nil {
t.Fatal(err)
}
v := s.ultima()
if v.Method != "POST" || v.Path != "/v1/pq/readiness" || v.Key != "" || v.Body != `{"domain":"example.com"}` {
t.Fatalf("fara attest: %+v", v)
}
if v.UA != "aere-cloud-go/"+Version { // versiunea din cod, nu scrisa fix (2026-09-26: 1.5.2 a rupt literalul)
t.Fatalf("user-agent: %q", v.UA)
}
c.Readiness(ctx, "example.com", true, true)
v = s.ultima()
if v.Key != "ak2800.0xAB.SINTETIC" || v.Body != `{"attest":true,"domain":"example.com","fresh":true}` {
t.Fatalf("cu attest: %+v", v)
}
}
func TestPerimetruFinalitateDovada(t *testing.T) {
s, c := porneste(t)
f := false
c.ReadinessPerimeter(ctx, "example.com", PerimeterOptions{Hosts: []string{"api", `www"x`}, Discover: &f, Attest: true})
v := s.ultima()
var b map[string]any
if err := json.Unmarshal([]byte(v.Body), &b); err != nil || b["discover"] != false || b["attest"] != true || len(b["hosts"].([]any)) != 2 || v.Key == "" {
t.Fatalf("perimetru: %+v", v)
}
c.PQFinality(ctx, "latest", "testnet", "both")
if v = s.ultima(); v.Path != "/v1/pq/finality/latest?network=testnet&client=both" || v.Key != "" {
t.Fatalf("finalitate: %+v", v)
}
c.Proof(ctx, "0xab")
if v = s.ultima(); v.Path != "/v1/proof/0xab" || v.Key == "" {
t.Fatalf("dovada: %+v", v)
}
c.Verify(ctx, json.RawMessage(`{"statementHash":"0x00"}`), 28001)
if v = s.ultima(); v.Method != "POST" || v.Path != "/v1/verify?chain=28001" || v.Key == "" || v.Body != `{"statementHash":"0x00"}` {
t.Fatalf("verify: %+v", v)
}
c.Verify(ctx, json.RawMessage(`{}`), 0)
if v = s.ultima(); v.Path != "/v1/verify" {
t.Fatalf("verify fara lant: %+v", v)
}
}
var zi = []byte("{\"t\":\"2026-05-01T10:00:00.000Z\",\"m\":\"GET\",\"p\":\"/v1/account\",\"s\":200}\n")
func TestExportulZilei(t *testing.T) {
s, c := porneste(t)
d := sha256Hex(zi)
s.pune("/v1/account/audit", 200, zi, map[string]string{"x-aere-digest": d, "x-aere-day": "2026-05-01", "x-aere-entries": "1"})
e, err := c.AuditExport(ctx, "2026-05-01", "jsonl")
if err != nil || e.Verified == nil || !*e.Verified || e.Entries != 1 || e.Day != "2026-05-01" || string(e.Bytes) != string(zi) {
t.Fatalf("jsonl: %+v %v", e, err)
}
if v := s.ultima(); v.Path != "/v1/account/audit?format=jsonl&day=2026-05-01" {
t.Fatalf("cale: %s", v.Path)
}
s.pune("/v1/account/audit", 200, []byte("alti octeti"), map[string]string{"x-aere-digest": d})
if e, _ = c.AuditExport(ctx, "2026-05-01", "jsonl"); e.Verified == nil || *e.Verified {
t.Fatal("octeti straini nu au voie sa iasa verificati")
}
if e, _ = c.AuditExport(ctx, "2026-05-01", "cef"); e.Verified != nil {
t.Fatal("cef nu pretinde verificare")
}
s.pune("/v1/account/audit", 400, []byte(`{"error":"bad_format"}`), nil)
var ae *Error
if _, err = c.AuditExport(ctx, "", "xml"); !errors.As(err, &ae) || ae.Status != 400 {
t.Fatalf("400 neridicat: %v", err)
}
}
func TestExportulPastrat(t *testing.T) {
s, c := porneste(t)
d := sha256Hex(zi)
s.pune("/v1/account/audit/retention", 200, []byte(`{"retentionDays":90}`), nil)
if r, err := c.AuditRetention(ctx); err != nil || !strings.Contains(string(r), "90") || s.ultima().Method != "GET" || s.ultima().Key == "" {
t.Fatalf("retentie: %s %v", r, err)
}
piatra := `{"error":"audit_day_expired","expired":{"day":"2026-05-01","digest":"` + d + `","entries":1,"deletedAt":"2026-09-25T12:00:01.000Z","retentionDays":90}}`
s.pune("/v1/account/audit", 410, []byte(piatra), nil)
k, err := c.AuditVerifyKept(ctx, "2026-05-01", zi)
if err != nil || k.State != "expired" || !k.Matches || k.Digest != d || k.DeletedAt != "2026-09-25T12:00:01.000Z" {
t.Fatalf("piatra: %+v %v", k, err)
}
stricat := append([]byte{}, zi...)
stricat[5] ^= 1
if k, _ = c.AuditVerifyKept(ctx, "2026-05-01", stricat); k.Matches {
t.Fatal("un export modificat nu are voie sa se potriveasca cu piatra")
}
s.pune("/v1/account/audit", 200, zi, map[string]string{"x-aere-digest": d})
if k, _ = c.AuditVerifyKept(ctx, "2026-05-01", zi); k.State != "held" || !k.Matches {
t.Fatalf("ziua tinuta: %+v", k)
}
if k, _ = c.AuditVerifyKept(ctx, "2026-05-01", stricat); k.Matches {
t.Fatal("ziua tinuta: un export modificat nu se potriveste")
}
s.pune("/v1/account/audit", 429, []byte(`{"error":"rate_limit"}`), nil)
var ae *Error
if _, err = c.AuditVerifyKept(ctx, "2026-05-01", zi); !errors.As(err, &ae) || ae.Status != 429 {
t.Fatalf("un 429 intors ca nepotrivire: %v", err)
}
}
func TestRedirectareaNuEUrmata(t *testing.T) {
s, c := porneste(t)
// o redirectare spre alta cale a aceluiasi server: daca ar fi urmata, cheia ar ajunge acolo
s.pune("/v1/account", 302, nil, map[string]string{"location": s.srv.URL + "/in-alta-parte"})
var ae *Error
if _, err := c.Account(ctx); !errors.As(err, &ae) || ae.Status != 302 {
t.Fatalf("302 trebuia intors ca raspuns, nu urmat: %v", err)
}
for _, v := range s.vazute {
if strings.HasPrefix(v.Path, "/in-alta-parte") {
t.Fatal("redirectarea a fost urmata")
}
}
}
// 2026-09-29: verdictul Trust API hashuieste statement-ul in ordinea LUI de chei; clientul trimite octetii plicului asa cum sunt.
// Perechea negativa: acelasi plic trecut printr-un map de Go iese cu cheile sortate, adica alt text canonic.
func TestVerifyPastreazaOrdinea(t *testing.T) {
s, c := porneste(t)
plic := `{"v":1,"statement":{"v":1,"kind":"aere-proof-of-data","subject":{"name":"x"},"createdAt":"2026-09-26T00:00:00Z"},"statementHash":"0x00"}`
if _, err := c.Verify(context.Background(), json.RawMessage(plic), 0); err != nil {
t.Fatal(err)
}
if v := s.ultima(); v.Body != plic {
t.Fatalf("ordinea cheilor schimbata: %s", v.Body)
}
var m map[string]any
_ = json.Unmarshal([]byte(plic), &m)
prin, _ := json.Marshal(m)
if string(prin) == plic {
t.Fatal("controlul probei: un map ar fi trebuit sa sorteze cheile")
}
}
// 1.7.0: cererea de identitate pleaca pe ruta ei, cu cheie, cu prezentarea NESCHIMBATA (un numar peste 2^53 ramane exact)
func TestIdentityVerifyPastreazaOctetii(t *testing.T) {
s, c := porneste(t)
prez := `{"v":1,"kind":"aere-presentation","credential":{"statement":{"claims":{"id":12345678901234567891}}}}`
if _, err := c.IdentityVerify(context.Background(), IdentityVerifyRequest{Presentation: json.RawMessage(prez), Audience: "https://v.example", Nonce: "n-1", TrustedIssuers: []json.RawMessage{json.RawMessage(`"aere-id:00"`)}}); err != nil {
t.Fatal(err)
}
v := s.ultima()
if v.Method != "POST" || v.Path != "/v1/identity/verify" || v.Key == "" || !strings.Contains(v.Body, `"presentation":`+prez) || !strings.Contains(v.Body, `"trustedIssuers":["aere-id:00"]`) || strings.Contains(v.Body, "statusLists") {
t.Fatalf("cererea de identitate: %+v", v)
}
if _, err := c.ComplianceCheck(context.Background(), ComplianceCheckRequest{Presentation: json.RawMessage(prez), Policy: json.RawMessage(`{"id":"p"}`), Audience: "a", Nonce: "n", Record: true}); err != nil {
t.Fatal(err)
}
if v = s.ultima(); v.Path != "/v1/compliance/check" || v.Key == "" || !strings.Contains(v.Body, `"presentation":`+prez) || !strings.Contains(v.Body, `"record":true`) {
t.Fatalf("cererea de conformitate: %+v", v)
}
var m map[string]any
_ = json.Unmarshal([]byte(prez), &m)
prin, _ := json.Marshal(m)
if strings.Contains(string(prin), "12345678901234567891") {
t.Fatal("controlul probei: un map ar fi trebuit sa strice numarul mare")
}
}
func TestVerifyWebhook(t *testing.T) {
secret := "AERE-SINTETIC-secret"
corp := []byte(`{"event":"pq-anchor","data":{"height":42}}`)
m := hmac.New(sha256.New, []byte(secret))
m.Write(corp)
bun := hex.EncodeToString(m.Sum(nil))
if !VerifyWebhook(corp, bun, secret) {
t.Fatal("semnatura buna refuzata")
}
stricata := bun[:len(bun)-1] + map[bool]string{true: "b", false: "a"}[strings.HasSuffix(bun, "a")]
if VerifyWebhook(corp, stricata, secret) || VerifyWebhook(corp, bun+"x", secret) || VerifyWebhook(append(corp, ' '), bun, secret) {
t.Fatal("o semnatura gresita sau un corp atins a trecut")
}
}
// 1.8.0: cererile de plati pleaca pe rutele lor, cu cheie, cu politica si intrarile NESCHIMBATE (o suma peste 2^53 ramane exacta)
func TestPaymentsPastreazaOctetii(t *testing.T) {
s, c := porneste(t)
pol := `{"v":1,"kind":"aere-payment-risk-policy","id":"p"}`
in := `{"chainId":28001,"amount":12345678901234567891,"asset":"native"}`
if _, err := c.PaymentsRisk(context.Background(), PaymentsRiskRequest{Policy: json.RawMessage(pol), Inputs: json.RawMessage(in)}); err != nil {
t.Fatal(err)
}
v := s.ultima()
if v.Method != "POST" || v.Path != "/v1/payments/risk" || v.Key == "" || !strings.Contains(v.Body, `"inputs":`+in) || !strings.Contains(v.Body, `"policy":`+pol) {
t.Fatalf("cererea de risc: %+v", v)
}
if _, err := c.PaymentsReceipt(context.Background(), PaymentsReceiptRequest{Policy: json.RawMessage(pol), Inputs: json.RawMessage(in), TxHash: "0x11", Purpose: "order 7"}); err != nil {
t.Fatal(err)
}
if v = s.ultima(); v.Path != "/v1/payments/receipt" || v.Key == "" || !strings.Contains(v.Body, `"txHash":"0x11"`) || !strings.Contains(v.Body, `"purpose":"order 7"`) || !strings.Contains(v.Body, `"inputs":`+in) {
t.Fatalf("cererea de chitanta: %+v", v)
}
rec := `{"instruction":{"amount":"12345678901234567891"},"envelope":{"v":1}}`
if _, err := c.PaymentsVerify(context.Background(), PaymentsVerifyRequest{Receipt: json.RawMessage(rec), Policy: json.RawMessage(pol), Inputs: json.RawMessage(in)}); err != nil {
t.Fatal(err)
}
if v = s.ultima(); v.Path != "/v1/payments/verify" || v.Key == "" || !strings.Contains(v.Body, `"receipt":`+rec) {
t.Fatalf("cererea de verificare: %+v", v)
}
}