...

Source file src/github.com/redhat-developer/odo/pkg/segment/segment_test.go

Documentation: github.com/redhat-developer/odo/pkg/segment

     1  package segment
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"os/user"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/golang/mock/gomock"
    19  	"github.com/sethvargo/go-envconfig"
    20  
    21  	"github.com/redhat-developer/odo/pkg/config"
    22  	"github.com/redhat-developer/odo/pkg/kclient"
    23  	"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
    24  
    25  	"github.com/redhat-developer/odo/pkg/log"
    26  	"github.com/redhat-developer/odo/pkg/preference"
    27  	scontext "github.com/redhat-developer/odo/pkg/segment/context"
    28  	"github.com/redhat-developer/odo/pkg/version"
    29  )
    30  
    31  type segmentResponse struct {
    32  	Batch []struct {
    33  		UserId    string `json:"userId"`
    34  		MessageId string `json:"messageId"`
    35  		Traits    struct {
    36  			OS string `json:"os"`
    37  		} `json:"traits"`
    38  		Properties struct {
    39  			Error         string `json:"error"`
    40  			ErrorType     string `json:"error-type"`
    41  			Success       bool   `json:"success"`
    42  			Version       string `json:"version"`
    43  			ComponentType string `json:"componentType"`
    44  			ClusterType   string `json:"clusterType"`
    45  		} `json:"properties"`
    46  		Type string `json:"type"`
    47  	} `json:"batch"`
    48  	MessageID string `json:"messageId"`
    49  }
    50  
    51  func mockServer() (chan []byte, *httptest.Server) {
    52  	done := make(chan []byte, 1)
    53  
    54  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    55  		defer r.Body.Close()
    56  		bin, err := io.ReadAll(r.Body)
    57  		if err != nil {
    58  			log.Error(err)
    59  			return
    60  		}
    61  		done <- bin
    62  	}))
    63  	return done, server
    64  }
    65  
    66  func TestClientUploadWithoutConsent(t *testing.T) {
    67  	body, server := mockServer()
    68  	defer server.Close()
    69  	defer close(body)
    70  
    71  	c, err := newCustomClient(createConfigDir(t), server.URL)
    72  	if err != nil {
    73  		t.Error(err)
    74  	}
    75  
    76  	testError := errors.New("error occurred")
    77  	uploadData := fakeTelemetryData("odo preference view", testError, context.Background())
    78  	// run a command, odo preference view
    79  	ctx := context.Background()
    80  	scontext.SetTelemetryStatus(ctx, false)
    81  	if err = c.Upload(ctx, uploadData); err != nil {
    82  		t.Error(err)
    83  	}
    84  
    85  	if err = c.Close(); err != nil {
    86  		t.Error(err)
    87  	}
    88  
    89  	select {
    90  	case x := <-body:
    91  		t.Errorf("server should not receive data: %q", x)
    92  	default:
    93  	}
    94  }
    95  
    96  func TestClientUploadWithConsent(t *testing.T) {
    97  	body, server := mockServer()
    98  	defer server.Close()
    99  	defer close(body)
   100  
   101  	tests := []struct {
   102  		cmd      string
   103  		testName string
   104  		err      error
   105  		success  bool
   106  		errType  string
   107  		version  string
   108  	}{
   109  		{
   110  			testName: "command ran successfully",
   111  			err:      nil,
   112  			success:  true,
   113  			errType:  "",
   114  			version:  version.VERSION,
   115  		},
   116  		{
   117  			testName: "command failed",
   118  			err:      errors.New("some error occurred"),
   119  			success:  false,
   120  			errType:  "*errors.errorString",
   121  			version:  version.VERSION,
   122  		},
   123  	}
   124  	for _, tt := range tests {
   125  		t.Log("Running test: ", tt.testName)
   126  		t.Run(tt.testName, func(t *testing.T) {
   127  			c, err := newCustomClient(createConfigDir(t), server.URL)
   128  			if err != nil {
   129  				t.Error(err)
   130  			}
   131  			uploadData := fakeTelemetryData("odo init", tt.err, context.Background())
   132  			// upload the data to Segment
   133  			ctx := scontext.NewContext(context.Background())
   134  			scontext.SetTelemetryStatus(ctx, true)
   135  			if err = c.Upload(ctx, uploadData); err != nil {
   136  				t.Error(err)
   137  			}
   138  			// segment.Client.SegmentClient uploads the data to server when a condition is met or when the connection is closed.
   139  			// This condition can be added by setting a BatchSize or Interval to the SegmentClient.
   140  			// BatchSize or Interval conditions have not been set for segment.Client.SegmentClient, so we will need to
   141  			// close the connection in order to upload the data to server.
   142  			// In case a condition is added, we can close the client in the teardown.
   143  			if err = c.Close(); err != nil {
   144  				t.Error(err)
   145  			}
   146  			// Note: This will need to be changed if segment.Client.SegmentClient has BatchSize or Interval set to something.
   147  			select {
   148  			case x := <-body:
   149  				s := segmentResponse{}
   150  				if err1 := json.Unmarshal(x, &s); err1 != nil {
   151  					t.Error(err1)
   152  				}
   153  				// Response returns 2 Batches in response -
   154  				// 1) identify - user's system information in case the server did not already have this information,
   155  				// and 2) track - information about the fired command
   156  				// This condition checks if both the responses were received
   157  				if s.Batch[0].Type != "identify" && s.Batch[1].Type != "track" {
   158  					t.Errorf("Missing Identify or Track information.\nIdentify: %v\nTrack:%v", s.Batch[0].Type, s.Batch[1].Type)
   159  				}
   160  				if s.Batch[0].Traits.OS != runtime.GOOS {
   161  					t.Error("OS does not match")
   162  				}
   163  				if !tt.success {
   164  					if s.Batch[1].Properties.Error != tt.err.Error() {
   165  						t.Error("Error does not match")
   166  					}
   167  				} else {
   168  					if s.Batch[1].Properties.Error != "" {
   169  						t.Error("Error does not match")
   170  					}
   171  				}
   172  				if s.Batch[1].Properties.Success != tt.success {
   173  					t.Error("Success does not match")
   174  				}
   175  				if s.Batch[1].Properties.ErrorType != tt.errType {
   176  					t.Error("Error Type does not match")
   177  				}
   178  				if !strings.Contains(s.Batch[1].Properties.Version, version.VERSION) {
   179  					t.Error("Odo version does not match")
   180  				}
   181  
   182  			default:
   183  				t.Error("Server should receive data")
   184  			}
   185  		})
   186  	}
   187  }
   188  
   189  func TestIsTelemetryEnabled(t *testing.T) {
   190  	type testStruct struct {
   191  		name                 string
   192  		env                  map[string]string
   193  		consentTelemetryPref bool
   194  		want                 func(odoDisableTelemetry, odoTrackingConsent string, consentTelemetry bool) bool
   195  	}
   196  	var tests []testStruct
   197  
   198  	// When there is no telemetry-related environment variable defined, rely on the ConsentTelemetry preference
   199  	for _, consentTelemetry := range []bool{true, false} {
   200  		consentTelemetry := consentTelemetry
   201  		tests = append(tests, testStruct{
   202  			name: fmt.Sprintf(
   203  				"ODO_DISABLE_TELEMETRY and ODO_TRACKING_CONSENT not set, consentTelemetry=%v", consentTelemetry),
   204  			consentTelemetryPref: consentTelemetry,
   205  			want: func(_, _ string, consentTelemetry bool) bool {
   206  				return consentTelemetry
   207  			},
   208  		})
   209  	}
   210  
   211  	// When only ODO_TRACKING_CONSENT is present in the env, it takes precedence over the ConsentTelemetry preference,
   212  	// unless it has an invalid value.
   213  	for _, odoTrackingConsent := range []string{"", "yes", "no", "bar"} {
   214  		for _, consentTelemetry := range []bool{true, false} {
   215  			odoTrackingConsent := odoTrackingConsent
   216  			consentTelemetry := consentTelemetry
   217  			tests = append(tests, testStruct{
   218  				name: fmt.Sprintf(
   219  					"ODO_DISABLE_TELEMETRY not set, ODO_TRACKING_CONSENT=%q, consentTelemetry=%v", odoTrackingConsent, consentTelemetry),
   220  				consentTelemetryPref: consentTelemetry,
   221  				env: map[string]string{
   222  					TrackingConsentEnv: odoTrackingConsent,
   223  				},
   224  				want: func(_, odoTrackingConsent string, consentTelemetry bool) bool {
   225  					// ODO_TRACKING_CONSENT takes precedence
   226  					switch odoTrackingConsent {
   227  					case "yes":
   228  						return true
   229  					case "no":
   230  						return false
   231  					default:
   232  						return consentTelemetry
   233  					}
   234  				},
   235  			})
   236  		}
   237  	}
   238  
   239  	// When only ODO_DISABLE_TELEMETRY is present in the env, it takes precedence over the ConsentTelemetry preference,
   240  	// only if ODO_DISABLE_TELEMETRY=true
   241  	for _, odoDisableTelemetry := range []string{"true", "false"} {
   242  		for _, consentTelemetry := range []bool{true, false} {
   243  			odoDisableTelemetry := odoDisableTelemetry
   244  			consentTelemetry := consentTelemetry
   245  			tests = append(tests, testStruct{
   246  				name: fmt.Sprintf("ODO_DISABLE_TELEMETRY=%q,ODO_TRACKING_CONSENT not set,ConsentTelemetry=%v",
   247  					odoDisableTelemetry, consentTelemetry),
   248  				env: map[string]string{
   249  					//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still want to test it
   250  					DisableTelemetryEnv: odoDisableTelemetry,
   251  				},
   252  				want: func(odoDisableTelemetry, _ string, consentTelemetry bool) bool {
   253  					if odoDisableTelemetry == "true" {
   254  						return false
   255  					}
   256  					//All other cases, we rely on the ConsentTelemetry preference
   257  					return consentTelemetry
   258  				},
   259  			})
   260  		}
   261  	}
   262  	//Cases where all the environment variables are there.
   263  	for _, odoDisableTelemetry := range []string{"true", "false"} {
   264  		for _, odoTrackingConsent := range []string{"", "yes", "no", "bar"} {
   265  			for _, consentTelemetry := range []bool{true, false} {
   266  				odoDisableTelemetry := odoDisableTelemetry
   267  				odoTrackingConsent := odoTrackingConsent
   268  				consentTelemetry := consentTelemetry
   269  				tests = append(tests, testStruct{
   270  					name: fmt.Sprintf("ODO_DISABLE_TELEMETRY=%q,ODO_TRACKING_CONSENT=%q,ConsentTelemetry=%v",
   271  						odoDisableTelemetry, odoTrackingConsent, consentTelemetry),
   272  					env: map[string]string{
   273  						//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still want to test it
   274  						DisableTelemetryEnv: odoDisableTelemetry,
   275  						TrackingConsentEnv:  odoTrackingConsent,
   276  					},
   277  					consentTelemetryPref: consentTelemetry,
   278  					want: func(odoDisableTelemetry, odoTrackingConsent string, consentTelemetry bool) bool {
   279  						if odoDisableTelemetry == "true" || odoTrackingConsent == "no" {
   280  							return false
   281  						}
   282  						if odoTrackingConsent == "yes" {
   283  							return true
   284  						}
   285  						return consentTelemetry
   286  					},
   287  				})
   288  			}
   289  		}
   290  	}
   291  
   292  	for _, tt := range tests {
   293  		t.Run(tt.name, func(t *testing.T) {
   294  			ctrl := gomock.NewController(t)
   295  			cfg := preference.NewMockClient(ctrl)
   296  			cfg.EXPECT().GetConsentTelemetry().Return(tt.consentTelemetryPref).AnyTimes()
   297  
   298  			envConfig, err := config.GetConfigurationWith(envconfig.MapLookuper(tt.env))
   299  			if err != nil {
   300  				t.Errorf("Get configuration fails: %v", err)
   301  			}
   302  			got := IsTelemetryEnabled(cfg, *envConfig)
   303  
   304  			//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still want to test it
   305  			want := tt.want(tt.env[DisableTelemetryEnv], tt.env[TrackingConsentEnv], tt.consentTelemetryPref)
   306  			if got != want {
   307  				t.Errorf(tt.name, "IsTelemetryEnabled: got %v, wanted %v. Env is set to %v. %s is set to %q.",
   308  					got, want, tt.env, preference.ConsentTelemetrySetting, tt.consentTelemetryPref)
   309  			}
   310  		})
   311  	}
   312  }
   313  
   314  func TestClientUploadWithContext(t *testing.T) {
   315  	var uploadData TelemetryData
   316  	body, server := mockServer()
   317  	defer server.Close()
   318  	defer close(body)
   319  
   320  	ctx := scontext.NewContext(context.Background())
   321  	scontext.SetTelemetryStatus(ctx, true)
   322  
   323  	for k, v := range map[string]string{scontext.ComponentType: "nodejs", scontext.ClusterType: ""} {
   324  		switch k {
   325  		case scontext.ComponentType:
   326  			scontext.SetComponentType(ctx, v)
   327  			uploadData = fakeTelemetryData("odo init", nil, ctx)
   328  		case scontext.ClusterType:
   329  			fakeClient, _ := kclient.FakeNew()
   330  			scontext.SetClusterType(ctx, fakeClient)
   331  			uploadData = fakeTelemetryData("odo set project", nil, ctx)
   332  		}
   333  		c, err := newCustomClient(createConfigDir(t), server.URL)
   334  		if err != nil {
   335  			t.Error(err)
   336  		}
   337  		// upload the data to Segment
   338  		if err = c.Upload(ctx, uploadData); err != nil {
   339  			t.Error(err)
   340  		}
   341  		if err = c.Close(); err != nil {
   342  			t.Error(err)
   343  		}
   344  		select {
   345  		case x := <-body:
   346  			s := segmentResponse{}
   347  			if err1 := json.Unmarshal(x, &s); err1 != nil {
   348  				t.Error(err1)
   349  			}
   350  			if s.Batch[1].Type == "identify" {
   351  				switch k {
   352  				case scontext.ComponentType:
   353  					if s.Batch[1].Properties.ComponentType != v {
   354  						t.Errorf("%v did not match. Want: %q Got: %q", scontext.ComponentType, v, s.Batch[1].Properties.ComponentType)
   355  					}
   356  				case scontext.ClusterType:
   357  					if s.Batch[1].Properties.ClusterType != v {
   358  						t.Errorf("%v did not match. Want: %q Got: %q", scontext.ClusterType, v, s.Batch[1].Properties.ClusterType)
   359  					}
   360  				}
   361  			}
   362  		default:
   363  			t.Error("Server should receive some data")
   364  		}
   365  	}
   366  }
   367  
   368  func TestSetError(t *testing.T) {
   369  	user, err := user.Current()
   370  	if err != nil {
   371  		t.Error(err.Error())
   372  	}
   373  
   374  	tests := []struct {
   375  		err    error
   376  		hasPII bool
   377  	}{
   378  		{
   379  			err:    errors.New("this is an error string"),
   380  			hasPII: false,
   381  		},
   382  		{
   383  			err:    fmt.Errorf("failed to execute devfile commands for component %s-comp. failed to Get https://my-cluster.project.local cannot run exec command [curl https://mycluster.domain.local -u foo -p password 123]", user.Username),
   384  			hasPII: true,
   385  		},
   386  	}
   387  
   388  	for _, tt := range tests {
   389  		var want string
   390  		got := SetError(tt.err)
   391  
   392  		// if error has PII, string returned by SetError must not be the same as the error since it was sanitized
   393  		// else it will be the same.
   394  		if tt.hasPII {
   395  			want = fmt.Sprintf("failed to execute devfile commands for component %s-comp. failed to Get %s cannot run exec command %s", Sanitizer, Sanitizer, Sanitizer)
   396  		} else {
   397  			want = tt.err.Error()
   398  		}
   399  		if got != want {
   400  			t.Errorf("got: %q\nwant:%q", got, want)
   401  		}
   402  
   403  	}
   404  }
   405  
   406  func Test_sanitizeExec(t *testing.T) {
   407  	err := fmt.Errorf("unable to execute the run command: unable to exec command [curl -K localhost:8080 -u user1 -p pwd123]")
   408  	got := sanitizeExec(err.Error())
   409  	want := fmt.Sprintf("unable to execute the run command: unable to exec command %s", Sanitizer)
   410  	if got != want {
   411  		t.Errorf("got: %q\nwant:%q", got, want)
   412  	}
   413  }
   414  
   415  func Test_sanitizeURL(t *testing.T) {
   416  	cases := []error{
   417  		fmt.Errorf("resource project validation check failed.: Get https://my-cluster.project.local request cancelled"),
   418  		fmt.Errorf("resource project validation check failed.: Get http://my-cluster.project.local request cancelled"),
   419  		fmt.Errorf("resource project validation check failed.: Get http://192.168.0.1:6443 request cancelled"),
   420  		fmt.Errorf("resource project validation check failed.: Get 10.18.25.1 request cancelled"),
   421  		fmt.Errorf("resource project validation check failed.: Get www.sample.com request cancelled"),
   422  	}
   423  
   424  	for _, err := range cases {
   425  		got := sanitizeURL(err.Error())
   426  		want := fmt.Sprintf("resource project validation check failed.: Get %s request cancelled", Sanitizer)
   427  		if got != want {
   428  			t.Errorf("got: %q\nwant:%q", got, want)
   429  		}
   430  	}
   431  }
   432  
   433  func Test_sanitizeFilePath(t *testing.T) {
   434  	unixPath := "/home/xyz/.odo/preference.yaml"
   435  	windowsPath := "C:\\User\\XYZ\\preference.yaml"
   436  
   437  	cases := []struct {
   438  		name string
   439  		err  error
   440  	}{
   441  		{
   442  			name: "filepath-unix",
   443  			err:  fmt.Errorf("cannot find the preference file at %s", unixPath),
   444  		},
   445  		{
   446  			name: "filepath-windows",
   447  			err:  fmt.Errorf("cannot find the preference file at %s", windowsPath),
   448  		},
   449  	}
   450  	for _, tt := range cases {
   451  		if tt.name == "filepath-windows" && os.Getenv("GOOS") != "windows" {
   452  			t.Skip("Cannot run a windows test on a unix system")
   453  		} else if tt.name == "filepath-unix" && os.Getenv("GOOS") != "linux" {
   454  			t.Skip("Cannot run a unix test on a windows system")
   455  		}
   456  
   457  		got := sanitizeFilePath(tt.err.Error())
   458  		want := fmt.Sprintf("cannot find the preference file at %s", Sanitizer)
   459  		if got != want {
   460  			t.Errorf("got: %q\nwant:%q", got, want)
   461  		}
   462  	}
   463  }
   464  
   465  func Test_sanitizeUserInfo(t *testing.T) {
   466  	user, err1 := user.Current()
   467  	if err1 != nil {
   468  		t.Error(err1.Error())
   469  	}
   470  
   471  	err := fmt.Errorf("cannot create component name with %s", user.Username)
   472  	got := sanitizeUserInfo(err.Error())
   473  	want := fmt.Sprintf("cannot create component name with %s", Sanitizer)
   474  	if got != want {
   475  		t.Errorf("got: %q\nwant:%q", got, want)
   476  	}
   477  }
   478  
   479  // createConfigDir creates a mock filesystem
   480  func createConfigDir(t *testing.T) string {
   481  	fs := filesystem.NewFakeFs()
   482  	configDir, err := fs.TempDir(os.TempDir(), "telemetry")
   483  	if err != nil {
   484  		t.Error(err)
   485  	}
   486  	return configDir
   487  }
   488  
   489  // fakeTelemetryData returns fake data to test segment client Upload
   490  func fakeTelemetryData(cmd string, err error, ctx context.Context) TelemetryData {
   491  	return TelemetryData{
   492  		Event: cmd,
   493  		Properties: TelemetryProperties{
   494  			Duration:      time.Second.Milliseconds(),
   495  			Error:         SetError(err),
   496  			ErrorType:     ErrorType(err),
   497  			Success:       err == nil,
   498  			Tty:           RunningInTerminal(),
   499  			Version:       version.VERSION,
   500  			CmdProperties: scontext.GetContextProperties(ctx),
   501  		},
   502  	}
   503  }
   504  

View as plain text