/* Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "compress/gzip" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "strconv" "strings" "testing" ) // testParsePayload builds a /submit request with the given body, and calls // parseRequest with it. // // if tempDir is empty, a new temp dir is created, and deleted when the test // completes. func testParsePayload(t *testing.T, body, contentType string, tempDir string) (*payload, *http.Response) { req, err := http.NewRequest("POST", "/api/submit", strings.NewReader(body)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Length", strconv.Itoa(len(body))) if contentType != "" { req.Header.Set("Content-Type", contentType) } // temporary dir for the uploaded files if tempDir == "" { tempDir = mkTempDir(t) defer os.RemoveAll(tempDir) } rr := httptest.NewRecorder() p := parseRequest(rr, req, tempDir) return p, rr.Result() } func TestEmptyJson(t *testing.T) { body := "{}" // we just test it is parsed without errors for now p, _ := testParsePayload(t, body, "application/json", "") if p == nil { t.Fatal("parseRequest returned nil") } if len(p.Labels) != 0 { t.Errorf("Labels: got %#v, want []", p.Labels) } } func TestJsonUpload(t *testing.T) { reportDir := mkTempDir(t) defer os.RemoveAll(reportDir) body := `{ "app": "riot-web", "logs": [ { "id": "instance-0.99152119701215051494400738905", "lines": "line1\nline2" } ], "text": "test message", "user_agent": "Mozilla", "version": "0.9.9" }` p, _ := testParsePayload(t, body, "application/json", reportDir) if p == nil { t.Fatal("parseRequest returned nil") } wanted := "test message" if p.UserText != wanted { t.Errorf("user text: got %s, want %s", p.UserText, wanted) } wanted = "riot-web" if p.AppName != wanted { t.Errorf("appname: got %s, want %s", p.AppName, wanted) } wanted = "0.9.9" if p.Data["Version"] != wanted { t.Errorf("version: got %s, want %s", p.Data["Version"], wanted) } checkUploadedFile(t, reportDir, "logs-0000.log.gz", true, "line1\nline2") } // check that we can unpick the json submitted by the android clients func TestUnpickAndroidMangling(t *testing.T) { body := `{"text": "test ylc 001", "version": "User : @ylc8001:matrix.org\nPhone : Lenovo P2a42\nVector version: 0:6:9\n", "user_agent": "Android" }` p, _ := testParsePayload(t, body, "", "") if p == nil { t.Fatal("parseRequest returned nil") } if p.UserText != "test ylc 001" { t.Errorf("user text: got %s, want %s", p.UserText, "test ylc 001") } if p.AppName != "riot-android" { t.Errorf("appname: got %s, want %s", p.AppName, "riot-android") } if p.Data["Version"] != "" { t.Errorf("version: got %s, want ''", p.Data["Version"]) } if p.Data["User"] != "@ylc8001:matrix.org" { t.Errorf("data.user: got %s, want %s", p.Data["User"], "@ylc8001:matrix.org") } if p.Data["Phone"] != "Lenovo P2a42" { t.Errorf("data.phone: got %s, want %s", p.Data["Phone"], "Lenovo P2a42") } if p.Data["Vector version"] != "0:6:9" { t.Errorf("data.version: got %s, want %s", p.Data["Version"], "0:6:9") } } func TestMultipartUpload(t *testing.T) { reportDir := mkTempDir(t) defer os.RemoveAll(reportDir) p, _ := testParsePayload(t, multipartBody(), "multipart/form-data; boundary=----WebKitFormBoundarySsdgl8Nq9voFyhdO", reportDir, ) if p == nil { t.Fatal("parseRequest returned nil") } checkParsedMultipartUpload(t, p) // check logs uploaded correctly checkUploadedFile(t, reportDir, "logs-0000.log.gz", true, "log\nlog\nlog") checkUploadedFile(t, reportDir, "console.0.log.gz", true, "log") checkUploadedFile(t, reportDir, "logs-0002.log.gz", true, "test\n") // check file uploaded correctly checkUploadedFile(t, reportDir, "passwd.txt", false, "bibblybobbly") checkUploadedFile(t, reportDir, "crash.log.gz", true, "test\n") } func multipartBody() (body string) { body = `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="text" test words. ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="app" riot-web ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="version" UNKNOWN ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="user_agent" Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="test-field" Test data ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="log"; filename="instance-0.215954445471346461492087122412" Content-Type: text/plain log log log ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="log"; filename="console.0.log" Content-Type: text/plain log ` body += `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="compressed-log"; filename="instance-0.0109372050779190651492004373866" Content-Type: application/octet-stream ` body += string([]byte{ 0x1f, 0x8b, 0x08, 0x00, 0xbf, 0xd8, 0xf5, 0x58, 0x00, 0x03, 0x2b, 0x49, 0x2d, 0x2e, 0xe1, 0x02, 0x00, 0xc6, 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x0a, }) body += `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="file"; filename="passwd.txt" Content-Type: application/octet-stream bibblybobbly ` body += `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="compressed-log"; filename="crash.log.gz" Content-Type: application/octet-stream ` body += string([]byte{ 0x1f, 0x8b, 0x08, 0x00, 0xbf, 0xd8, 0xf5, 0x58, 0x00, 0x03, 0x2b, 0x49, 0x2d, 0x2e, 0xe1, 0x02, 0x00, 0xc6, 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x0a, }) body += "------WebKitFormBoundarySsdgl8Nq9voFyhdO--\n" return } func checkParsedMultipartUpload(t *testing.T, p *payload) { wanted := "test words." if p.UserText != wanted { t.Errorf("User text: got %s, want %s", p.UserText, wanted) } if len(p.Logs) != 4 { t.Errorf("Log length: got %d, want 4", len(p.Logs)) } if len(p.Data) != 3 { t.Errorf("Data length: got %d, want 3", len(p.Data)) } if len(p.Labels) != 0 { t.Errorf("Labels: got %#v, want []", p.Labels) } wanted = "Test data" if p.Data["test-field"] != wanted { t.Errorf("test-field: got %s, want %s", p.Data["test-field"], wanted) } wanted = "logs-0000.log.gz" if p.Logs[0] != wanted { t.Errorf("Log 0: got %s, want %s", p.Logs[0], wanted) } wanted = "console.0.log.gz" if p.Logs[1] != wanted { t.Errorf("Log 1: got %s, want %s", p.Logs[1], wanted) } wanted = "logs-0002.log.gz" if p.Logs[2] != wanted { t.Errorf("Log 2: got %s, want %s", p.Logs[2], wanted) } wanted = "crash.log.gz" if p.Logs[3] != wanted { t.Errorf("Log 3: got %s, want %s", p.Logs[3], wanted) } } func TestLabels(t *testing.T) { body := `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="label" label1 ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="label" label2 ------WebKitFormBoundarySsdgl8Nq9voFyhdO-- ` p, _ := testParsePayload(t, body, "multipart/form-data; boundary=----WebKitFormBoundarySsdgl8Nq9voFyhdO", "", ) if p == nil { t.Fatal("parseRequest returned nil") } wantedLabels := []string{"label1", "label2"} if !stringSlicesEqual(p.Labels, wantedLabels) { t.Errorf("Labels: got %v, want %v", p.Labels, wantedLabels) } } func stringSlicesEqual(got, want []string) bool { if len(got) != len(want) { return false } for i := range got { if got[i] != want[i] { return false } } return true } /* FIXME these should just give a message in the details file now func TestEmptyFilename(t *testing.T) { body := `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="file" file ------WebKitFormBoundarySsdgl8Nq9voFyhdO-- ` p, resp := testParsePayload(t, body, "multipart/form-data; boundary=----WebKitFormBoundarySsdgl8Nq9voFyhdO", "") if p != nil { t.Error("parsePayload accepted upload with no filename") } if resp.StatusCode != 400 { t.Errorf("response code: got %v, want %v", resp.StatusCode, 400) } } func TestBadFilename(t *testing.T) { body := `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="file"; filename="etc/passwd" file ------WebKitFormBoundarySsdgl8Nq9voFyhdO-- ` p, resp := testParsePayload(t, body, "multipart/form-data; boundary=----WebKitFormBoundarySsdgl8Nq9voFyhdO", "") if p != nil { t.Error("parsePayload accepted upload with bad filename") } if resp.StatusCode != 400 { t.Errorf("response code: got %v, want %v", resp.StatusCode, 400) } } */ func checkUploadedFile(t *testing.T, reportDir, leafName string, gzipped bool, wanted string) { fi, err := os.Open(filepath.Join(reportDir, leafName)) if err != nil { t.Errorf("unable to open uploaded file %s: %v", leafName, err) return } defer fi.Close() var rdr io.Reader if !gzipped { rdr = fi } else { gz, err2 := gzip.NewReader(fi) if err2 != nil { t.Errorf("unable to ungzip uploaded file %s: %v", leafName, err2) return } defer gz.Close() rdr = gz } dat, err := ioutil.ReadAll(rdr) if err != nil { t.Errorf("unable to read uploaded file %s: %v", leafName, err) return } datstr := string(dat) if datstr != wanted { t.Errorf("File %s: got %s, want %s", leafName, datstr, wanted) } } func mkTempDir(t *testing.T) string { td, err := ioutil.TempDir("", "rageshake_test") if err != nil { t.Fatal(err) } return td } /***************************************************************************** * * buildGithubIssueRequest tests */ func TestBuildGithubIssueLeadingNewline(t *testing.T) { body := `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="text" test words. ------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="app" riot-web ------WebKitFormBoundarySsdgl8Nq9voFyhdO-- ` p, _ := testParsePayload(t, body, "multipart/form-data; boundary=----WebKitFormBoundarySsdgl8Nq9voFyhdO", "", ) if p == nil { t.Fatal("parseRequest returned nil") } issueReq := buildGithubIssueRequest(*p, "http://test/listing/foo") if *issueReq.Title != "test words." { t.Errorf("Title: got %s, want %s", *issueReq.Title, "test words.") } expectedBody := "User message:\n\n\ntest words.\n" if !strings.HasPrefix(*issueReq.Body, expectedBody) { t.Errorf("Body: got %s, want %s", *issueReq.Body, expectedBody) } } func TestBuildGithubIssueEmptyBody(t *testing.T) { body := `------WebKitFormBoundarySsdgl8Nq9voFyhdO Content-Disposition: form-data; name="text" ------WebKitFormBoundarySsdgl8Nq9voFyhdO-- ` p, _ := testParsePayload(t, body, "multipart/form-data; boundary=----WebKitFormBoundarySsdgl8Nq9voFyhdO", "", ) if p == nil { t.Fatal("parseRequest returned nil") } issueReq := buildGithubIssueRequest(*p, "http://test/listing/foo") if *issueReq.Title != "Untitled report" { t.Errorf("Title: got %s, want %s", *issueReq.Title, "Untitled report") } expectedBody := "User message:\n\n\n" if !strings.HasPrefix(*issueReq.Body, expectedBody) { t.Errorf("Body: got %s, want %s", *issueReq.Body, expectedBody) } } func TestTestSortDataKeys(t *testing.T) { expect := ` Number of logs: 0 Application: Labels: User-Agent: xxx Version: 1 device_id: id user_id: id ` expect = strings.TrimSpace(expect) sample := []struct { data map[string]string }{ { map[string]string{ "Version": "1", "User-Agent": "xxx", "user_id": "id", "device_id": "id", }, }, { map[string]string{ "user_id": "id", "device_id": "id", "Version": "1", "User-Agent": "xxx", }, }, } var buf bytes.Buffer for _, v := range sample { p := payload{Data: v.data} buf.Reset() p.WriteTo(&buf) got := strings.TrimSpace(buf.String()) if got != expect { t.Errorf("expected %s got %s", expect, got) } } for k, v := range sample { p := payload{Data: v.data} res := buildGithubIssueRequest(p, "") got := *res.Body if k == 0 { expect = got continue } if got != expect { t.Errorf("expected %s got %s", expect, got) } } }