github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/cmd/ubuntu-report/main_test.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/spf13/cobra"
    16  
    17  	"github.com/ubuntu/ubuntu-report/internal/helper"
    18  )
    19  
    20  const (
    21  	expectedReportItem = `"Version":`
    22  	optOutJSON         = `{"OptOut": true}`
    23  )
    24  
    25  func TestShow(t *testing.T) {
    26  	helper.SkipIfShort(t)
    27  	a := helper.Asserter{T: t}
    28  	stdout, restoreStdout := helper.CaptureStdout(t)
    29  	defer restoreStdout()
    30  
    31  	cmd := generateRootCmd()
    32  	cmd.SetArgs([]string{"show"})
    33  
    34  	var c *cobra.Command
    35  	cmdErrs := helper.RunFunctionWithTimeout(t, func() error {
    36  		var err error
    37  		c, err = cmd.ExecuteC()
    38  		restoreStdout() // close stdout to release ReadAll()
    39  		return err
    40  	})
    41  
    42  	if err := <-cmdErrs; err != nil {
    43  		t.Fatal("got an error when expecting none:", err)
    44  	}
    45  	a.Equal(c.Name(), "show")
    46  	got, err := ioutil.ReadAll(stdout)
    47  	if err != nil {
    48  		t.Error("couldn't read from stdout", err)
    49  	}
    50  	if !strings.Contains(string(got), expectedReportItem) {
    51  		t.Errorf("Expected %s to be in output, but got: %s", expectedReportItem, string(got))
    52  	}
    53  }
    54  
    55  // Test Verbosity level with Show
    56  func TestVerbosity(t *testing.T) {
    57  	helper.SkipIfShort(t)
    58  
    59  	testCases := []struct {
    60  		verbosity string
    61  	}{
    62  		{""},
    63  		{"-v"},
    64  		{"-vv"},
    65  	}
    66  	for _, tc := range testCases {
    67  		tc := tc // capture range variable for parallel execution
    68  		t.Run("verbosity level "+tc.verbosity, func(t *testing.T) {
    69  			a := helper.Asserter{T: t}
    70  			out, restoreLogs := helper.CaptureLogs(t)
    71  			defer restoreLogs()
    72  
    73  			cmd := generateRootCmd()
    74  			args := []string{"show"}
    75  			if tc.verbosity != "" {
    76  				args = append(args, tc.verbosity)
    77  			}
    78  			cmd.SetArgs(args)
    79  
    80  			cmdErrs := helper.RunFunctionWithTimeout(t, func() error {
    81  				var err error
    82  				_, err = cmd.ExecuteC()
    83  				restoreLogs() // send EOF to log to release io.Copy()
    84  				return err
    85  			})
    86  
    87  			var got bytes.Buffer
    88  			io.Copy(&got, out)
    89  
    90  			if err := <-cmdErrs; err != nil {
    91  				t.Fatal("got an error when expecting none:", err)
    92  			}
    93  
    94  			switch tc.verbosity {
    95  			case "":
    96  				a.Equal(got.String(), "")
    97  			case "-v":
    98  				// empty logs, apart info on dcd, installer or upgrade telemetry (file can be missing)
    99  				// and other GPU, screen and autologin that you won't have in Travis CI.
   100  				scanner := bufio.NewScanner(bytes.NewReader(got.Bytes()))
   101  				for scanner.Scan() {
   102  					l := scanner.Text()
   103  					if strings.Contains(l, "level=info") {
   104  						allowedLog := false
   105  						for _, msg := range []string{"/telemetry", "DCD", "GPU info", "Disk info", "Screen info", "CPU info", "autologin information", "/sys/class/dmi/id/", "hwcap"} {
   106  							if strings.Contains(l, msg) {
   107  								allowedLog = true
   108  							}
   109  						}
   110  						if allowedLog {
   111  							continue
   112  						}
   113  						t.Errorf("Expected no log output with -v apart from missing telemetry, GPU, Disk, Screen, sys and autologin information, but got: %s", l)
   114  					}
   115  				}
   116  			case "-vv":
   117  				if !strings.Contains(got.String(), "level=debug") {
   118  					t.Errorf("Expected some debug log to be printed, but got: %s", got.String())
   119  				}
   120  			}
   121  		})
   122  	}
   123  }
   124  
   125  func TestSend(t *testing.T) {
   126  	helper.SkipIfShort(t)
   127  
   128  	testCases := []struct {
   129  		name   string
   130  		answer string
   131  
   132  		shouldHitServer bool
   133  		wantErr         bool
   134  	}{
   135  		{"regular report auto", "yes", true, false},
   136  		{"regular report opt-out", "no", true, false},
   137  		{"dist-upgrade report", "upgrade", true, false},
   138  	}
   139  	for _, tc := range testCases {
   140  		tc := tc // capture range variable for parallel execution
   141  		t.Run(tc.name, func(t *testing.T) {
   142  			a := helper.Asserter{T: t}
   143  
   144  			out, tearDown := helper.TempDir(t)
   145  			defer tearDown()
   146  			defer helper.ChangeEnv("XDG_CACHE_HOME", out)()
   147  			out = filepath.Join(out, "ubuntu-report")
   148  			// create a previous report with fake json data (which isn't optout)
   149  			if err := os.MkdirAll(out, 0700); err != nil {
   150  				t.Fatalf("couldn't create ubuntu-report directory: %v", err)
   151  			}
   152  			if err := ioutil.WriteFile(filepath.Join(out, "ubuntu.10.10"), []byte(`{ "some-opt-in-data': true}`), 0644); err != nil {
   153  				t.Fatalf("couldn't setup previous report file: %v", err)
   154  			}
   155  
   156  			// we don't really care where we hit for this API integration test, internal ones test it
   157  			// and we don't really control /etc/os-release version and id.
   158  			// Same for report file
   159  			serverHit := false
   160  			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   161  				serverHit = true
   162  			}))
   163  			defer ts.Close()
   164  
   165  			cmd := generateRootCmd()
   166  			args := []string{"send", tc.answer, "--url", ts.URL}
   167  			cmd.SetArgs(args)
   168  
   169  			cmdErrs := helper.RunFunctionWithTimeout(t, func() error {
   170  				var err error
   171  				_, err = cmd.ExecuteC()
   172  				return err
   173  			})
   174  
   175  			if err := <-cmdErrs; err != nil {
   176  				t.Fatal("got an error when expecting none:", err)
   177  			}
   178  
   179  			a.Equal(serverHit, tc.shouldHitServer)
   180  			// get highest report path
   181  			reportP := ""
   182  			files, err := ioutil.ReadDir(out)
   183  			if err != nil {
   184  				t.Fatalf("couldn't scan %s: %v", out, err)
   185  			}
   186  			for _, f := range files {
   187  				if f.Name() > reportP {
   188  					reportP = f.Name()
   189  				}
   190  			}
   191  			data, err := ioutil.ReadFile(filepath.Join(out, reportP))
   192  			if err != nil {
   193  				t.Fatalf("couldn't open report file %s", reportP)
   194  			}
   195  			d := string(data)
   196  
   197  			switch tc.answer {
   198  			case "yes":
   199  				fallthrough
   200  			case "upgrade":
   201  				if !strings.Contains(d, expectedReportItem) {
   202  					t.Errorf("we expected to find %s in report file, got: %s", expectedReportItem, d)
   203  				}
   204  			case "no":
   205  				if !strings.Contains(d, optOutJSON) {
   206  					t.Errorf("we expected to find %s in report file, got: %s", optOutJSON, d)
   207  				}
   208  			}
   209  		})
   210  	}
   211  }
   212  
   213  func TestInteractive(t *testing.T) {
   214  	helper.SkipIfShort(t)
   215  
   216  	testCases := []struct {
   217  		name    string
   218  		cmd     string
   219  		answers []string
   220  
   221  		sendOnlyOptOutData bool
   222  		wantWriteAndUpload bool
   223  	}{
   224  		{"root yes command", "", []string{"yes"}, false, true},
   225  		{"root YES", "", []string{"YES"}, false, true},
   226  		{"root Y", "", []string{"Y"}, false, true},
   227  		{"root no", "", []string{"no"}, true, true},
   228  		{"root n", "", []string{"n"}, true, true},
   229  		{"root NO", "", []string{"NO"}, true, true},
   230  		{"root n", "", []string{"N"}, true, true},
   231  		{"root quit", "", []string{"quit"}, false, false},
   232  		{"root q", "", []string{"q"}, false, false},
   233  		{"root QUIT", "", []string{"QUIT"}, false, false},
   234  		{"root Q", "", []string{"Q"}, false, false},
   235  		{"root default-quit", "", []string{""}, false, false},
   236  		{"root garbage-then-quit", "", []string{"garbage", "yesgarbage", "nogarbage", "quitgarbage", "Q"}, false, false},
   237  		{"root ctrl-c-input", "", []string{"CTRL-C"}, false, false},
   238  		{"interactive yes command", "interactive", []string{"yes"}, false, true},
   239  		{"interactive no command", "interactive", []string{"no"}, true, true},
   240  		{"interactive ctrl-c-input", "interactive", []string{"CTRL-C"}, false, false},
   241  	}
   242  	for _, tc := range testCases {
   243  		tc := tc // capture range variable for parallel execution
   244  		t.Run(tc.name, func(t *testing.T) {
   245  			a := helper.Asserter{T: t}
   246  
   247  			out, tearDown := helper.TempDir(t)
   248  			defer tearDown()
   249  			defer helper.ChangeEnv("XDG_CACHE_HOME", out)()
   250  			out = filepath.Join(out, "ubuntu-report")
   251  			// we don't really care where we hit for this API integration test, internal ones test it
   252  			// and we don't really control /etc/os-release version and id.
   253  			// Same for report file
   254  			serverHit := false
   255  			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   256  				serverHit = true
   257  			}))
   258  			defer ts.Close()
   259  
   260  			stdout, restoreStdout := helper.CaptureStdout(t)
   261  			defer restoreStdout()
   262  			stdin, tearDown := helper.CaptureStdin(t)
   263  			defer tearDown()
   264  
   265  			cmd := generateRootCmd()
   266  			args := []string{}
   267  			if tc.cmd != "" {
   268  				args = append(args, tc.cmd)
   269  			}
   270  			args = append(args, "--url", ts.URL)
   271  			cmd.SetArgs(args)
   272  
   273  			cmdErrs := helper.RunFunctionWithTimeout(t, func() error {
   274  				var err error
   275  				_, err = cmd.ExecuteC()
   276  				restoreStdout()
   277  				return err
   278  			})
   279  
   280  			gotJSONReport := false
   281  			answerIndex := 0
   282  			scanner := bufio.NewScanner(stdout)
   283  			scanner.Split(scanLinesOrQuestion)
   284  			for scanner.Scan() {
   285  				txt := scanner.Text()
   286  				// first, we should have a known element
   287  				if strings.Contains(txt, expectedReportItem) {
   288  					gotJSONReport = true
   289  				}
   290  				if !strings.Contains(txt, "Do you agree to report this?") {
   291  					continue
   292  				}
   293  				a := tc.answers[answerIndex]
   294  				if a == "CTRL-C" {
   295  					stdin.Close()
   296  					break
   297  				} else {
   298  					stdin.Write([]byte(tc.answers[answerIndex] + "\n"))
   299  				}
   300  				answerIndex = answerIndex + 1
   301  				// all answers have be provided
   302  				if answerIndex >= len(tc.answers) {
   303  					stdin.Close()
   304  					break
   305  				}
   306  			}
   307  
   308  			if err := <-cmdErrs; err != nil {
   309  				t.Fatal("didn't expect to get an error, got:", err)
   310  			}
   311  			a.Equal(gotJSONReport, true)
   312  			a.Equal(serverHit, tc.wantWriteAndUpload)
   313  
   314  			if !tc.wantWriteAndUpload {
   315  				if _, err := os.Stat(filepath.Join(out, "ubuntu-report")); err == nil || (err != nil && !os.IsNotExist(err)) {
   316  					t.Fatal("we didn't want to get a report but we got one")
   317  				}
   318  				return
   319  			}
   320  
   321  			p := filepath.Join(out, helper.FindInDirectory(t, "", out))
   322  			data, err := ioutil.ReadFile(p)
   323  			if err != nil {
   324  				t.Fatalf("couldn't open report file %s", out)
   325  			}
   326  			d := string(data)
   327  			expected := expectedReportItem
   328  			if tc.sendOnlyOptOutData {
   329  				expected = optOutJSON
   330  			}
   331  			if !strings.Contains(d, expected) {
   332  				t.Errorf("we expected to find %s in report file, got: %s", expected, d)
   333  			}
   334  		})
   335  	}
   336  }
   337  
   338  func TestService(t *testing.T) {
   339  	helper.SkipIfShort(t)
   340  
   341  	testCases := []struct {
   342  		name string
   343  
   344  		shouldHitServer bool
   345  	}{
   346  		{"regular send", true},
   347  	}
   348  	for _, tc := range testCases {
   349  		tc := tc // capture range variable for parallel execution
   350  		t.Run(tc.name, func(t *testing.T) {
   351  			a := helper.Asserter{T: t}
   352  
   353  			out, tearDown := helper.TempDir(t)
   354  			defer tearDown()
   355  			defer helper.ChangeEnv("XDG_CACHE_HOME", out)()
   356  			out = filepath.Join(out, "ubuntu-report")
   357  
   358  			pendingReportData, err := ioutil.ReadFile(filepath.Join("testdata", "good", "ubuntu-report", "pending"))
   359  			if err != nil {
   360  				t.Fatalf("couldn't open pending report file: %v", err)
   361  			}
   362  			pendingReportPath := filepath.Join(out, "pending")
   363  			if err := os.MkdirAll(out, 0700); err != nil {
   364  				t.Fatal("couldn't create parent directory of pending report", err)
   365  			}
   366  			if err := ioutil.WriteFile(pendingReportPath, pendingReportData, 0644); err != nil {
   367  				t.Fatalf("couldn't copy pending report file to cache directory: %v", err)
   368  			}
   369  
   370  			// we don't really care where we hit for this API integration test, internal ones test it
   371  			// and we don't really control /etc/os-release version and id.
   372  			// Same for report file
   373  			serverHit := false
   374  			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   375  				serverHit = true
   376  			}))
   377  			defer ts.Close()
   378  
   379  			cmd := generateRootCmd()
   380  			args := []string{"service", "--url", ts.URL}
   381  			cmd.SetArgs(args)
   382  
   383  			cmdErrs := helper.RunFunctionWithTimeout(t, func() error {
   384  				var err error
   385  				_, err = cmd.ExecuteC()
   386  				return err
   387  			})
   388  
   389  			if err := <-cmdErrs; err != nil {
   390  				t.Fatal("got an error when expecting none:", err)
   391  			}
   392  
   393  			a.Equal(serverHit, tc.shouldHitServer)
   394  
   395  			if _, pendingReportErr := os.Stat(pendingReportPath); os.IsExist(pendingReportErr) {
   396  				t.Errorf("we expected the pending report to be removed and it wasn't")
   397  			}
   398  
   399  			p := filepath.Join(out, helper.FindInDirectory(t, "", out))
   400  			got, err := ioutil.ReadFile(p)
   401  			if err != nil {
   402  				t.Fatalf("couldn't open report file %s", out)
   403  			}
   404  			a.Equal(got, pendingReportData)
   405  		})
   406  	}
   407  }
   408  
   409  // scanLinesOrQuestion is copy of ScanLines, adding the expected question string as we don't return here
   410  func scanLinesOrQuestion(data []byte, atEOF bool) (advance int, token []byte, err error) {
   411  	if atEOF && len(data) == 0 {
   412  		return 0, nil, nil
   413  	}
   414  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   415  		// We have a full newline-terminated line.
   416  		return i + 1, dropCR(data[0:i]), nil
   417  	}
   418  	if i := bytes.IndexByte(data, ']'); i >= 0 {
   419  		// We have a full newline-terminated line.
   420  		return i + 1, dropCR(data[0:i]), nil
   421  	}
   422  	// If we're at EOF, we have a final, non-terminated line. Return it.
   423  	if atEOF {
   424  		return len(data), dropCR(data), nil
   425  	}
   426  	// Request more data.
   427  	return 0, nil, nil
   428  }
   429  
   430  // dropCR drops a terminal \r from the data.
   431  func dropCR(data []byte) []byte {
   432  	if len(data) > 0 && data[len(data)-1] == '\r' {
   433  		return data[0 : len(data)-1]
   434  	}
   435  	return data
   436  }