This commit is contained in:
2025-04-23 04:18:28 +02:00
parent 10a7d9bb6b
commit a16ac8f627
276 changed files with 85166 additions and 1 deletions

View 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
}

View 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")
}
})
}