package mxgateway import ( "context" "errors" "strings" "testing" pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated" ) // TestAdviseSupervisoryBuildsCommandAndExposesRawReply pins that the promoted // typed helper emits an ADVISE_SUPERVISORY command carrying the server/item // handles and returns the raw reply. func TestAdviseSupervisoryBuildsCommandAndExposesRawReply(t *testing.T) { fake := &fakeGatewayServer{ invokeReply: &pb.MxCommandReply{ SessionId: "session-1", Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY, ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK}, }, } client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") reply, err := session.AdviseSupervisoryRaw(context.Background(), 12, 34) if err != nil { t.Fatalf("AdviseSupervisoryRaw() error = %v", err) } if reply.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY { t.Fatalf("reply kind = %s", reply.GetKind()) } cmd := fake.invokeRequest.GetCommand() if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY { t.Fatalf("command kind = %s", cmd.GetKind()) } if cmd.GetAdviseSupervisory().GetServerHandle() != 12 || cmd.GetAdviseSupervisory().GetItemHandle() != 34 { t.Fatalf("advise-supervisory handles = (%d, %d), want (12, 34)", cmd.GetAdviseSupervisory().GetServerHandle(), cmd.GetAdviseSupervisory().GetItemHandle()) } // The error-returning wrapper drops the reply but must not error on success. if err := session.AdviseSupervisory(context.Background(), 12, 34); err != nil { t.Fatalf("AdviseSupervisory() error = %v", err) } } // TestWriteSecuredSurfacesNativeFailureWithoutPriorAuthenticate pins MXAccess // parity: WriteSecured issued without a preceding AuthenticateUser is rejected // natively, and the client surfaces that failure as a typed MxAccessError rather // than pre-validating or reordering. The write value is also kept out of the // surfaced error text. func TestWriteSecuredSurfacesNativeFailureWithoutPriorAuthenticate(t *testing.T) { hresult := int32(-2147024891) // E_ACCESSDENIED fake := &fakeGatewayServer{ invokeReply: &pb.MxCommandReply{ SessionId: "session-1", Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED, Hresult: &hresult, DiagnosticMessage: "WriteSecured requires a prior AuthenticateUser", ProtocolStatus: &pb.ProtocolStatus{ Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE, Message: "MXAccess failed", }, }, } client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") securedValue := "supersecret-payload" err := session.WriteSecured(context.Background(), 12, 34, 0, 0, StringValue(securedValue)) var mxErr *MxAccessError if !errors.As(err, &mxErr) { t.Fatalf("error %T does not support errors.As(*MxAccessError); err = %v", err, err) } if strings.Contains(err.Error(), securedValue) { t.Fatalf("surfaced error leaked the secured payload: %q", err.Error()) } // The command must carry the secured fields verbatim (parity: unaltered). cmd := fake.invokeRequest.GetCommand() if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED { t.Fatalf("command kind = %s", cmd.GetKind()) } if cmd.GetWriteSecured().GetValue().GetStringValue() != securedValue { t.Fatalf("wire value = %q, want %q", cmd.GetWriteSecured().GetValue().GetStringValue(), securedValue) } } // TestWriteSecuredRejectsNilValueWithoutRoundTrip pins the client-side required // guard, which never echoes the (absent) value. func TestWriteSecuredRejectsNilValue(t *testing.T) { fake := &fakeGatewayServer{} client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") if err := session.WriteSecured(context.Background(), 1, 2, 0, 0, nil); err == nil || !strings.Contains(err.Error(), "write-secured value is required") { t.Fatalf("WriteSecured(nil value) error = %v", err) } } // TestAuthenticateUserReturnsUserIDOnHappyPath pins the typed helper unpacking of // AuthenticateUserReply.user_id and that the credential is carried on the wire // but never surfaced. func TestAuthenticateUserReturnsUserIDOnHappyPath(t *testing.T) { fake := &fakeGatewayServer{ invokeReply: &pb.MxCommandReply{ SessionId: "session-1", Kind: pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER, ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK}, Payload: &pb.MxCommandReply_AuthenticateUser{ AuthenticateUser: &pb.AuthenticateUserReply{UserId: 4242}, }, }, } client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") userID, err := session.AuthenticateUser(context.Background(), 12, "operator", "hunter2-password") if err != nil { t.Fatalf("AuthenticateUser() error = %v", err) } if userID != 4242 { t.Fatalf("user id = %d, want 4242", userID) } cmd := fake.invokeRequest.GetCommand() if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER { t.Fatalf("command kind = %s", cmd.GetKind()) } if cmd.GetAuthenticateUser().GetVerifyUser() != "operator" { t.Fatalf("verify user = %q, want operator", cmd.GetAuthenticateUser().GetVerifyUser()) } if cmd.GetAuthenticateUser().GetVerifyUserPassword() != "hunter2-password" { t.Fatalf("password not carried to the wire verbatim") } } // TestAuthenticateUserScrubsCredentialFromSurfacedError proves the redaction // seam: even when the gateway diagnostic message echoes the credential back, the // surfaced error redacts it while the typed MxAccessError remains reachable via // errors.As. func TestAuthenticateUserScrubsCredentialFromSurfacedError(t *testing.T) { password := "hunter2-password" fake := &fakeGatewayServer{ invokeReply: &pb.MxCommandReply{ SessionId: "session-1", Kind: pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER, DiagnosticMessage: "authentication failed for password " + password, // Message is intentionally left empty so MxAccessError.Error() falls // through to the diagnostic message — the free-text field that could // otherwise echo the credential back to a caller's log. ProtocolStatus: &pb.ProtocolStatus{ Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE, }, }, } client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") _, err := session.AuthenticateUser(context.Background(), 12, "operator", password) if err == nil { t.Fatal("AuthenticateUser() returned no error on native failure") } if strings.Contains(err.Error(), password) { t.Fatalf("surfaced error leaked the credential: %q", err.Error()) } if !strings.Contains(err.Error(), redactedSecretMarker) { t.Fatalf("surfaced error missing redaction marker: %q", err.Error()) } var mxErr *MxAccessError if !errors.As(err, &mxErr) { t.Fatalf("redaction wrapper broke errors.As(*MxAccessError); err = %v", err) } } // TestAuthenticateUserRequiresVerifyUser pins the client-side required guard. func TestAuthenticateUserRequiresVerifyUser(t *testing.T) { fake := &fakeGatewayServer{} client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") if _, err := session.AuthenticateUser(context.Background(), 12, "", "pw"); err == nil || !strings.Contains(err.Error(), "verify user is required") { t.Fatalf("AuthenticateUser(empty user) error = %v", err) } } // TestSuspendActivateReturnStatus covers two Phase 2 helpers that unpack an // MxStatusProxy from their dedicated reply arms. func TestSuspendActivateReturnStatus(t *testing.T) { fake := &fakeGatewayServer{ invokeReply: &pb.MxCommandReply{ SessionId: "session-1", Kind: pb.MxCommandKind_MX_COMMAND_KIND_SUSPEND, ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK}, Payload: &pb.MxCommandReply_Suspend{ Suspend: &pb.SuspendReply{Status: &pb.MxStatusProxy{Success: 1, DiagnosticText: "suspended"}}, }, }, } client, cleanup := newBufconnClient(t, fake) defer cleanup() session := NewSessionForID(client, "session-1") status, err := session.Suspend(context.Background(), 12, 34) if err != nil { t.Fatalf("Suspend() error = %v", err) } if status.GetDiagnosticText() != "suspended" { t.Fatalf("status diagnostic = %q, want suspended", status.GetDiagnosticText()) } if fake.invokeRequest.GetCommand().GetSuspend().GetItemHandle() != 34 { t.Fatalf("suspend item handle = %d, want 34", fake.invokeRequest.GetCommand().GetSuspend().GetItemHandle()) } }