mirror of
https://github.com/vxcontrol/cloud.git
synced 2026-07-19 11:51:48 -04:00
642 lines
18 KiB
Go
642 lines
18 KiB
Go
package sdk
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var testLicenseKey = [10]byte{5, 214, 101, 133, 220, 47, 241, 139, 53, 124}
|
|
|
|
func ExampleBuild() {
|
|
var calls struct {
|
|
DownloadInstaller CallReqRespWriter
|
|
SendError CallReqBytesRespBytes
|
|
}
|
|
callsConfig := []CallConfig{
|
|
{
|
|
Calls: []any{&calls.DownloadInstaller},
|
|
Host: "localhost",
|
|
Name: "download-installer",
|
|
Path: "/api/v1/downloads/installer",
|
|
Method: CallMethodGET,
|
|
},
|
|
{
|
|
Calls: []any{&calls.SendError},
|
|
Host: "localhost",
|
|
Name: "send-error",
|
|
Path: "/api/v1/support/errors",
|
|
Method: CallMethodPOST,
|
|
},
|
|
}
|
|
if err := Build(callsConfig); err != nil {
|
|
panic(fmt.Sprintf("failed to build SDK: %v", err))
|
|
}
|
|
|
|
fmt.Println(calls.DownloadInstaller != nil)
|
|
fmt.Println(calls.SendError != nil)
|
|
// Output:
|
|
// true
|
|
// true
|
|
}
|
|
|
|
func TestBuildValidation(t *testing.T) {
|
|
tests := []struct {
|
|
config []CallConfig
|
|
wantErr bool
|
|
}{
|
|
{
|
|
config: []CallConfig{{
|
|
Calls: []any{new(CallReqRespBytes)},
|
|
Host: "api.example.com",
|
|
Name: "test",
|
|
Path: "/api/v1/test",
|
|
Method: CallMethodGET,
|
|
}},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
config: []CallConfig{{
|
|
Calls: []any{new(CallReqRespBytes)},
|
|
Name: "test",
|
|
Path: "/api/v1/test",
|
|
Method: CallMethodGET,
|
|
}},
|
|
wantErr: true, // missing host
|
|
},
|
|
{
|
|
config: []CallConfig{{
|
|
Calls: []any{new(CallReqBytesRespBytes)},
|
|
Host: "api.example.com",
|
|
Name: "test",
|
|
Path: "/api/v1/test",
|
|
Method: CallMethodGET,
|
|
}},
|
|
wantErr: true, // body with GET
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
err := Build(tt.config)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Build() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServerErrorParsing(t *testing.T) {
|
|
tests := []struct {
|
|
statusCode int
|
|
body []byte
|
|
wantErr error
|
|
}{
|
|
{200, []byte("success"), nil},
|
|
{502, []byte(`{"code":"BadGateway"}`), ErrBadGateway},
|
|
{429, []byte(`{"code":"TooManyRequestsRPM"}`), ErrTooManyRequestsRPM},
|
|
{500, []byte("invalid json"), ErrRequestFailed},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
err := parseServerError(tt.statusCode, nil, tt.body)
|
|
if tt.wantErr == nil {
|
|
if err != nil {
|
|
t.Errorf("parseServerError(%d) error = %v, wantErr nil", tt.statusCode, err)
|
|
}
|
|
} else {
|
|
if err == nil {
|
|
t.Errorf("parseServerError(%d) error = nil, wantErr %v", tt.statusCode, tt.wantErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseServerError_Blocked(t *testing.T) {
|
|
err := parseServerError(http.StatusForbidden, http.Header{}, []byte(`{"status":"error","code":"Blocked"}`))
|
|
if !errors.Is(err, ErrBlocked) {
|
|
t.Fatalf("errors.Is(ErrBlocked) failed for %v", err)
|
|
}
|
|
if errors.Is(err, ErrForbidden) {
|
|
t.Fatal("firewall block must be distinct from a tier/auth Forbidden")
|
|
}
|
|
}
|
|
|
|
func TestErrBlocked_IsFatal(t *testing.T) {
|
|
if isTemporaryError(ErrBlocked) {
|
|
t.Fatal("a firewall block must never be auto-retried")
|
|
}
|
|
}
|
|
|
|
func TestParseServerError_BlockedDistinctFromQuotaBlocked(t *testing.T) {
|
|
blocked := parseServerError(http.StatusForbidden, http.Header{}, []byte(`{"code":"Blocked"}`))
|
|
quota := parseServerError(http.StatusForbidden, http.Header{}, []byte(`{"code":"QuotaBlocked"}`))
|
|
if !errors.Is(blocked, ErrBlocked) {
|
|
t.Fatalf("errors.Is(ErrBlocked) failed for %v", blocked)
|
|
}
|
|
if !errors.Is(quota, ErrQuotaBlocked) {
|
|
t.Fatalf("errors.Is(ErrQuotaBlocked) failed for %v", quota)
|
|
}
|
|
if errors.Is(blocked, ErrQuotaBlocked) {
|
|
t.Fatal("ErrBlocked must not satisfy ErrQuotaBlocked")
|
|
}
|
|
}
|
|
|
|
func TestParseServerError_QuotaExceededDailyWithRetryAfter(t *testing.T) {
|
|
hdr := http.Header{}
|
|
hdr.Set("Retry-After", "3600")
|
|
body := []byte(`{"status":"error","code":"QuotaExceededDaily"}`)
|
|
|
|
err := parseServerError(http.StatusTooManyRequests, hdr, body)
|
|
|
|
if !errors.Is(err, ErrQuotaExceededDaily) {
|
|
t.Fatalf("errors.Is(ErrQuotaExceededDaily) failed for %v", err)
|
|
}
|
|
var qe *QuotaError
|
|
if !errors.As(err, &qe) {
|
|
t.Fatalf("errors.As(*QuotaError) failed for %v", err)
|
|
}
|
|
if qe.Scope != QuotaScopeDaily {
|
|
t.Fatalf("scope = %q, want %q", qe.Scope, QuotaScopeDaily)
|
|
}
|
|
if qe.RetryAfter != time.Hour {
|
|
t.Fatalf("retry-after = %s, want 1h", qe.RetryAfter)
|
|
}
|
|
}
|
|
|
|
func TestParseServerError_QuotaBlockedNoRetry(t *testing.T) {
|
|
body := []byte(`{"status":"error","code":"QuotaBlocked"}`)
|
|
err := parseServerError(http.StatusForbidden, nil, body)
|
|
|
|
if !errors.Is(err, ErrQuotaBlocked) {
|
|
t.Fatalf("errors.Is(ErrQuotaBlocked) failed for %v", err)
|
|
}
|
|
var qe *QuotaError
|
|
if !errors.As(err, &qe) {
|
|
t.Fatalf("errors.As(*QuotaError) failed for %v", err)
|
|
}
|
|
if qe.RetryAfter != 0 {
|
|
t.Fatalf("blocked retry-after = %s, want 0", qe.RetryAfter)
|
|
}
|
|
// blocked is fatal and never auto-retried
|
|
if isTemporaryError(err) {
|
|
t.Fatalf("QuotaBlocked must not be temporary")
|
|
}
|
|
}
|
|
|
|
func TestParseServerError_QuotaMonthlyIsFatal(t *testing.T) {
|
|
body := []byte(`{"status":"error","code":"QuotaExceededMonthly"}`)
|
|
err := parseServerError(http.StatusTooManyRequests, nil, body)
|
|
if !errors.Is(err, ErrQuotaExceededMonthly) {
|
|
t.Fatalf("errors.Is(ErrQuotaExceededMonthly) failed for %v", err)
|
|
}
|
|
if isTemporaryError(err) {
|
|
t.Fatalf("QuotaExceededMonthly must not be temporary (fatal, surfaced with cooldown)")
|
|
}
|
|
}
|
|
|
|
func TestParseServerError_RateLimitWithRetryAfter(t *testing.T) {
|
|
cases := []struct {
|
|
code string
|
|
sentinel error
|
|
scope RateLimitScope
|
|
temporary bool
|
|
}{
|
|
{"TooManyRequests", ErrTooManyRequests, RateLimitScopeGeneral, true},
|
|
{"TooManyRequestsRPM", ErrTooManyRequestsRPM, RateLimitScopeRPM, true},
|
|
{"TooManyRequestsRPH", ErrTooManyRequestsRPH, RateLimitScopeRPH, false},
|
|
{"TooManyRequestsRPD", ErrTooManyRequestsRPD, RateLimitScopeRPD, false},
|
|
}
|
|
for _, c := range cases {
|
|
hdr := http.Header{}
|
|
hdr.Set("Retry-After", "42")
|
|
body := []byte(`{"status":"error","code":"` + c.code + `"}`)
|
|
|
|
err := parseServerError(http.StatusTooManyRequests, hdr, body)
|
|
|
|
if !errors.Is(err, c.sentinel) {
|
|
t.Fatalf("%s: errors.Is(sentinel) failed for %v", c.code, err)
|
|
}
|
|
var rle *RateLimitError
|
|
if !errors.As(err, &rle) {
|
|
t.Fatalf("%s: errors.As(*RateLimitError) failed for %v", c.code, err)
|
|
}
|
|
if rle.Scope != c.scope {
|
|
t.Fatalf("%s: scope = %q, want %q", c.code, rle.Scope, c.scope)
|
|
}
|
|
if rle.RetryAfter != 42*time.Second {
|
|
t.Fatalf("%s: retry-after = %s, want 42s", c.code, rle.RetryAfter)
|
|
}
|
|
if isTemporaryError(err) != c.temporary {
|
|
t.Fatalf("%s: isTemporaryError = %v, want %v", c.code, isTemporaryError(err), c.temporary)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRateLimitError_NoHeader(t *testing.T) {
|
|
// Without a Retry-After header the wrapper still classifies correctly.
|
|
err := parseServerError(http.StatusTooManyRequests, nil, []byte(`{"code":"TooManyRequestsRPM"}`))
|
|
var rle *RateLimitError
|
|
if !errors.As(err, &rle) || rle.RetryAfter != 0 {
|
|
t.Fatalf("expected *RateLimitError with zero RetryAfter, got %v", err)
|
|
}
|
|
if !isTemporaryError(err) {
|
|
t.Fatalf("RPM should be temporary")
|
|
}
|
|
}
|
|
|
|
func TestCheck_EmptyConfigs(t *testing.T) {
|
|
results, err := Check(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("expected no top-level error for empty configs, got: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected empty results, got %d entries", len(results))
|
|
}
|
|
}
|
|
|
|
func TestCheck_InvalidConfigs(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
cfg CallConfig
|
|
wantIs error
|
|
}{
|
|
{
|
|
name: "missing host",
|
|
cfg: CallConfig{Name: "test-endpoint"},
|
|
wantIs: ErrInvalidConfiguration,
|
|
},
|
|
{
|
|
name: "missing name",
|
|
cfg: CallConfig{Host: "api.example.com"},
|
|
wantIs: ErrInvalidConfiguration,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
// key in results is always cfg.Name (may be "")
|
|
key := c.cfg.Name
|
|
|
|
results, err := Check(context.Background(), []CallConfig{c.cfg})
|
|
if err != nil {
|
|
t.Fatalf("unexpected top-level error: %v", err)
|
|
}
|
|
|
|
status, ok := results[key]
|
|
if !ok {
|
|
t.Fatalf("no result for key %q (results: %v)", key, results)
|
|
}
|
|
if status.LastError() == nil {
|
|
t.Error("expected per-endpoint error, got nil")
|
|
}
|
|
if !errors.Is(status.LastError(), c.wantIs) {
|
|
t.Errorf("errors.Is(%v) failed; got: %v", c.wantIs, status.LastError())
|
|
}
|
|
// IsReachable works via pointer receiver
|
|
if status.IsReachable() {
|
|
t.Error("expected IsReachable=false for an invalid config")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRetryAfterOf(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want time.Duration
|
|
}{
|
|
// no hint: returns 0
|
|
{"nil", nil, 0},
|
|
{"plain ErrForbidden", ErrForbidden, 0},
|
|
{"plain ErrBadRequest", ErrBadRequest, 0},
|
|
{"plain ErrTooManyRequestsRPM bare sentinel", ErrTooManyRequestsRPM, 0},
|
|
{"QuotaBlocked — never retry", &QuotaError{
|
|
Err: ErrQuotaBlocked, Scope: QuotaScopeBlocked}, 0,
|
|
},
|
|
{"RateLimitError with zero RetryAfter", &RateLimitError{
|
|
Err: ErrTooManyRequestsRPM, Scope: RateLimitScopeRPM, RetryAfter: 0}, 0,
|
|
},
|
|
|
|
// server hint carried in *RateLimitError
|
|
{"RPM with RetryAfter", &RateLimitError{
|
|
Err: ErrTooManyRequestsRPM, Scope: RateLimitScopeRPM, RetryAfter: 42 * time.Second}, 42 * time.Second,
|
|
},
|
|
{"RPH with RetryAfter", &RateLimitError{
|
|
Err: ErrTooManyRequestsRPH, Scope: RateLimitScopeRPH, RetryAfter: 3300 * time.Second}, 3300 * time.Second,
|
|
},
|
|
{"RPD with RetryAfter", &RateLimitError{
|
|
Err: ErrTooManyRequestsRPD, Scope: RateLimitScopeRPD, RetryAfter: 79200 * time.Second}, 79200 * time.Second,
|
|
},
|
|
{"general with RetryAfter", &RateLimitError{
|
|
Err: ErrTooManyRequests, Scope: RateLimitScopeGeneral, RetryAfter: 5 * time.Second}, 5 * time.Second,
|
|
},
|
|
|
|
// server hint carried in *QuotaError
|
|
{"QuotaExceededDaily with RetryAfter", &QuotaError{
|
|
Err: ErrQuotaExceededDaily, Scope: QuotaScopeDaily, RetryAfter: time.Hour}, time.Hour,
|
|
},
|
|
{"QuotaExceededMonthly with RetryAfter", &QuotaError{
|
|
Err: ErrQuotaExceededMonthly, Scope: QuotaScopeMonthly, RetryAfter: 720 * time.Hour}, 720 * time.Hour,
|
|
},
|
|
|
|
// wrapped via %w — errors.As still finds the inner type
|
|
{"wrapped RateLimitError", fmt.Errorf("request failed: %w", &RateLimitError{
|
|
Err: ErrTooManyRequestsRPM, Scope: RateLimitScopeRPM, RetryAfter: 7 * time.Second}), 7 * time.Second,
|
|
},
|
|
|
|
// context deadline joined with rate-limit error (our fix in calls.go)
|
|
{"context deadline + RateLimitError", fmt.Errorf("%w: %w", context.DeadlineExceeded, &RateLimitError{
|
|
Err: ErrTooManyRequestsRPM, Scope: RateLimitScopeRPM, RetryAfter: 55 * time.Second}), 55 * time.Second,
|
|
},
|
|
{"context deadline alone", fmt.Errorf("%w", context.DeadlineExceeded), 0},
|
|
|
|
// parseServerError produces these via http.Header with Retry-After
|
|
{"parseServerError RPM", parseServerError(429, func() http.Header {
|
|
h := http.Header{}
|
|
h.Set("Retry-After", "30")
|
|
return h
|
|
}(), []byte(`{"code":"TooManyRequestsRPM"}`)), 30 * time.Second},
|
|
{"parseServerError QuotaDaily", parseServerError(429, func() http.Header {
|
|
h := http.Header{}
|
|
h.Set("Retry-After", "3600")
|
|
return h
|
|
}(), []byte(`{"code":"QuotaExceededDaily"}`)), time.Hour},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := RetryAfterOf(tt.err)
|
|
if got != tt.want {
|
|
t.Errorf("RetryAfterOf() = %v, want %v (err = %v)", got, tt.want, tt.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRetryAfterOf_UsagePattern(t *testing.T) {
|
|
// Simulate what parseServerError returns for a real 429 response
|
|
h := http.Header{}
|
|
h.Set("Retry-After", "42")
|
|
err := parseServerError(429, h, []byte(`{"code":"TooManyRequestsRPM"}`))
|
|
|
|
// Caller does NOT need to know about *RateLimitError:
|
|
wait := RetryAfterOf(err)
|
|
if wait != 42*time.Second {
|
|
t.Fatalf("expected 42s, got %v", wait)
|
|
}
|
|
|
|
// errors.Is classification still works normally
|
|
if !errors.Is(err, ErrTooManyRequestsRPM) {
|
|
t.Fatal("errors.Is check failed")
|
|
}
|
|
}
|
|
|
|
func TestBuildCallValidation(t *testing.T) {
|
|
s := defaultSDK()
|
|
|
|
t.Run("method_body_compatibility", func(t *testing.T) {
|
|
// GET/DELETE should not have body calls
|
|
err := s.buildCall(CallConfig{
|
|
Host: "api.com",
|
|
Name: "test",
|
|
Path: "/test",
|
|
Method: CallMethodGET,
|
|
Calls: []any{new(CallReqBytesRespBytes)},
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for GET with body")
|
|
}
|
|
|
|
// POST/PUT/PATCH should not have query calls
|
|
err = s.buildCall(CallConfig{
|
|
Host: "api.com",
|
|
Name: "test",
|
|
Path: "/test",
|
|
Method: CallMethodPOST,
|
|
Calls: []any{new(CallReqQueryRespBytes)},
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for POST with query")
|
|
}
|
|
})
|
|
|
|
t.Run("path_args_validation", func(t *testing.T) {
|
|
// Path with args should use WithArgs call types
|
|
err := s.buildCall(CallConfig{
|
|
Host: "api.com",
|
|
Name: "test",
|
|
Path: "/users/:id",
|
|
Method: CallMethodGET,
|
|
Calls: []any{new(CallReqRespBytes)},
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for path args without WithArgs call type")
|
|
}
|
|
|
|
// Valid args configuration
|
|
err = s.buildCall(CallConfig{
|
|
Host: "api.com",
|
|
Name: "test",
|
|
Path: "/users/:id",
|
|
Method: CallMethodGET,
|
|
Calls: []any{new(CallReqWithArgsRespBytes)},
|
|
})
|
|
if err != nil {
|
|
t.Errorf("valid args config failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSDKInitialization(t *testing.T) {
|
|
sdk := defaultSDK()
|
|
|
|
if sdk.clientName != DefaultClientName || sdk.clientVersion != DefaultClientVersion {
|
|
t.Error("default client info incorrect")
|
|
}
|
|
|
|
if sdk.installationID == [16]byte{} {
|
|
t.Error("installation ID not generated")
|
|
}
|
|
|
|
if sdk.clientPublicKey != nil || sdk.clientPrivateKey != nil {
|
|
t.Error("NaCL keys should be nil before Build()")
|
|
}
|
|
|
|
// test options
|
|
WithClient("test", "2.0")(sdk)
|
|
if sdk.clientName != "test" {
|
|
t.Errorf("WithClient failed: got %s", sdk.clientName)
|
|
}
|
|
|
|
WithMaxRetries(3)(sdk)
|
|
if sdk.maxRetries != 3 {
|
|
t.Errorf("WithMaxRetries failed: got %d", sdk.maxRetries)
|
|
}
|
|
}
|
|
|
|
func TestPathTemplates(t *testing.T) {
|
|
sdk := defaultSDK()
|
|
|
|
t.Run("simple_path", func(t *testing.T) {
|
|
generator, argsCount, err := sdk.parsePath("/api/v1/test")
|
|
if err != nil {
|
|
t.Fatalf("parsePath error: %v", err)
|
|
}
|
|
if argsCount != 0 {
|
|
t.Errorf("expected 0 args, got %d", argsCount)
|
|
}
|
|
|
|
result, err := generator(nil)
|
|
if err != nil {
|
|
t.Errorf("generator error: %v", err)
|
|
}
|
|
if result != "/api/v1/test" {
|
|
t.Errorf("expected /api/v1/test, got %s", result)
|
|
}
|
|
})
|
|
|
|
t.Run("single_arg_path", func(t *testing.T) {
|
|
generator, argsCount, err := sdk.parsePath("/api/v1/users/:id")
|
|
if err != nil {
|
|
t.Fatalf("parsePath error: %v", err)
|
|
}
|
|
if argsCount != 1 {
|
|
t.Errorf("expected 1 arg, got %d", argsCount)
|
|
}
|
|
|
|
result, err := generator([]string{"123"})
|
|
if err != nil {
|
|
t.Errorf("generator error: %v", err)
|
|
}
|
|
if result != "/api/v1/users/123" {
|
|
t.Errorf("expected /api/v1/users/123, got %s", result)
|
|
}
|
|
|
|
// Test error cases
|
|
_, err = generator([]string{})
|
|
if err == nil {
|
|
t.Error("expected error for missing args")
|
|
}
|
|
|
|
_, err = generator([]string{"123", "456"})
|
|
if err == nil {
|
|
t.Error("expected error for too many args")
|
|
}
|
|
})
|
|
|
|
t.Run("multiple_args_path", func(t *testing.T) {
|
|
generator, argsCount, err := sdk.parsePath("/api/v1/users/:userId/posts/:postId")
|
|
if err != nil {
|
|
t.Fatalf("parsePath error: %v", err)
|
|
}
|
|
if argsCount != 2 {
|
|
t.Errorf("expected 2 args, got %d", argsCount)
|
|
}
|
|
|
|
result, err := generator([]string{"user123", "post456"})
|
|
if err != nil {
|
|
t.Errorf("generator error: %v", err)
|
|
}
|
|
if result != "/api/v1/users/user123/posts/post456" {
|
|
t.Errorf("expected /api/v1/users/user123/posts/post456, got %s", result)
|
|
}
|
|
})
|
|
|
|
t.Run("complex_path", func(t *testing.T) {
|
|
generator, argsCount, err := sdk.parsePath("/api/:version/users/:id/settings/:key")
|
|
if err != nil {
|
|
t.Fatalf("parsePath error: %v", err)
|
|
}
|
|
if argsCount != 3 {
|
|
t.Errorf("expected 3 args, got %d", argsCount)
|
|
}
|
|
|
|
result, err := generator([]string{"v2", "user123", "theme"})
|
|
if err != nil {
|
|
t.Errorf("generator error: %v", err)
|
|
}
|
|
expected := "/api/v2/users/user123/settings/theme"
|
|
if result != expected {
|
|
t.Errorf("expected %s, got %s", expected, result)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSDKOptions(t *testing.T) {
|
|
t.Run("all_options", func(t *testing.T) {
|
|
sdk := defaultSDK()
|
|
|
|
// Test all option functions
|
|
WithClient("TestApp", "1.2.3")(sdk)
|
|
if sdk.clientName != "TestApp" || sdk.clientVersion != "1.2.3" {
|
|
t.Error("WithClient option failed")
|
|
}
|
|
|
|
WithPowTimeout(45 * time.Second)(sdk)
|
|
if sdk.powTimeout != 45*time.Second {
|
|
t.Error("WithPowTimeout option failed")
|
|
}
|
|
|
|
WithMaxRetries(5)(sdk)
|
|
if sdk.maxRetries != 5 {
|
|
t.Error("WithMaxRetries option failed")
|
|
}
|
|
|
|
WithLicenseKey(encodeLicenseKey(testLicenseKey))(sdk)
|
|
if sdk.licenseKey != testLicenseKey || sdk.licenseFP != computeLicenseKeyFP(testLicenseKey) {
|
|
t.Error("WithLicenseKey option failed")
|
|
}
|
|
|
|
testID := [16]byte{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
|
|
WithInstallationID(testID)(sdk)
|
|
if sdk.installationID != testID {
|
|
t.Error("WithInstallationID option failed")
|
|
}
|
|
|
|
transport := DefaultTransport()
|
|
WithTransport(transport)(sdk)
|
|
if sdk.transport != transport {
|
|
t.Error("WithTransport option failed")
|
|
}
|
|
|
|
logger := DefaultLogger()
|
|
WithLogger(logger)(sdk)
|
|
if sdk.logger != logger {
|
|
t.Error("WithLogger option failed")
|
|
}
|
|
})
|
|
}
|
|
|
|
func encodeLicenseKey(key [10]byte) string {
|
|
alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234679"
|
|
expand := func(bkey [5]byte) [8]byte {
|
|
result := [8]byte{}
|
|
for idx := 4; idx >= 0; idx-- {
|
|
result[4-idx+3] = bkey[idx]
|
|
}
|
|
pkey := binary.BigEndian.Uint64(result[:])
|
|
for idx := range 8 {
|
|
result[idx] = byte((pkey >> (5 * idx)) & 0x1F)
|
|
}
|
|
return result
|
|
}
|
|
|
|
result := [19]byte{}
|
|
for idx, b := range expand([5]byte(key[0:5])) {
|
|
result[idx+idx/4] = alphabet[b]
|
|
}
|
|
for idx, b := range expand([5]byte(key[5:10])) {
|
|
result[idx+8+idx/4+2] = alphabet[b]
|
|
}
|
|
|
|
result[4] = '-'
|
|
result[9] = '-'
|
|
result[14] = '-'
|
|
|
|
return string(result[:])
|
|
}
|