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 }