blob: c9b8f8688edb78df01e0f5053001f92c430ae84c [file] [log] [blame]
// 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]
}