9351ec1d2c
Every RPC failure surfaced as a GatewayError with no way to tell a rejected key from a missing scope, though docs/ClientLibrariesDesign.md requires the two be treated distinctly. GatewayError now matches the new sentinels ErrUnauthenticated and ErrPermissionDenied via errors.Is (by gRPC status code only, so it works through the secret-redaction wrapper) and exposes Code(). Tests: errors_auth_test.go — sentinel matching per code, Code(), and a bufconn AcknowledgeAlarm round trip proving the match survives the wire and the password stays redacted (both behavioural tests fail without GatewayError.Is).
86 lines
2.7 KiB
Go
86 lines
2.7 KiB
Go
package mxgateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// #198 — the gateway answers a bad key with Unauthenticated and a missing scope
|
|
// with PermissionDenied; callers must be able to tell them apart with errors.Is.
|
|
|
|
func TestGatewayErrorMatchesAuthSentinelsByStatusCode(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
unauthenticated bool
|
|
permissionDenied bool
|
|
}{
|
|
{"unauthenticated", status.Error(codes.Unauthenticated, "bad key"), true, false},
|
|
{"permission denied", status.Error(codes.PermissionDenied, "scope required"), false, true},
|
|
{"unavailable", status.Error(codes.Unavailable, "down"), false, false},
|
|
{"non-grpc", errors.New("plain"), false, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := fmt.Errorf("outer: %w", &GatewayError{Op: "invoke", Err: tc.err})
|
|
if got := errors.Is(err, ErrUnauthenticated); got != tc.unauthenticated {
|
|
t.Fatalf("errors.Is(ErrUnauthenticated) = %v, want %v", got, tc.unauthenticated)
|
|
}
|
|
if got := errors.Is(err, ErrPermissionDenied); got != tc.permissionDenied {
|
|
t.Fatalf("errors.Is(ErrPermissionDenied) = %v, want %v", got, tc.permissionDenied)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGatewayErrorCode(t *testing.T) {
|
|
if got := (&GatewayError{Err: status.Error(codes.PermissionDenied, "x")}).Code(); got != codes.PermissionDenied {
|
|
t.Fatalf("Code() = %v", got)
|
|
}
|
|
if got := (*GatewayError)(nil).Code(); got != codes.OK {
|
|
t.Fatalf("nil Code() = %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAuthSentinelsSurviveTheWireAndRedaction(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
code codes.Code
|
|
sentinel error
|
|
other error
|
|
}{
|
|
{codes.Unauthenticated, ErrUnauthenticated, ErrPermissionDenied},
|
|
{codes.PermissionDenied, ErrPermissionDenied, ErrUnauthenticated},
|
|
} {
|
|
t.Run(tc.code.String(), func(t *testing.T) {
|
|
fake := &fakeGatewayWithAlarms{acknowledgeError: status.Error(tc.code, "denied pw-secret")}
|
|
client, cleanup := newBufconnClientWithAlarms(t, fake)
|
|
defer cleanup()
|
|
|
|
_, err := client.AcknowledgeAlarm(context.Background(), &pb.AcknowledgeAlarmRequest{
|
|
AlarmFullReference: "Tank01.Level.HiHi",
|
|
OperatorUser: "alice",
|
|
OperatorPassword: "pw-secret",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("AcknowledgeAlarm() returned no error")
|
|
}
|
|
if !errors.Is(err, tc.sentinel) {
|
|
t.Fatalf("errors.Is(%v) = false for %v", tc.sentinel, err)
|
|
}
|
|
if errors.Is(err, tc.other) {
|
|
t.Fatalf("errors.Is(%v) = true for %v", tc.other, err)
|
|
}
|
|
if strings.Contains(err.Error(), "pw-secret") {
|
|
t.Fatalf("error leaked the operator password: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|