1// Copyright 2017 syzkaller project authors. All rights reserved. 2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4// The test uses aetest package that starts local dev_appserver and handles all requests locally: 5// https://cloud.google.com/appengine/docs/standard/go/tools/localunittesting/reference 6// The test requires installed appengine SDK (dev_appserver), so we guard it by aetest tag. 7// Run the test with: goapp test -tags=aetest 8 9// +build aetest 10 11package dash 12 13import ( 14 "bytes" 15 "fmt" 16 "io/ioutil" 17 "net/http" 18 "net/http/httptest" 19 "path/filepath" 20 "reflect" 21 "runtime" 22 "strings" 23 "sync" 24 "testing" 25 "time" 26 27 "github.com/google/syzkaller/dashboard/dashapi" 28 "golang.org/x/net/context" 29 "google.golang.org/appengine" 30 "google.golang.org/appengine/aetest" 31 "google.golang.org/appengine/datastore" 32 aemail "google.golang.org/appengine/mail" 33 "google.golang.org/appengine/user" 34) 35 36type Ctx struct { 37 t *testing.T 38 inst aetest.Instance 39 ctx context.Context 40 mockedTime time.Time 41 emailSink chan *aemail.Message 42 client *apiClient 43 client2 *apiClient 44} 45 46func NewCtx(t *testing.T) *Ctx { 47 t.Parallel() 48 inst, err := aetest.NewInstance(&aetest.Options{ 49 // Without this option datastore queries return data with slight delay, 50 // which fails reporting tests. 51 StronglyConsistentDatastore: true, 52 }) 53 if err != nil { 54 t.Fatal(err) 55 } 56 r, err := inst.NewRequest("GET", "", nil) 57 if err != nil { 58 t.Fatal(err) 59 } 60 c := &Ctx{ 61 t: t, 62 inst: inst, 63 ctx: appengine.NewContext(r), 64 mockedTime: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), 65 emailSink: make(chan *aemail.Message, 100), 66 } 67 c.client = c.makeClient(client1, key1, true) 68 c.client2 = c.makeClient(client2, key2, true) 69 registerContext(r, c) 70 return c 71} 72 73func (c *Ctx) expectOK(err error) { 74 if err != nil { 75 c.t.Fatalf("\n%v: %v", caller(0), err) 76 } 77} 78 79func (c *Ctx) expectFail(msg string, err error) { 80 if err == nil { 81 c.t.Fatalf("\n%v: expected to fail, but it does not", caller(0)) 82 } 83 if !strings.Contains(err.Error(), msg) { 84 c.t.Fatalf("\n%v: expected to fail with %q, but failed with %q", caller(0), msg, err) 85 } 86} 87 88func (c *Ctx) expectForbidden(err error) { 89 if err == nil { 90 c.t.Fatalf("\n%v: expected to fail as 403, but it does not", caller(0)) 91 } 92 httpErr, ok := err.(HttpError) 93 if !ok || httpErr.Code != http.StatusForbidden { 94 c.t.Fatalf("\n%v: expected to fail as 403, but it failed as %v", caller(0), err) 95 } 96} 97 98func (c *Ctx) expectEQ(got, want interface{}) { 99 if !reflect.DeepEqual(got, want) { 100 c.t.Fatalf("\n%v: got %#v, want %#v", caller(0), got, want) 101 } 102} 103 104func (c *Ctx) expectTrue(v bool) { 105 if !v { 106 c.t.Fatalf("\n%v: failed", caller(0)) 107 } 108} 109 110func caller(skip int) string { 111 _, file, line, _ := runtime.Caller(skip + 2) 112 return fmt.Sprintf("%v:%v", filepath.Base(file), line) 113} 114 115func (c *Ctx) Close() { 116 if !c.t.Failed() { 117 // Ensure that we can render main page and all bugs in the final test state. 118 c.expectOK(c.GET("/")) 119 var bugs []*Bug 120 keys, err := datastore.NewQuery("Bug").GetAll(c.ctx, &bugs) 121 if err != nil { 122 c.t.Errorf("ERROR: failed to query bugs: %v", err) 123 } 124 for _, key := range keys { 125 c.expectOK(c.GET(fmt.Sprintf("/bug?id=%v", key.StringID()))) 126 } 127 c.expectOK(c.GET("/email_poll")) 128 for len(c.emailSink) != 0 { 129 c.t.Errorf("ERROR: leftover email: %v", (<-c.emailSink).Body) 130 } 131 } 132 unregisterContext(c) 133 c.inst.Close() 134} 135 136func (c *Ctx) advanceTime(d time.Duration) { 137 c.mockedTime = c.mockedTime.Add(d) 138} 139 140// GET sends admin-authorized HTTP GET request to the app. 141func (c *Ctx) GET(url string) error { 142 _, err := c.httpRequest("GET", url, "", AccessAdmin) 143 return err 144} 145 146// AuthGET sends HTTP GET request to the app with the specified authorization. 147func (c *Ctx) AuthGET(access AccessLevel, url string) ([]byte, error) { 148 return c.httpRequest("GET", url, "", access) 149} 150 151// POST sends admin-authorized HTTP POST request to the app. 152func (c *Ctx) POST(url, body string) error { 153 _, err := c.httpRequest("POST", url, body, AccessAdmin) 154 return err 155} 156 157func (c *Ctx) httpRequest(method, url, body string, access AccessLevel) ([]byte, error) { 158 c.t.Logf("%v: %v", method, url) 159 r, err := c.inst.NewRequest(method, url, strings.NewReader(body)) 160 if err != nil { 161 c.t.Fatal(err) 162 } 163 registerContext(r, c) 164 if access == AccessAdmin || access == AccessUser { 165 user := &user.User{ 166 Email: "user@syzkaller.com", 167 AuthDomain: "gmail.com", 168 } 169 if access == AccessAdmin { 170 user.Admin = true 171 } 172 aetest.Login(user, r) 173 } 174 w := httptest.NewRecorder() 175 http.DefaultServeMux.ServeHTTP(w, r) 176 c.t.Logf("REPLY: %v", w.Code) 177 if w.Code != http.StatusOK { 178 return nil, HttpError{w.Code, w.Body.String()} 179 } 180 return w.Body.Bytes(), nil 181} 182 183type HttpError struct { 184 Code int 185 Body string 186} 187 188func (err HttpError) Error() string { 189 return fmt.Sprintf("%v: %v", err.Code, err.Body) 190} 191 192func (c *Ctx) loadBug(extID string) (*Bug, *Crash, *Build) { 193 bug, _, err := findBugByReportingID(c.ctx, extID) 194 if err != nil { 195 c.t.Fatalf("failed to load bug: %v", err) 196 } 197 crash, _, err := findCrashForBug(c.ctx, bug) 198 if err != nil { 199 c.t.Fatalf("failed to load crash: %v", err) 200 } 201 build, err := loadBuild(c.ctx, bug.Namespace, crash.BuildID) 202 if err != nil { 203 c.t.Fatalf("failed to load build: %v", err) 204 } 205 return bug, crash, build 206} 207 208func (c *Ctx) loadJob(extID string) (*Job, *Build) { 209 jobKey, err := jobID2Key(c.ctx, extID) 210 if err != nil { 211 c.t.Fatalf("failed to create job key: %v", err) 212 } 213 job := new(Job) 214 if err := datastore.Get(c.ctx, jobKey, job); err != nil { 215 c.t.Fatalf("failed to get job %v: %v", extID, err) 216 } 217 build, err := loadBuild(c.ctx, job.Namespace, job.BuildID) 218 if err != nil { 219 c.t.Fatalf("failed to load build: %v", err) 220 } 221 return job, build 222} 223 224func (c *Ctx) checkURLContents(url string, want []byte) { 225 got, err := c.AuthGET(AccessAdmin, url) 226 if err != nil { 227 c.t.Fatalf("\n%v: %v request failed: %v", caller(0), url, err) 228 } 229 if !bytes.Equal(got, want) { 230 c.t.Fatalf("\n%v: url %v: got:\n%s\nwant:\n%s\n", caller(0), url, got, want) 231 } 232} 233 234type apiClient struct { 235 *Ctx 236 *dashapi.Dashboard 237} 238 239func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient { 240 doer := func(r *http.Request) (*http.Response, error) { 241 registerContext(r, c) 242 w := httptest.NewRecorder() 243 http.DefaultServeMux.ServeHTTP(w, r) 244 // Later versions of Go have a nice w.Result method, 245 // but we stuck on 1.6 on appengine. 246 if w.Body == nil { 247 w.Body = new(bytes.Buffer) 248 } 249 res := &http.Response{ 250 StatusCode: w.Code, 251 Status: http.StatusText(w.Code), 252 Body: ioutil.NopCloser(bytes.NewReader(w.Body.Bytes())), 253 } 254 return res, nil 255 } 256 logger := func(msg string, args ...interface{}) { 257 c.t.Logf("%v: "+msg, append([]interface{}{caller(3)}, args...)...) 258 } 259 errorHandler := func(err error) { 260 if failOnErrors { 261 c.t.Fatalf("\n%v: %v", caller(2), err) 262 } 263 } 264 return &apiClient{ 265 Ctx: c, 266 Dashboard: dashapi.NewCustom(client, "", key, c.inst.NewRequest, doer, logger, errorHandler), 267 } 268} 269 270func (client *apiClient) pollBugs(expect int) []*dashapi.BugReport { 271 resp, _ := client.ReportingPollBugs("test") 272 if len(resp.Reports) != expect { 273 client.t.Fatalf("\n%v: want %v reports, got %v", caller(0), expect, len(resp.Reports)) 274 } 275 for _, rep := range resp.Reports { 276 reproLevel := dashapi.ReproLevelNone 277 if len(rep.ReproC) != 0 { 278 reproLevel = dashapi.ReproLevelC 279 } else if len(rep.ReproSyz) != 0 { 280 reproLevel = dashapi.ReproLevelSyz 281 } 282 reply, _ := client.ReportingUpdate(&dashapi.BugUpdate{ 283 ID: rep.ID, 284 Status: dashapi.BugStatusOpen, 285 ReproLevel: reproLevel, 286 CrashID: rep.CrashID, 287 }) 288 client.expectEQ(reply.Error, false) 289 client.expectEQ(reply.OK, true) 290 } 291 return resp.Reports 292} 293 294func (client *apiClient) pollBug() *dashapi.BugReport { 295 return client.pollBugs(1)[0] 296} 297 298func (client *apiClient) updateBug(extID string, status dashapi.BugStatus, dup string) { 299 reply, _ := client.ReportingUpdate(&dashapi.BugUpdate{ 300 ID: extID, 301 Status: status, 302 DupOf: dup, 303 }) 304 client.expectTrue(reply.OK) 305} 306 307type ( 308 EmailOptMessageID int 309 EmailOptFrom string 310 EmailOptCC []string 311) 312 313func (c *Ctx) incomingEmail(to, body string, opts ...interface{}) { 314 id := 0 315 from := "default@sender.com" 316 cc := []string{"test@syzkaller.com", "bugs@syzkaller.com"} 317 for _, o := range opts { 318 switch opt := o.(type) { 319 case EmailOptMessageID: 320 id = int(opt) 321 case EmailOptFrom: 322 from = string(opt) 323 case EmailOptCC: 324 cc = []string(opt) 325 } 326 } 327 email := fmt.Sprintf(`Sender: %v 328Date: Tue, 15 Aug 2017 14:59:00 -0700 329Message-ID: <%v> 330Subject: crash1 331From: %v 332Cc: %v 333To: %v 334Content-Type: text/plain 335 336%v 337`, from, id, from, strings.Join(cc, ","), to, body) 338 c.expectOK(c.POST("/_ah/mail/", email)) 339} 340 341func initMocks() { 342 // Mock time as some functionality relies on real time. 343 timeNow = func(c context.Context) time.Time { 344 return getRequestContext(c).mockedTime 345 } 346 sendEmail = func(c context.Context, msg *aemail.Message) error { 347 getRequestContext(c).emailSink <- msg 348 return nil 349 } 350} 351 352// Machinery to associate mocked time with requests. 353type RequestMapping struct { 354 c context.Context 355 ctx *Ctx 356} 357 358var ( 359 requestMu sync.Mutex 360 requestContexts []RequestMapping 361) 362 363func registerContext(r *http.Request, c *Ctx) { 364 requestMu.Lock() 365 defer requestMu.Unlock() 366 requestContexts = append(requestContexts, RequestMapping{appengine.NewContext(r), c}) 367} 368 369func getRequestContext(c context.Context) *Ctx { 370 requestMu.Lock() 371 defer requestMu.Unlock() 372 for _, m := range requestContexts { 373 if reflect.DeepEqual(c, m.c) { 374 return m.ctx 375 } 376 } 377 panic(fmt.Sprintf("no context for: %#v", c)) 378} 379 380func unregisterContext(c *Ctx) { 381 requestMu.Lock() 382 defer requestMu.Unlock() 383 n := 0 384 for _, m := range requestContexts { 385 if m.ctx == c { 386 continue 387 } 388 requestContexts[n] = m 389 n++ 390 } 391 requestContexts = requestContexts[:n] 392} 393