• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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