267 lines
7.2 KiB
Go
267 lines
7.2 KiB
Go
package tsnats
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const testDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
|
|
type mockRequester struct {
|
|
mu sync.Mutex
|
|
lastSubj string
|
|
lastData []byte
|
|
reply []byte
|
|
err error
|
|
}
|
|
|
|
func (m *mockRequester) Request(ctx context.Context, subj string, data []byte) ([]byte, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.lastSubj = subj
|
|
m.lastData = append([]byte(nil), data...)
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return m.reply, nil
|
|
}
|
|
|
|
type mockSubConn struct {
|
|
mu sync.Mutex
|
|
subj string
|
|
handler MessageHandler
|
|
unsub bool
|
|
}
|
|
|
|
func (m *mockSubConn) Subscribe(subj string, cb MessageHandler) (Subscription, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.subj = subj
|
|
m.handler = cb
|
|
return &mockSubscription{parent: m}, nil
|
|
}
|
|
|
|
type mockSubscription struct {
|
|
parent *mockSubConn
|
|
}
|
|
|
|
func (s *mockSubscription) Unsubscribe() error {
|
|
s.parent.mu.Lock()
|
|
defer s.parent.mu.Unlock()
|
|
s.parent.unsub = true
|
|
return nil
|
|
}
|
|
|
|
func TestSubjects(t *testing.T) {
|
|
if SubjectRequest != "verae.ts.request" {
|
|
t.Fatalf("SubjectRequest=%q", SubjectRequest)
|
|
}
|
|
if SubjectVerify != "verae.ts.verify" {
|
|
t.Fatalf("SubjectVerify=%q", SubjectVerify)
|
|
}
|
|
if SubjectReport != "verae.hash.report" {
|
|
t.Fatalf("SubjectReport=%q", SubjectReport)
|
|
}
|
|
if SubjectCompleted != "verae.ts.completed" {
|
|
t.Fatalf("SubjectCompleted=%q", SubjectCompleted)
|
|
}
|
|
}
|
|
|
|
func TestStamp_SendsJSONAndParsesReceiptList(t *testing.T) {
|
|
reply, _ := json.Marshal([]map[string]any{{
|
|
"receipt_id": "rid-1",
|
|
"hash": testDigest,
|
|
"scope": "global",
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"merkle": "m1",
|
|
}})
|
|
mock := &mockRequester{reply: reply}
|
|
c := NewClient(mock)
|
|
|
|
receipts, err := c.Stamp(context.Background(), testDigest, StampOptions{Scope: "global"})
|
|
if err != nil {
|
|
t.Fatalf("Stamp: %v", err)
|
|
}
|
|
if mock.lastSubj != SubjectRequest {
|
|
t.Fatalf("subject=%q want %q", mock.lastSubj, SubjectRequest)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(mock.lastData, &body); err != nil {
|
|
t.Fatalf("request json: %v", err)
|
|
}
|
|
if body["hash"] != testDigest || body["scope"] != "global" {
|
|
t.Fatalf("request body=%v", body)
|
|
}
|
|
if len(receipts) != 1 || receipts[0].ReceiptID != "rid-1" || receipts[0].Hash != testDigest {
|
|
t.Fatalf("receipts=%+v", receipts)
|
|
}
|
|
}
|
|
|
|
func TestStamp_NormalizesDigestAndRejectsInvalid(t *testing.T) {
|
|
reply, _ := json.Marshal(map[string]any{
|
|
"receipt_id": "rid-1",
|
|
"hash": testDigest,
|
|
"scope": "local",
|
|
"created_at": "t",
|
|
"merkle": "m",
|
|
})
|
|
mock := &mockRequester{reply: reply}
|
|
c := NewClient(mock)
|
|
|
|
_, err := c.Stamp(context.Background(), "sha256:"+strings.ToUpper(testDigest), StampOptions{Scope: "local"})
|
|
if err != nil {
|
|
t.Fatalf("Stamp: %v", err)
|
|
}
|
|
var body map[string]any
|
|
_ = json.Unmarshal(mock.lastData, &body)
|
|
if body["hash"] != testDigest {
|
|
t.Fatalf("normalized hash=%v", body["hash"])
|
|
}
|
|
|
|
beforeSubj, beforeData := mock.lastSubj, string(mock.lastData)
|
|
_, err = c.Stamp(context.Background(), "notadigest", StampOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected digest error")
|
|
}
|
|
if mock.lastSubj != beforeSubj || string(mock.lastData) != beforeData {
|
|
t.Fatal("invalid digest must not publish")
|
|
}
|
|
}
|
|
|
|
func TestStamp_IncludesBearerFields(t *testing.T) {
|
|
reply, _ := json.Marshal(map[string]any{
|
|
"receipt_id": "rid-1",
|
|
"hash": testDigest,
|
|
"scope": "global",
|
|
"created_at": "t",
|
|
"merkle": "m",
|
|
})
|
|
mock := &mockRequester{reply: reply}
|
|
c := NewClient(mock)
|
|
_, err := c.Stamp(context.Background(), testDigest, StampOptions{
|
|
Scope: "global",
|
|
Authorization: "tok-a",
|
|
AccessToken: "tok-b",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Stamp: %v", err)
|
|
}
|
|
var body map[string]any
|
|
_ = json.Unmarshal(mock.lastData, &body)
|
|
if body["authorization"] != "tok-a" || body["access_token"] != "tok-b" {
|
|
t.Fatalf("auth fields missing: %v", body)
|
|
}
|
|
}
|
|
|
|
func TestStamp_ErrorAndJobID(t *testing.T) {
|
|
mock := &mockRequester{reply: []byte(`{"error":"boom"}`)}
|
|
c := NewClient(mock)
|
|
_, err := c.Stamp(context.Background(), testDigest, StampOptions{Scope: "local"})
|
|
if err == nil || !strings.Contains(err.Error(), "boom") {
|
|
t.Fatalf("want boom error, got %v", err)
|
|
}
|
|
|
|
mock.reply = []byte(`{"jobId":"job-9"}`)
|
|
_, err = c.Stamp(context.Background(), testDigest, StampOptions{Scope: "local"})
|
|
if err == nil || !strings.Contains(err.Error(), "job-9") {
|
|
t.Fatalf("want pending jobId error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerify_ParsesReceiptAndInvalid(t *testing.T) {
|
|
mock := &mockRequester{reply: []byte(`{"receipt_id":"rid-1","hash":"` + testDigest + `","scope":"local","created_at":"t","merkle":"m"}`)}
|
|
c := NewClient(mock)
|
|
res, err := c.Verify(context.Background(), "rid-1", VerifyOptions{WantScope: "local"})
|
|
if err != nil {
|
|
t.Fatalf("Verify: %v", err)
|
|
}
|
|
if mock.lastSubj != SubjectVerify {
|
|
t.Fatalf("subject=%q", mock.lastSubj)
|
|
}
|
|
var body map[string]any
|
|
_ = json.Unmarshal(mock.lastData, &body)
|
|
if body["receipt_id"] != "rid-1" || body["want_scope"] != "local" {
|
|
t.Fatalf("body=%v", body)
|
|
}
|
|
if !res.Valid || res.Receipt == nil || res.Receipt.ReceiptID != "rid-1" {
|
|
t.Fatalf("result=%+v", res)
|
|
}
|
|
|
|
mock.reply = []byte(`{"valid":false,"error":"unknown"}`)
|
|
res, err = c.Verify(context.Background(), "missing", VerifyOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Verify invalid: %v", err)
|
|
}
|
|
if res.Valid || res.Error != "unknown" {
|
|
t.Fatalf("result=%+v", res)
|
|
}
|
|
}
|
|
|
|
func TestHashReport_ParsesReceiptsWrapper(t *testing.T) {
|
|
mock := &mockRequester{reply: []byte(`{"receipts":[{"receipt_id":"rid-1","hash":"` + testDigest + `","scope":"local","created_at":"t","merkle":"m"}]}`)}
|
|
c := NewClient(mock)
|
|
receipts, err := c.HashReport(context.Background(), testDigest, ReportOptions{OrgID: "org-1", WantScope: "local"})
|
|
if err != nil {
|
|
t.Fatalf("HashReport: %v", err)
|
|
}
|
|
if mock.lastSubj != SubjectReport {
|
|
t.Fatalf("subject=%q", mock.lastSubj)
|
|
}
|
|
var body map[string]any
|
|
_ = json.Unmarshal(mock.lastData, &body)
|
|
if body["hash"] != testDigest || body["org_id"] != "org-1" || body["want_scope"] != "local" {
|
|
t.Fatalf("body=%v", body)
|
|
}
|
|
if len(receipts) != 1 || receipts[0].ReceiptID != "rid-1" {
|
|
t.Fatalf("receipts=%+v", receipts)
|
|
}
|
|
}
|
|
|
|
func TestSubscribeCompleted(t *testing.T) {
|
|
sub := &mockSubConn{}
|
|
c := NewClient(&mockRequester{}, WithSubscriber(sub))
|
|
var got CompletedEvent
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
s, err := c.SubscribeCompleted(context.Background(), func(ev CompletedEvent) {
|
|
got = ev
|
|
wg.Done()
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubscribeCompleted: %v", err)
|
|
}
|
|
if sub.subj != SubjectCompleted {
|
|
t.Fatalf("subj=%q", sub.subj)
|
|
}
|
|
sub.handler([]byte(`{"jobId":"j1","receipt_id":"r1","hash":"` + testDigest + `"}`))
|
|
done := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("handler not called")
|
|
}
|
|
if got.JobID != "j1" || got.ReceiptID != "r1" || got.Hash != testDigest {
|
|
t.Fatalf("event=%+v", got)
|
|
}
|
|
if err := s.Unsubscribe(); err != nil {
|
|
t.Fatalf("Unsubscribe: %v", err)
|
|
}
|
|
if !sub.unsub {
|
|
t.Fatal("expected unsubscribe")
|
|
}
|
|
}
|
|
|
|
func TestDefaultTimeout(t *testing.T) {
|
|
c := NewClient(&mockRequester{})
|
|
if c.Timeout() != 2*time.Second {
|
|
t.Fatalf("timeout=%v", c.Timeout())
|
|
}
|
|
}
|