...
This commit is contained in:
209
pkg/openrpcmanager/client/client.go
Normal file
209
pkg/openrpcmanager/client/client.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/freeflowuniverse/herolauncher/pkg/openrpcmanager"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrConnectionFailed = errors.New("failed to connect to OpenRPC server")
|
||||
ErrRequestFailed = errors.New("failed to send request to OpenRPC server")
|
||||
ErrResponseFailed = errors.New("failed to read response from OpenRPC server")
|
||||
ErrUnmarshalFailed = errors.New("failed to unmarshal response")
|
||||
ErrUnexpectedResponse = errors.New("unexpected response format")
|
||||
ErrRPCError = errors.New("RPC error")
|
||||
ErrAuthenticationFailed = errors.New("authentication failed")
|
||||
)
|
||||
|
||||
// RPCRequest represents an outgoing RPC request
|
||||
type RPCRequest struct {
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
ID int `json:"id"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
}
|
||||
|
||||
// RPCResponse represents an incoming RPC response
|
||||
type RPCResponse struct {
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *RPCError `json:"error,omitempty"`
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
}
|
||||
|
||||
// RPCError represents an RPC error
|
||||
type RPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Error returns a string representation of the RPC error
|
||||
func (e *RPCError) Error() string {
|
||||
if e.Data != nil {
|
||||
return fmt.Sprintf("RPC error %d: %s - %v", e.Code, e.Message, e.Data)
|
||||
}
|
||||
return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IntrospectionResponse represents the response from the rpc.introspect method
|
||||
type IntrospectionResponse struct {
|
||||
Logs []openrpcmanager.CallLog `json:"logs"`
|
||||
Total int `json:"total"`
|
||||
Filtered int `json:"filtered"`
|
||||
}
|
||||
|
||||
// Client is the interface that all OpenRPC clients must implement
|
||||
type Client interface {
|
||||
// Discover returns the OpenRPC schema
|
||||
Discover() (openrpcmanager.OpenRPCSchema, error)
|
||||
|
||||
// Introspect returns information about recent RPC calls
|
||||
Introspect(limit int, method string, status string) (IntrospectionResponse, error)
|
||||
|
||||
// Request sends a request to the OpenRPC server and returns the result
|
||||
Request(method string, params json.RawMessage, secret string) (interface{}, error)
|
||||
|
||||
// Close closes the client connection
|
||||
Close() error
|
||||
}
|
||||
|
||||
// BaseClient provides a base implementation of the Client interface
|
||||
type BaseClient struct {
|
||||
socketPath string
|
||||
secret string
|
||||
nextID int
|
||||
}
|
||||
|
||||
// NewClient creates a new OpenRPC client
|
||||
func NewClient(socketPath, secret string) *BaseClient {
|
||||
return &BaseClient{
|
||||
socketPath: socketPath,
|
||||
secret: secret,
|
||||
nextID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Discover returns the OpenRPC schema
|
||||
func (c *BaseClient) Discover() (openrpcmanager.OpenRPCSchema, error) {
|
||||
result, err := c.Request("rpc.discover", json.RawMessage("{}"), "")
|
||||
if err != nil {
|
||||
return openrpcmanager.OpenRPCSchema{}, err
|
||||
}
|
||||
|
||||
// Convert result to schema
|
||||
resultJSON, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return openrpcmanager.OpenRPCSchema{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err)
|
||||
}
|
||||
|
||||
var schema openrpcmanager.OpenRPCSchema
|
||||
if err := json.Unmarshal(resultJSON, &schema); err != nil {
|
||||
return openrpcmanager.OpenRPCSchema{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err)
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// Introspect returns information about recent RPC calls
|
||||
func (c *BaseClient) Introspect(limit int, method string, status string) (IntrospectionResponse, error) {
|
||||
// Create the params object
|
||||
params := struct {
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}{
|
||||
Limit: limit,
|
||||
Method: method,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
// Marshal the params
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return IntrospectionResponse{}, fmt.Errorf("failed to marshal introspection params: %v", err)
|
||||
}
|
||||
|
||||
// Make the request
|
||||
result, err := c.Request("rpc.introspect", paramsJSON, c.secret)
|
||||
if err != nil {
|
||||
return IntrospectionResponse{}, err
|
||||
}
|
||||
|
||||
// Convert result to introspection response
|
||||
resultJSON, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return IntrospectionResponse{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err)
|
||||
}
|
||||
|
||||
var response IntrospectionResponse
|
||||
if err := json.Unmarshal(resultJSON, &response); err != nil {
|
||||
return IntrospectionResponse{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Request sends a request to the OpenRPC server and returns the result
|
||||
func (c *BaseClient) Request(method string, params json.RawMessage, secret string) (interface{}, error) {
|
||||
// Connect to the Unix socket
|
||||
conn, err := net.Dial("unix", c.socketPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrConnectionFailed, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Create the request
|
||||
request := RPCRequest{
|
||||
Method: method,
|
||||
Params: params,
|
||||
ID: c.nextID,
|
||||
Secret: secret,
|
||||
JSONRPC: "2.0",
|
||||
}
|
||||
c.nextID++
|
||||
|
||||
// Marshal the request
|
||||
requestData, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
// Send the request
|
||||
_, err = conn.Write(requestData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrRequestFailed, err)
|
||||
}
|
||||
|
||||
// Read the response
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrResponseFailed, err)
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
var response RPCResponse
|
||||
if err := json.Unmarshal(buf[:n], &response); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err)
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if response.Error != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrRPCError, response.Error)
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (c *BaseClient) Close() error {
|
||||
// Nothing to do for the base client since we create a new connection for each request
|
||||
return nil
|
||||
}
|
283
pkg/openrpcmanager/client/client_test.go
Normal file
283
pkg/openrpcmanager/client/client_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/freeflowuniverse/herolauncher/pkg/openrpcmanager"
|
||||
)
|
||||
|
||||
// MockClient implements the Client interface for testing
|
||||
type MockClient struct {
|
||||
BaseClient
|
||||
}
|
||||
|
||||
// TestMethod is a test method that returns a greeting
|
||||
func (c *MockClient) TestMethod(name string) (string, error) {
|
||||
params := map[string]string{"name": name}
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := c.Request("test.method", paramsJSON, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Convert result to string
|
||||
greeting, ok := result.(string)
|
||||
if !ok {
|
||||
return "", ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
return greeting, nil
|
||||
}
|
||||
|
||||
// SecureMethod is a test method that requires authentication
|
||||
func (c *MockClient) SecureMethod() (map[string]interface{}, error) {
|
||||
result, err := c.Request("secure.method", json.RawMessage("{}"), c.secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert result to map
|
||||
data, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
// Create a temporary socket path
|
||||
tempDir, err := os.MkdirTemp("", "openrpc-client-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
socketPath := filepath.Join(tempDir, "openrpc.sock")
|
||||
secret := "test-secret"
|
||||
|
||||
// Create test schema and handlers
|
||||
schema := openrpcmanager.OpenRPCSchema{
|
||||
OpenRPC: "1.2.6",
|
||||
Info: openrpcmanager.InfoObject{
|
||||
Title: "Test API",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Methods: []openrpcmanager.MethodObject{
|
||||
{
|
||||
Name: "test.method",
|
||||
Params: []openrpcmanager.ContentDescriptorObject{
|
||||
{
|
||||
Name: "name",
|
||||
Schema: openrpcmanager.SchemaObject{"type": "string"},
|
||||
},
|
||||
},
|
||||
Result: &openrpcmanager.ContentDescriptorObject{
|
||||
Name: "result",
|
||||
Schema: openrpcmanager.SchemaObject{"type": "string"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "secure.method",
|
||||
Params: []openrpcmanager.ContentDescriptorObject{},
|
||||
Result: &openrpcmanager.ContentDescriptorObject{
|
||||
Name: "result",
|
||||
Schema: openrpcmanager.SchemaObject{"type": "object"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handlers := map[string]openrpcmanager.RPCHandler{
|
||||
"test.method": func(params json.RawMessage) (interface{}, error) {
|
||||
var request struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.Unmarshal(params, &request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return "Hello, " + request.Name + "!", nil
|
||||
},
|
||||
"secure.method": func(params json.RawMessage) (interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"secure": true,
|
||||
"data": "sensitive information",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create and start OpenRPC manager and Unix server
|
||||
manager, err := openrpcmanager.NewOpenRPCManager(schema, handlers, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create OpenRPCManager: %v", err)
|
||||
}
|
||||
|
||||
server, err := openrpcmanager.NewUnixServer(manager, socketPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create UnixServer: %v", err)
|
||||
}
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatalf("Failed to start UnixServer: %v", err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
// Wait for server to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Create client
|
||||
client := &MockClient{
|
||||
BaseClient: BaseClient{
|
||||
socketPath: socketPath,
|
||||
secret: secret,
|
||||
},
|
||||
}
|
||||
|
||||
// Test Discover method
|
||||
t.Run("Discover", func(t *testing.T) {
|
||||
schema, err := client.Discover()
|
||||
if err != nil {
|
||||
t.Fatalf("Discover failed: %v", err)
|
||||
}
|
||||
|
||||
if schema.OpenRPC != "1.2.6" {
|
||||
t.Errorf("Expected OpenRPC version 1.2.6, got: %s", schema.OpenRPC)
|
||||
}
|
||||
|
||||
if len(schema.Methods) < 2 {
|
||||
t.Errorf("Expected at least 2 methods, got: %d", len(schema.Methods))
|
||||
}
|
||||
|
||||
// Check if our test methods are in the schema
|
||||
foundTestMethod := false
|
||||
foundSecureMethod := false
|
||||
for _, method := range schema.Methods {
|
||||
if method.Name == "test.method" {
|
||||
foundTestMethod = true
|
||||
}
|
||||
if method.Name == "secure.method" {
|
||||
foundSecureMethod = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundTestMethod {
|
||||
t.Error("test.method not found in schema")
|
||||
}
|
||||
if !foundSecureMethod {
|
||||
t.Error("secure.method not found in schema")
|
||||
}
|
||||
})
|
||||
|
||||
// Test TestMethod
|
||||
t.Run("TestMethod", func(t *testing.T) {
|
||||
greeting, err := client.TestMethod("World")
|
||||
if err != nil {
|
||||
t.Fatalf("TestMethod failed: %v", err)
|
||||
}
|
||||
|
||||
expected := "Hello, World!"
|
||||
if greeting != expected {
|
||||
t.Errorf("Expected greeting %q, got: %q", expected, greeting)
|
||||
}
|
||||
})
|
||||
|
||||
// Test Introspect method
|
||||
t.Run("Introspect", func(t *testing.T) {
|
||||
// Make several requests to generate logs
|
||||
_, err := client.TestMethod("World")
|
||||
if err != nil {
|
||||
t.Fatalf("TestMethod failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.SecureMethod()
|
||||
if err != nil {
|
||||
t.Fatalf("SecureMethod failed: %v", err)
|
||||
}
|
||||
|
||||
// Test introspection
|
||||
response, err := client.Introspect(10, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Introspect failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify we have logs
|
||||
if response.Total < 2 {
|
||||
t.Errorf("Expected at least 2 logs, got: %d", response.Total)
|
||||
}
|
||||
|
||||
// Test filtering by method
|
||||
response, err = client.Introspect(10, "test.method", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Introspect with method filter failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify filtering works
|
||||
for _, log := range response.Logs {
|
||||
if log.Method != "test.method" {
|
||||
t.Errorf("Expected only test.method logs, got: %s", log.Method)
|
||||
}
|
||||
}
|
||||
|
||||
// Test filtering by status
|
||||
response, err = client.Introspect(10, "", "success")
|
||||
if err != nil {
|
||||
t.Fatalf("Introspect with status filter failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify status filtering works
|
||||
for _, log := range response.Logs {
|
||||
if log.Status != "success" {
|
||||
t.Errorf("Expected only success logs, got: %s", log.Status)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test SecureMethod with valid secret
|
||||
t.Run("SecureMethod", func(t *testing.T) {
|
||||
data, err := client.SecureMethod()
|
||||
if err != nil {
|
||||
t.Fatalf("SecureMethod failed: %v", err)
|
||||
}
|
||||
|
||||
secure, ok := data["secure"].(bool)
|
||||
if !ok || !secure {
|
||||
t.Errorf("Expected secure to be true, got: %v", data["secure"])
|
||||
}
|
||||
|
||||
sensitiveData, ok := data["data"].(string)
|
||||
if !ok || sensitiveData != "sensitive information" {
|
||||
t.Errorf("Expected data to be 'sensitive information', got: %v", data["data"])
|
||||
}
|
||||
})
|
||||
|
||||
// Test SecureMethod with invalid secret
|
||||
t.Run("SecureMethod with invalid secret", func(t *testing.T) {
|
||||
invalidClient := &MockClient{
|
||||
BaseClient: BaseClient{
|
||||
socketPath: socketPath,
|
||||
secret: "wrong-secret",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := invalidClient.SecureMethod()
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid secret, but got nil")
|
||||
}
|
||||
})
|
||||
|
||||
// Test non-existent method
|
||||
t.Run("Non-existent method", func(t *testing.T) {
|
||||
_, err := client.Request("non.existent", json.RawMessage("{}"), "")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent method, but got nil")
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user