164 lines
5.5 KiB
Go
164 lines
5.5 KiB
Go
package webingress
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDefaultFabricBinderBuildsSignedEnvelopeAndSendsIt(t *testing.T) {
|
|
signer := &recordingEnvelopeSigner{
|
|
signature: FabricEnvelopeSignature{KeyID: "node-key-1", Alg: "ed25519", Signature: "sig-1", SignedAt: "2026-05-17T00:00:02Z"},
|
|
}
|
|
sender := &recordingEnvelopeSender{
|
|
response: FabricResponse{StatusCode: http.StatusAccepted, Body: []byte(`{"accepted":true}`)},
|
|
}
|
|
binder := DefaultFabricBinder{Signer: signer, Sender: sender, Now: fixedEnvelopeNow}
|
|
|
|
response, err := binder.Forward(context.Background(), FabricRequest{
|
|
SchemaVersion: "rap.web_ingress.fabric_request.v1",
|
|
Method: "post",
|
|
Path: "/platform-admin/root",
|
|
Query: "tab=nodes",
|
|
Host: "admin.example.test",
|
|
ServiceType: "admin-ingress",
|
|
Scope: "platform",
|
|
ServiceClass: "platform_admin",
|
|
Headers: http.Header{
|
|
"X-Trace-Id": []string{"trace-b", "trace-a"},
|
|
"Authorization": []string{"Bearer should-not-forward"},
|
|
"X-Empty-Header": []string{" "},
|
|
},
|
|
Body: []byte(`{"hello":"world"}`),
|
|
ObservedAt: fixedNow(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Forward failed: %v", err)
|
|
}
|
|
if response.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("response = %+v", response)
|
|
}
|
|
if len(signer.canonical) == 0 {
|
|
t.Fatal("signer did not receive canonical envelope")
|
|
}
|
|
if !bytes.Equal(sender.envelope.Canonical, signer.canonical) {
|
|
t.Fatalf("sender canonical does not match signer canonical")
|
|
}
|
|
if sender.envelope.SchemaVersion != "rap.web_ingress.signed_fabric_service_channel_envelope.v1" {
|
|
t.Fatalf("signed schema = %q", sender.envelope.SchemaVersion)
|
|
}
|
|
if sender.envelope.Signature.KeyID != "node-key-1" || sender.envelope.Signature.Signature != "sig-1" {
|
|
t.Fatalf("signature = %+v", sender.envelope.Signature)
|
|
}
|
|
|
|
var canonical FabricServiceChannelEnvelope
|
|
if err := json.Unmarshal(signer.canonical, &canonical); err != nil {
|
|
t.Fatalf("decode canonical: %v", err)
|
|
}
|
|
if canonical.SchemaVersion != FabricServiceChannelEnvelopeSchema ||
|
|
canonical.RequestSchema != "rap.web_ingress.fabric_request.v1" ||
|
|
canonical.Method != http.MethodPost ||
|
|
canonical.Scope != "platform" ||
|
|
canonical.ServiceClass != "platform_admin" ||
|
|
canonical.BodyBase64 != "eyJoZWxsbyI6IndvcmxkIn0=" ||
|
|
canonical.ObservedAt != "2026-05-17T00:00:00Z" ||
|
|
canonical.EnvelopedAt != "2026-05-17T00:00:01Z" {
|
|
t.Fatalf("canonical envelope = %+v", canonical)
|
|
}
|
|
if got := canonical.Headers["X-Trace-Id"]; len(got) != 2 || got[0] != "trace-a" || got[1] != "trace-b" {
|
|
t.Fatalf("canonical headers = %#v", canonical.Headers)
|
|
}
|
|
if canonical.Headers["Authorization"] != nil || canonical.Headers["X-Empty-Header"] != nil {
|
|
t.Fatalf("unsafe/empty headers leaked: %#v", canonical.Headers)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFabricBinderRequiresSignerAndSender(t *testing.T) {
|
|
request := FabricRequest{Scope: "platform", ServiceClass: "platform_admin"}
|
|
|
|
_, err := (DefaultFabricBinder{Sender: &recordingEnvelopeSender{}}).Forward(context.Background(), request)
|
|
if !errors.Is(err, ErrFabricEnvelopeSignerRequired) {
|
|
t.Fatalf("signer error = %v", err)
|
|
}
|
|
|
|
_, err = (DefaultFabricBinder{Signer: &recordingEnvelopeSigner{}}).Forward(context.Background(), request)
|
|
if !errors.Is(err, ErrFabricEnvelopeSenderRequired) {
|
|
t.Fatalf("sender error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFabricBinderRequiresScopeAndServiceClass(t *testing.T) {
|
|
binder := DefaultFabricBinder{Signer: &recordingEnvelopeSigner{}, Sender: &recordingEnvelopeSender{}}
|
|
|
|
_, err := binder.Forward(context.Background(), FabricRequest{ServiceClass: "platform_admin"})
|
|
if !errors.Is(err, ErrFabricEnvelopeScopeRequired) {
|
|
t.Fatalf("scope error = %v", err)
|
|
}
|
|
|
|
_, err = binder.Forward(context.Background(), FabricRequest{Scope: "platform"})
|
|
if !errors.Is(err, ErrFabricEnvelopeClassRequired) {
|
|
t.Fatalf("class error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFabricBinderPropagatesSignerAndSenderFailures(t *testing.T) {
|
|
signerErr := errors.New("sign failed")
|
|
senderErr := errors.New("send failed")
|
|
request := FabricRequest{Scope: "platform", ServiceClass: "platform_admin"}
|
|
|
|
_, err := (DefaultFabricBinder{
|
|
Signer: &recordingEnvelopeSigner{err: signerErr},
|
|
Sender: &recordingEnvelopeSender{},
|
|
}).Forward(context.Background(), request)
|
|
if !errors.Is(err, signerErr) {
|
|
t.Fatalf("signer error = %v", err)
|
|
}
|
|
|
|
_, err = (DefaultFabricBinder{
|
|
Signer: &recordingEnvelopeSigner{},
|
|
Sender: &recordingEnvelopeSender{err: senderErr},
|
|
}).Forward(context.Background(), request)
|
|
if !errors.Is(err, senderErr) {
|
|
t.Fatalf("sender error = %v", err)
|
|
}
|
|
}
|
|
|
|
func fixedEnvelopeNow() time.Time {
|
|
return time.Date(2026, 5, 17, 0, 0, 1, 0, time.UTC)
|
|
}
|
|
|
|
type recordingEnvelopeSigner struct {
|
|
canonical []byte
|
|
signature FabricEnvelopeSignature
|
|
err error
|
|
}
|
|
|
|
func (s *recordingEnvelopeSigner) Sign(_ context.Context, canonical []byte) (FabricEnvelopeSignature, error) {
|
|
s.canonical = append([]byte{}, canonical...)
|
|
if s.err != nil {
|
|
return FabricEnvelopeSignature{}, s.err
|
|
}
|
|
if s.signature.KeyID == "" {
|
|
s.signature = FabricEnvelopeSignature{KeyID: "test-key", Alg: "ed25519", Signature: "test-signature"}
|
|
}
|
|
return s.signature, nil
|
|
}
|
|
|
|
type recordingEnvelopeSender struct {
|
|
envelope SignedFabricServiceChannelEnvelope
|
|
response FabricResponse
|
|
err error
|
|
}
|
|
|
|
func (s *recordingEnvelopeSender) Send(_ context.Context, envelope SignedFabricServiceChannelEnvelope) (FabricResponse, error) {
|
|
s.envelope = envelope
|
|
if s.err != nil {
|
|
return FabricResponse{}, s.err
|
|
}
|
|
return s.response, nil
|
|
}
|