226 lines
6.2 KiB
Go
226 lines
6.2 KiB
Go
package sendtelemetry
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/cli/cli/v2/internal/barista/observability"
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/telemetry"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockTelemetryAPI struct {
|
|
request *observability.RecordEventsRequest
|
|
err error
|
|
}
|
|
|
|
func (m *mockTelemetryAPI) RecordEvents(_ context.Context, req *observability.RecordEventsRequest) (*observability.RecordEventsResponse, error) {
|
|
m.request = req
|
|
return &observability.RecordEventsResponse{}, m.err
|
|
}
|
|
|
|
func TestNewCmdSendTelemetry(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stdin string
|
|
env map[string]string
|
|
wantOpts SendTelemetryOptions
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "reads payload from stdin",
|
|
stdin: `{"events":[{"type":"usage","dimensions":{"command":"gh pr list"}}]}`,
|
|
wantOpts: SendTelemetryOptions{
|
|
TelemetryEndpointURL: defaultTelemetryEndpointURL,
|
|
PayloadJSON: `{"events":[{"type":"usage","dimensions":{"command":"gh pr list"}}]}`,
|
|
},
|
|
},
|
|
{
|
|
name: "uses GH_TELEMETRY_ENDPOINT_URL env var",
|
|
stdin: `{"events":[]}`,
|
|
env: map[string]string{"GH_TELEMETRY_ENDPOINT_URL": "https://custom.endpoint"},
|
|
wantOpts: SendTelemetryOptions{
|
|
TelemetryEndpointURL: "https://custom.endpoint",
|
|
PayloadJSON: `{"events":[]}`,
|
|
},
|
|
},
|
|
{
|
|
name: "defaults endpoint when env var not set",
|
|
stdin: `{}`,
|
|
wantOpts: SendTelemetryOptions{
|
|
TelemetryEndpointURL: defaultTelemetryEndpointURL,
|
|
PayloadJSON: `{}`,
|
|
},
|
|
},
|
|
{
|
|
name: "errors on empty stdin",
|
|
stdin: "",
|
|
wantErr: "no payload provided on stdin",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
for k, v := range tt.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
|
|
ios, _, _, _ := iostreams.Test()
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
Config: func() (gh.Config, error) {
|
|
return config.NewBlankConfig(), nil
|
|
},
|
|
}
|
|
|
|
var gotOpts *SendTelemetryOptions
|
|
cmd := newCmdSendTelemetry(f, func(opts *SendTelemetryOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{})
|
|
cmd.SetIn(strings.NewReader(tt.stdin))
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
|
|
_, err := cmd.ExecuteC()
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gotOpts)
|
|
assert.Equal(t, tt.wantOpts.TelemetryEndpointURL, gotOpts.TelemetryEndpointURL)
|
|
assert.Equal(t, tt.wantOpts.PayloadJSON, gotOpts.PayloadJSON)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunSendTelemetry(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
payload telemetry.SendTelemetryPayload
|
|
serverErr error
|
|
wantErr bool
|
|
assertFunc func(t *testing.T, req *observability.RecordEventsRequest)
|
|
}{
|
|
{
|
|
name: "posts single event to endpoint",
|
|
payload: telemetry.SendTelemetryPayload{
|
|
Events: []telemetry.PayloadEvent{
|
|
{
|
|
Type: "command_invocation",
|
|
Dimensions: map[string]string{
|
|
"command": "gh pr create",
|
|
"device_id": "abc123",
|
|
"os": "darwin",
|
|
},
|
|
Measures: map[string]int64{"duration_ms": 150},
|
|
},
|
|
},
|
|
},
|
|
assertFunc: func(t *testing.T, req *observability.RecordEventsRequest) {
|
|
t.Helper()
|
|
require.Len(t, req.Events, 1)
|
|
event := req.Events[0]
|
|
assert.Equal(t, "github-cli", event.App)
|
|
assert.Equal(t, "command_invocation", event.EventType)
|
|
assert.Equal(t, "gh pr create", event.Dimensions["command"])
|
|
assert.Equal(t, "abc123", event.Dimensions["device_id"])
|
|
assert.Equal(t, "darwin", event.Dimensions["os"])
|
|
},
|
|
},
|
|
{
|
|
name: "posts multiple events in single batch request",
|
|
payload: telemetry.SendTelemetryPayload{
|
|
Events: []telemetry.PayloadEvent{
|
|
{Type: "event1", Dimensions: map[string]string{"a": "1"}},
|
|
{Type: "event2", Dimensions: map[string]string{"b": "2"}},
|
|
},
|
|
},
|
|
assertFunc: func(t *testing.T, req *observability.RecordEventsRequest) {
|
|
t.Helper()
|
|
require.Len(t, req.Events, 2)
|
|
assert.Equal(t, "1", req.Events[0].Dimensions["a"])
|
|
assert.Equal(t, "2", req.Events[1].Dimensions["b"])
|
|
assert.Equal(t, "github-cli", req.Events[0].App)
|
|
assert.Equal(t, "event1", req.Events[0].EventType)
|
|
assert.Equal(t, "github-cli", req.Events[1].App)
|
|
assert.Equal(t, "event2", req.Events[1].EventType)
|
|
},
|
|
},
|
|
{
|
|
name: "empty events list produces no request",
|
|
payload: telemetry.SendTelemetryPayload{
|
|
Events: []telemetry.PayloadEvent{},
|
|
},
|
|
assertFunc: func(t *testing.T, req *observability.RecordEventsRequest) {
|
|
t.Helper()
|
|
assert.Nil(t, req)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mock := &mockTelemetryAPI{err: tt.serverErr}
|
|
handler := observability.NewTelemetryAPIServer(mock)
|
|
server := httptest.NewServer(handler)
|
|
defer server.Close()
|
|
|
|
opts := &SendTelemetryOptions{
|
|
TelemetryEndpointURL: server.URL,
|
|
PayloadJSON: mustMarshal(t, tt.payload),
|
|
}
|
|
|
|
err := runSendTelemetry(context.Background(), opts)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
if tt.assertFunc != nil {
|
|
tt.assertFunc(t, mock.request)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunSendTelemetryInvalidPayload(t *testing.T) {
|
|
err := runSendTelemetry(context.Background(), &SendTelemetryOptions{
|
|
TelemetryEndpointURL: "http://localhost:0",
|
|
PayloadJSON: "not-json",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestRunSendTelemetryServerError(t *testing.T) {
|
|
mock := &mockTelemetryAPI{err: assert.AnError}
|
|
handler := observability.NewTelemetryAPIServer(mock)
|
|
server := httptest.NewServer(handler)
|
|
defer server.Close()
|
|
|
|
err := runSendTelemetry(context.Background(), &SendTelemetryOptions{
|
|
TelemetryEndpointURL: server.URL,
|
|
PayloadJSON: `{"events":[{"type":"test","dimensions":{"a":"1"}}]}`,
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func mustMarshal(t *testing.T, v any) string {
|
|
t.Helper()
|
|
data, err := json.Marshal(v)
|
|
require.NoError(t, err)
|
|
return string(data)
|
|
}
|