Skip to content

Commit

Permalink
RSDK-9194: streamline app connection (viamrobotics#4516)
Browse files Browse the repository at this point in the history
  • Loading branch information
purplenicole730 authored Nov 15, 2024
1 parent 146d5c0 commit 8f47d2c
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 0 deletions.
68 changes: 68 additions & 0 deletions app/viam_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Package app contains all logic needed for communication and interaction with app.
package app

import (
"context"
"errors"
"net/url"
"strings"

"go.viam.com/utils/rpc"

"go.viam.com/rdk/logging"
)

// ViamClient is a gRPC client for method calls to Viam app.
type ViamClient struct {
conn rpc.ClientConn
}

// Options has the options necessary to connect through gRPC.
type Options struct {
baseURL string
entity string
credentials rpc.Credentials
}

var dialDirectGRPC = rpc.DialDirectGRPC

// CreateViamClientWithOptions creates a ViamClient with an Options struct.
func CreateViamClientWithOptions(ctx context.Context, options Options, logger logging.Logger) (*ViamClient, error) {
if options.baseURL == "" {
options.baseURL = "https://app.viam.com"
} else if !strings.HasPrefix(options.baseURL, "http://") && !strings.HasPrefix(options.baseURL, "https://") {
return nil, errors.New("use valid URL")
}
serviceHost, err := url.Parse(options.baseURL + ":443")
if err != nil {
return nil, err
}

if options.credentials.Payload == "" || options.entity == "" {
return nil, errors.New("entity and payload cannot be empty")
}
opts := rpc.WithEntityCredentials(options.entity, options.credentials)

conn, err := dialDirectGRPC(ctx, serviceHost.Host, logger, opts)
if err != nil {
return nil, err
}
return &ViamClient{conn: conn}, nil
}

// CreateViamClientWithAPIKey creates a ViamClient with an API key.
func CreateViamClientWithAPIKey(
ctx context.Context, options Options, apiKey, apiKeyID string, logger logging.Logger,
) (*ViamClient, error) {
options.entity = apiKeyID
options.credentials = rpc.Credentials{
Type: rpc.CredentialsTypeAPIKey,
Payload: apiKey,
}
return CreateViamClientWithOptions(ctx, options, logger)
}

// Close closes the gRPC connection.
func (c *ViamClient) Close() error {
return c.conn.Close()
}
118 changes: 118 additions & 0 deletions app/viam_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package app

import (
"context"
"testing"

"github.com/viamrobotics/webrtc/v3"
"go.viam.com/utils"
"go.viam.com/utils/rpc"
"google.golang.org/grpc"

"go.viam.com/rdk/logging"
)

var (
logger = logging.NewLogger("test")
defaultURL = "https://app.viam.com"
testAPIKey = "abcdefghijklmnopqrstuv0123456789"
testAPIKeyID = "abcd0123-ef45-gh67-ij89-klmnopqr01234567"
)

type MockConn struct{}

func (m *MockConn) NewStream(
ctx context.Context,
desc *grpc.StreamDesc,
method string,
opts ...grpc.CallOption,
) (grpc.ClientStream, error) {
return nil, nil
}

func (m *MockConn) Invoke(ctx context.Context, method string, args, reply any, opts ...grpc.CallOption) error {
return nil
}
func (m *MockConn) PeerConn() *webrtc.PeerConnection { return nil }
func (m *MockConn) Close() error { return nil }
func mockDialDirectGRPC(
ctx context.Context,
address string,
logger utils.ZapCompatibleLogger,
opts ...rpc.DialOption,
) (rpc.ClientConn, error) {
return &MockConn{}, nil
}

func TestCreateViamClientWithOptions(t *testing.T) {
urlTests := []struct {
name string
baseURL string
entity string
payload string
expectErr bool
}{
{"Default URL", defaultURL, testAPIKeyID, testAPIKey, false},
{"Default URL", defaultURL, "", "", true},
{"Default URL", defaultURL, "", testAPIKey, true},
{"Default URL", defaultURL, testAPIKeyID, "", true},
{name: "No URL", entity: testAPIKey, payload: testAPIKey, expectErr: false},
{"Empty URL", "", testAPIKeyID, testAPIKey, false},
{"Valid URL", "https://test.com", testAPIKeyID, testAPIKey, false},
{"Invalid URL", "test", testAPIKey, testAPIKey, true},
}
originalDialDirectGRPC := dialDirectGRPC
dialDirectGRPC = mockDialDirectGRPC
defer func() { dialDirectGRPC = originalDialDirectGRPC }()
for _, tt := range urlTests {
t.Run(tt.name, func(t *testing.T) {
opts := Options{
baseURL: tt.baseURL,
entity: tt.entity,
credentials: rpc.Credentials{
Type: rpc.CredentialsTypeAPIKey,
Payload: tt.payload,
},
}
client, err := CreateViamClientWithOptions(context.Background(), opts, logger)
if (err != nil) != tt.expectErr {
t.Errorf("Expected error: %v, got: %v", tt.expectErr, err)
}
if !tt.expectErr {
if client == nil {
t.Error("Expected a valid client, got nil")
} else {
client.Close()
}
}
})
}
}

func TestCreateViamClientWithAPIKeyTests(t *testing.T) {
apiKeyTests := []struct {
name string
apiKey string
apiKeyID string
expectErr bool
}{
{"Valid API Key", testAPIKey, testAPIKeyID, false},
{"Empty API Key", "", testAPIKeyID, true},
{"Empty API Key ID", testAPIKey, "", true},
}
for _, tt := range apiKeyTests {
t.Run(tt.name, func(t *testing.T) {
client, err := CreateViamClientWithAPIKey(context.Background(), Options{}, tt.apiKey, tt.apiKeyID, logger)
if (err != nil) != tt.expectErr {
t.Errorf("Expected error: %v, got: %v", tt.expectErr, err)
}
if !tt.expectErr {
if client == nil {
t.Error("Expected a valid client, got nil")
} else {
client.Close()
}
}
})
}
}

0 comments on commit 8f47d2c

Please sign in to comment.