| // Copyright 2017 syzkaller project authors. All rights reserved. |
| // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. |
| |
| // The test uses aetest package that starts local dev_appserver and handles all requests locally: |
| // https://cloud.google.com/appengine/docs/standard/go/tools/localunittesting/reference |
| // The test requires installed appengine SDK (dev_appserver), so we guard it by aetest tag. |
| // Run the test with: goapp test -tags=aetest |
| |
| // +build aetest |
| |
| package dash |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io/ioutil" |
| "net/http" |
| "net/http/httptest" |
| "path/filepath" |
| "reflect" |
| "runtime" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "github.com/google/syzkaller/dashboard/dashapi" |
| "golang.org/x/net/context" |
| "google.golang.org/appengine" |
| "google.golang.org/appengine/aetest" |
| "google.golang.org/appengine/datastore" |
| aemail "google.golang.org/appengine/mail" |
| "google.golang.org/appengine/user" |
| ) |
| |
| type Ctx struct { |
| t *testing.T |
| inst aetest.Instance |
| ctx context.Context |
| mockedTime time.Time |
| emailSink chan *aemail.Message |
| } |
| |
| func NewCtx(t *testing.T) *Ctx { |
| t.Parallel() |
| inst, err := aetest.NewInstance(&aetest.Options{ |
| // Without this option datastore queries return data with slight delay, |
| // which fails reporting tests. |
| StronglyConsistentDatastore: true, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| r, err := inst.NewRequest("GET", "", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| c := &Ctx{ |
| t: t, |
| inst: inst, |
| ctx: appengine.NewContext(r), |
| mockedTime: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), |
| emailSink: make(chan *aemail.Message, 100), |
| } |
| registerContext(r, c) |
| return c |
| } |
| |
| func (c *Ctx) expectOK(err error) { |
| if err != nil { |
| c.t.Fatalf("\n%v: %v", caller(0), err) |
| } |
| } |
| |
| func (c *Ctx) expectFail(msg string, err error) { |
| if err == nil { |
| c.t.Fatal("\n%v: expected to fail, but it does not", caller(0)) |
| } |
| if !strings.Contains(err.Error(), msg) { |
| c.t.Fatalf("\n%v: expected to fail with %q, but failed with %q", caller(0), msg, err) |
| } |
| } |
| |
| func (c *Ctx) expectEQ(got, want interface{}) { |
| if !reflect.DeepEqual(got, want) { |
| c.t.Fatalf("\n%v: got %#v, want %#v", caller(0), got, want) |
| } |
| } |
| |
| func caller(skip int) string { |
| _, file, line, _ := runtime.Caller(skip + 2) |
| return fmt.Sprintf("%v:%v", filepath.Base(file), line) |
| } |
| |
| func (c *Ctx) Close() { |
| if !c.t.Failed() { |
| // Ensure that we can render main page and all bugs in the final test state. |
| c.expectOK(c.GET("/")) |
| var bugs []*Bug |
| keys, err := datastore.NewQuery("Bug").GetAll(c.ctx, &bugs) |
| if err != nil { |
| c.t.Errorf("ERROR: failed to query bugs: %v", err) |
| } |
| for _, key := range keys { |
| c.expectOK(c.GET(fmt.Sprintf("/bug?id=%v", key.StringID()))) |
| } |
| c.expectOK(c.GET("/email_poll")) |
| for len(c.emailSink) != 0 { |
| c.t.Errorf("ERROR: leftover email: %v", (<-c.emailSink).Body) |
| } |
| } |
| unregisterContext(c) |
| c.inst.Close() |
| } |
| |
| func (c *Ctx) advanceTime(d time.Duration) { |
| c.mockedTime = c.mockedTime.Add(d) |
| } |
| |
| // API makes an api request to the app from the specified client. |
| func (c *Ctx) API(client, key, method string, req, reply interface{}) error { |
| doer := func(r *http.Request) (*http.Response, error) { |
| registerContext(r, c) |
| w := httptest.NewRecorder() |
| http.DefaultServeMux.ServeHTTP(w, r) |
| // Later versions of Go have a nice w.Result method, |
| // but we stuck on 1.6 on appengine. |
| if w.Body == nil { |
| w.Body = new(bytes.Buffer) |
| } |
| res := &http.Response{ |
| StatusCode: w.Code, |
| Status: http.StatusText(w.Code), |
| Body: ioutil.NopCloser(bytes.NewReader(w.Body.Bytes())), |
| } |
| return res, nil |
| } |
| |
| c.t.Logf("API(%v): %#v", method, req) |
| err := dashapi.Query(client, "", key, method, c.inst.NewRequest, doer, req, reply) |
| if err != nil { |
| c.t.Logf("ERROR: %v", err) |
| return err |
| } |
| c.t.Logf("REPLY: %#v", reply) |
| return nil |
| } |
| |
| // GET sends authorized HTTP GET request to the app. |
| func (c *Ctx) GET(url string) error { |
| return c.httpRequest("GET", url, "") |
| } |
| |
| // POST sends authorized HTTP POST request to the app. |
| func (c *Ctx) POST(url, body string) error { |
| return c.httpRequest("POST", url, body) |
| } |
| |
| func (c *Ctx) httpRequest(method, url, body string) error { |
| c.t.Logf("%v: %v", method, url) |
| r, err := c.inst.NewRequest(method, url, strings.NewReader(body)) |
| if err != nil { |
| c.t.Fatal(err) |
| } |
| registerContext(r, c) |
| user := &user.User{ |
| Email: "test@syzkaller.com", |
| AuthDomain: "gmail.com", |
| Admin: true, |
| } |
| aetest.Login(user, r) |
| w := httptest.NewRecorder() |
| http.DefaultServeMux.ServeHTTP(w, r) |
| c.t.Logf("REPLY: %v", w.Code) |
| if w.Code != http.StatusOK { |
| return fmt.Errorf("%v", w.Body.String()) |
| } |
| return nil |
| } |
| |
| func (c *Ctx) incomingEmail(to, body string) { |
| email := fmt.Sprintf(`Sender: foo@bar.com |
| Date: Tue, 15 Aug 2017 14:59:00 -0700 |
| Message-ID: <1234> |
| Subject: crash1 |
| From: default@sender.com |
| Cc: test@syzkaller.com, bugs@syzkaller.com |
| To: %v |
| Content-Type: text/plain |
| |
| %v |
| `, to, body) |
| c.expectOK(c.POST("/_ah/mail/", email)) |
| } |
| |
| func init() { |
| // Mock time as some functionality relies on real time. |
| timeNow = func(c context.Context) time.Time { |
| return getRequestContext(c).mockedTime |
| } |
| sendEmail = func(c context.Context, msg *aemail.Message) error { |
| getRequestContext(c).emailSink <- msg |
| return nil |
| } |
| } |
| |
| // Machinery to associate mocked time with requests. |
| type RequestMapping struct { |
| c context.Context |
| ctx *Ctx |
| } |
| |
| var ( |
| requestMu sync.Mutex |
| requestContexts []RequestMapping |
| ) |
| |
| func registerContext(r *http.Request, c *Ctx) { |
| requestMu.Lock() |
| defer requestMu.Unlock() |
| requestContexts = append(requestContexts, RequestMapping{appengine.NewContext(r), c}) |
| } |
| |
| func getRequestContext(c context.Context) *Ctx { |
| requestMu.Lock() |
| defer requestMu.Unlock() |
| for _, m := range requestContexts { |
| if reflect.DeepEqual(c, m.c) { |
| return m.ctx |
| } |
| } |
| panic(fmt.Sprintf("no context for: %#v", c)) |
| } |
| |
| func unregisterContext(c *Ctx) { |
| requestMu.Lock() |
| defer requestMu.Unlock() |
| n := 0 |
| for _, m := range requestContexts { |
| if m.ctx == c { |
| continue |
| } |
| requestContexts[n] = m |
| n++ |
| } |
| requestContexts = requestContexts[:n] |
| } |