forked from viamrobotics/rdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RSDK-9194: streamline app connection (viamrobotics#4516)
- Loading branch information
1 parent
146d5c0
commit 8f47d2c
Showing
2 changed files
with
186 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
}) | ||
} | ||
} |