github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/main_test.go (about)

     1  // Copyright 2023 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  package main
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"net/http"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/google/syzkaller/dashboard/dashapi"
    15  	"github.com/stretchr/testify/assert"
    16  )
    17  
    18  func TestOnlyManagerFilter(t *testing.T) {
    19  	c := NewCtx(t)
    20  	defer c.Close()
    21  
    22  	client := c.client
    23  	build1 := testBuild(1)
    24  	client.UploadBuild(build1)
    25  	build2 := testBuild(2)
    26  	client.UploadBuild(build2)
    27  
    28  	crash1 := testCrash(build1, 1)
    29  	crash1.Title = "only the first manager"
    30  	client.ReportCrash(crash1)
    31  
    32  	crash2 := testCrash(build2, 2)
    33  	crash2.Title = "only the second manager"
    34  	client.ReportCrash(crash2)
    35  
    36  	crashBoth1 := testCrash(build1, 3)
    37  	crashBoth1.Title = "both managers"
    38  	client.ReportCrash(crashBoth1)
    39  
    40  	crashBoth2 := testCrash(build2, 4)
    41  	crashBoth2.Title = "both managers"
    42  	client.ReportCrash(crashBoth2)
    43  
    44  	// Make sure all those bugs are present on the main page.
    45  	reply, err := c.AuthGET(AccessAdmin, "/test1")
    46  	c.expectOK(err)
    47  	for _, title := range []string{crash1.Title, crash2.Title, crashBoth1.Title} {
    48  		if !bytes.Contains(reply, []byte(title)) {
    49  			t.Fatalf("%#v is not contained on the main page", title)
    50  		}
    51  	}
    52  
    53  	// Check that filtering on the main page works.
    54  	reply, err = c.AuthGET(AccessAdmin, "/test1?only_manager="+build1.Manager)
    55  	c.expectOK(err)
    56  	for _, title := range []string{crash2.Title, crashBoth1.Title} {
    57  		if bytes.Contains(reply, []byte(title)) {
    58  			t.Fatalf("%#v is contained on the main page", title)
    59  		}
    60  	}
    61  	if !bytes.Contains(reply, []byte(crash1.Title)) {
    62  		t.Fatalf("%#v is not contained on the main page", crash1.Title)
    63  	}
    64  
    65  	// Invalidate all these bugs.
    66  	polledBugs := client.pollBugs(3)
    67  	for _, bug := range polledBugs {
    68  		client.updateBug(bug.ID, dashapi.BugStatusInvalid, "")
    69  	}
    70  
    71  	// Verify that the filtering works on the invalid bugs page.
    72  	reply, err = c.AuthGET(AccessAdmin, "/test1/invalid?only_manager="+build2.Manager)
    73  	c.expectOK(err)
    74  	for _, title := range []string{crash1.Title, crashBoth1.Title} {
    75  		if bytes.Contains(reply, []byte(title)) {
    76  			t.Fatalf("%#v is contained on the invalid bugs page", title)
    77  		}
    78  	}
    79  	if !bytes.Contains(reply, []byte(crash2.Title)) {
    80  		t.Fatalf("%#v is not contained on the invalid bugs page", crash2.Title)
    81  	}
    82  }
    83  
    84  const (
    85  	subsystemA = "subsystemA"
    86  	subsystemB = "subsystemB"
    87  )
    88  
    89  func TestSubsystemFilterMain(t *testing.T) {
    90  	c := NewCtx(t)
    91  	defer c.Close()
    92  
    93  	client := c.client
    94  	build := testBuild(1)
    95  	client.UploadBuild(build)
    96  
    97  	crash1 := testCrash(build, 1)
    98  	crash1.Title = "first bug"
    99  	crash1.GuiltyFiles = []string{"a.c"}
   100  	client.ReportCrash(crash1)
   101  
   102  	crash2 := testCrash(build, 2)
   103  	crash2.Title = "second bug"
   104  	crash2.GuiltyFiles = []string{"b.c"}
   105  	client.ReportCrash(crash2)
   106  
   107  	client.pollBugs(2)
   108  	// Make sure all those bugs are present on the main page.
   109  	reply, err := c.AuthGET(AccessAdmin, "/test1")
   110  	c.expectOK(err)
   111  	for _, title := range []string{crash1.Title, crash2.Title} {
   112  		if !bytes.Contains(reply, []byte(title)) {
   113  			t.Fatalf("%#v is not contained on the main page", title)
   114  		}
   115  	}
   116  	// Check that filtering on the main page works.
   117  	reply, err = c.AuthGET(AccessAdmin, "/test1?label=subsystems:"+subsystemA)
   118  	c.expectOK(err)
   119  	for _, title := range []string{crash2.Title} {
   120  		if bytes.Contains(reply, []byte(title)) {
   121  			t.Fatalf("%#v is contained on the main page", title)
   122  		}
   123  	}
   124  	if !bytes.Contains(reply, []byte(crash1.Title)) {
   125  		t.Fatalf("%#v is not contained on the main page", crash2.Title)
   126  	}
   127  }
   128  
   129  func TestSubsystemFilterTerminal(t *testing.T) {
   130  	c := NewCtx(t)
   131  	defer c.Close()
   132  
   133  	client := c.client
   134  	build := testBuild(1)
   135  	client.UploadBuild(build)
   136  
   137  	crash1 := testCrash(build, 1)
   138  	crash1.Title = "first bug"
   139  	crash1.GuiltyFiles = []string{"a.c"}
   140  	client.ReportCrash(crash1)
   141  
   142  	crash2 := testCrash(build, 2)
   143  	crash2.Title = "second bug"
   144  	crash2.GuiltyFiles = []string{"b.c"}
   145  	client.ReportCrash(crash2)
   146  
   147  	// Invalidate all these bugs.
   148  	polledBugs := client.pollBugs(2)
   149  	for _, bug := range polledBugs {
   150  		client.updateBug(bug.ID, dashapi.BugStatusInvalid, "")
   151  	}
   152  
   153  	// Verify that the filtering works on the invalid bugs page.
   154  	reply, err := c.AuthGET(AccessAdmin, "/test1/invalid?label=subsystems:"+subsystemB)
   155  	c.expectOK(err)
   156  	for _, title := range []string{crash1.Title} {
   157  		if bytes.Contains(reply, []byte(title)) {
   158  			t.Fatalf("%#v is contained on the invalid bugs page", title)
   159  		}
   160  	}
   161  	if !bytes.Contains(reply, []byte(crash2.Title)) {
   162  		t.Fatalf("%#v is not contained on the invalid bugs page", crash2.Title)
   163  	}
   164  }
   165  
   166  func TestMainBugFilters(t *testing.T) {
   167  	c := NewCtx(t)
   168  	defer c.Close()
   169  
   170  	client := c.client
   171  	build1 := testBuild(1)
   172  	build1.Manager = "manager-name-123"
   173  	client.UploadBuild(build1)
   174  
   175  	crash1 := testCrash(build1, 1)
   176  	crash1.Title = "my-crash-title"
   177  	client.ReportCrash(crash1)
   178  	client.pollBugs(1)
   179  
   180  	// The normal main page.
   181  	reply, err := c.AuthGET(AccessAdmin, "/test1")
   182  	c.expectOK(err)
   183  	assert.Contains(t, string(reply), build1.Manager)
   184  	assert.NotContains(t, string(reply), "Applied filters")
   185  
   186  	reply, err = c.AuthGET(AccessAdmin, "/test1?label=subsystems:abcd")
   187  	c.expectOK(err)
   188  	assert.NotContains(t, string(reply), build1.Manager) // managers are hidden
   189  	assert.Contains(t, string(reply), "Applied filters") // we're seeing a prompt to disable the filter
   190  	assert.NotContains(t, string(reply), crash1.Title)   // the bug does not belong to the subsystem
   191  
   192  	reply, err = c.AuthGET(AccessAdmin, "/test1?no_subsystem=true")
   193  	c.expectOK(err)
   194  	assert.Contains(t, string(reply), crash1.Title) // the bug has no subsystems
   195  }
   196  
   197  func TestSubsystemsList(t *testing.T) {
   198  	c := NewCtx(t)
   199  	defer c.Close()
   200  
   201  	client := c.client
   202  	build := testBuild(1)
   203  	client.UploadBuild(build)
   204  
   205  	crash1 := testCrash(build, 1)
   206  	crash1.GuiltyFiles = []string{"a.c"}
   207  	client.ReportCrash(crash1)
   208  	client.pollBug()
   209  
   210  	crash2 := testCrash(build, 2)
   211  	crash2.GuiltyFiles = []string{"b.c"}
   212  	client.ReportCrash(crash2)
   213  	client.updateBug(client.pollBug().ID, dashapi.BugStatusInvalid, "")
   214  
   215  	_, err := c.AuthGET(AccessUser, "/cron/refresh_subsystems")
   216  	c.expectOK(err)
   217  
   218  	reply, err := c.AuthGET(AccessAdmin, "/test1/subsystems")
   219  	c.expectOK(err)
   220  	assert.Contains(t, string(reply), "subsystemA")
   221  	assert.NotContains(t, string(reply), "subsystemB")
   222  
   223  	reply, err = c.AuthGET(AccessAdmin, "/test1/subsystems?all=true")
   224  	c.expectOK(err)
   225  	assert.Contains(t, string(reply), "subsystemA")
   226  	assert.Contains(t, string(reply), "subsystemB")
   227  }
   228  
   229  func TestSubsystemPage(t *testing.T) {
   230  	c := NewCtx(t)
   231  	defer c.Close()
   232  
   233  	client := c.client
   234  	build := testBuild(1)
   235  	client.UploadBuild(build)
   236  
   237  	crash1 := testCrash(build, 1)
   238  	crash1.Title = "test crash title"
   239  	crash1.GuiltyFiles = []string{"a.c"}
   240  	client.ReportCrash(crash1)
   241  	client.pollBug()
   242  
   243  	crash2 := testCrash(build, 2)
   244  	crash2.GuiltyFiles = []string{"b.c"}
   245  	client.ReportCrash(crash2)
   246  	crash2.Title = "crash that must not be present"
   247  	client.updateBug(client.pollBug().ID, dashapi.BugStatusInvalid, "")
   248  
   249  	reply, err := c.AuthGET(AccessAdmin, "/test1/s/subsystemA")
   250  	c.expectOK(err)
   251  	assert.Contains(t, string(reply), crash1.Title)
   252  	assert.NotContains(t, string(reply), crash2.Title)
   253  }
   254  
   255  func TestMultiLabelFilter(t *testing.T) {
   256  	c := NewCtx(t)
   257  	defer c.Close()
   258  
   259  	client := c.makeClient(clientPublicEmail, keyPublicEmail, true)
   260  	mailingList := c.config().Namespaces["access-public-email"].Reporting[0].Config.(*EmailConfig).Email
   261  
   262  	build1 := testBuild(1)
   263  	build1.Manager = "manager-name-123"
   264  	client.UploadBuild(build1)
   265  
   266  	crash1 := testCrash(build1, 1)
   267  	crash1.GuiltyFiles = []string{"a.c"}
   268  	crash1.Title = "crash-with-subsystem-A"
   269  	client.ReportCrash(crash1)
   270  	c.pollEmailBug()
   271  
   272  	crash2 := testCrash(build1, 2)
   273  	crash2.GuiltyFiles = []string{"a.c"}
   274  	crash2.Title = "prio-crash-subsystem-A"
   275  	client.ReportCrash(crash2)
   276  
   277  	c.incomingEmail(c.pollEmailBug().Sender, "#syz set prio: low\n",
   278  		EmailOptFrom("test@requester.com"), EmailOptCC([]string{mailingList}))
   279  
   280  	// The normal main page.
   281  	reply, err := c.AuthGET(AccessAdmin, "/access-public-email")
   282  	c.expectOK(err)
   283  	assert.Contains(t, string(reply), build1.Manager)
   284  	assert.NotContains(t, string(reply), "Applied filters")
   285  
   286  	reply, err = c.AuthGET(AccessAdmin, "/access-public-email?label=subsystems:subsystemA")
   287  	c.expectOK(err)
   288  	assert.Contains(t, string(reply), "Applied filters") // we're seeing a prompt to disable the filter
   289  	assert.Contains(t, string(reply), crash1.Title)
   290  	assert.Contains(t, string(reply), crash2.Title)
   291  
   292  	// Test filters together.
   293  	reply, err = c.AuthGET(AccessAdmin, "/access-public-email?label=subsystems:subsystemA&&label=prio:low")
   294  	c.expectOK(err)
   295  	assert.NotContains(t, string(reply), crash1.Title)
   296  	assert.Contains(t, string(reply), crash2.Title)
   297  
   298  	// Ensure we provide links that drop labels.
   299  	assert.NotContains(t, string(reply), "/access-public-email?label=subsystems:subsystemA\"")
   300  	assert.NotContains(t, string(reply), "/access-public-email?label=prop:low\"")
   301  }
   302  
   303  func TestAdminJobList(t *testing.T) {
   304  	c := NewCtx(t)
   305  	defer c.Close()
   306  
   307  	client := c.client2
   308  	build := testBuild(1)
   309  	client.UploadBuild(build)
   310  
   311  	crash := testCrash(build, 1)
   312  	crash.Title = "some bug title"
   313  	crash.GuiltyFiles = []string{"a.c"}
   314  	crash.ReproOpts = []byte("repro opts")
   315  	crash.ReproSyz = []byte("repro syz")
   316  	crash.ReproC = []byte("repro C")
   317  	client.ReportCrash(crash)
   318  	client.pollEmailBug()
   319  
   320  	c.advanceTime(24 * time.Hour)
   321  
   322  	pollResp := client.pollSpecificJobs(build.Manager, dashapi.ManagerJobs{BisectCause: true})
   323  	c.expectNE(pollResp.ID, "")
   324  
   325  	causeJobsLink := "/admin?job_type=1"
   326  	fixJobsLink := "/admin?job_type=2"
   327  	reply, err := c.AuthGET(AccessAdmin, "/admin")
   328  	c.expectOK(err)
   329  	assert.Contains(t, string(reply), causeJobsLink)
   330  	assert.Contains(t, string(reply), fixJobsLink)
   331  
   332  	// Verify the bug is in the bisect cause jobs list.
   333  	reply, err = c.AuthGET(AccessAdmin, causeJobsLink)
   334  	c.expectOK(err)
   335  	assert.Contains(t, string(reply), crash.Title)
   336  
   337  	// Verify the bug is NOT in the fix jobs list.
   338  	reply, err = c.AuthGET(AccessAdmin, fixJobsLink)
   339  	c.expectOK(err)
   340  	assert.NotContains(t, string(reply), crash.Title)
   341  }
   342  
   343  func TestSubsystemsPageRedirect(t *testing.T) {
   344  	c := NewCtx(t)
   345  	defer c.Close()
   346  
   347  	// Verify that the normal subsystem page works.
   348  	_, err := c.AuthGET(AccessAdmin, "/access-public-email/s/subsystemA")
   349  	c.expectOK(err)
   350  
   351  	// Verify that the old subsystem name points to the new one.
   352  	_, err = c.AuthGET(AccessAdmin, "/access-public-email/s/oldSubsystem")
   353  	var httpErr *HTTPError
   354  	c.expectTrue(errors.As(err, &httpErr))
   355  	c.expectEQ(httpErr.Code, http.StatusMovedPermanently)
   356  	c.expectEQ(httpErr.Headers["Location"], []string{"/access-public-email/s/subsystemA"})
   357  }
   358  
   359  func TestNoThrottle(t *testing.T) {
   360  	c := NewCtx(t)
   361  	defer c.Close()
   362  
   363  	assert.True(t, c.config().Throttle.Empty())
   364  	for i := 0; i < 10; i++ {
   365  		c.advanceTime(time.Millisecond)
   366  		_, err := c.AuthGET(AccessPublic, "/access-public-email")
   367  		c.expectOK(err)
   368  	}
   369  }
   370  
   371  func TestThrottle(t *testing.T) {
   372  	c := NewCtx(t)
   373  	defer c.Close()
   374  
   375  	c.transformContext = func(c context.Context) context.Context {
   376  		newConfig := *getConfig(c)
   377  		newConfig.Throttle = ThrottleConfig{
   378  			Window: 10 * time.Second,
   379  			Limit:  10,
   380  		}
   381  		return contextWithConfig(c, &newConfig)
   382  	}
   383  
   384  	// Adhere to the limit.
   385  	for i := 0; i < 15; i++ {
   386  		c.advanceTime(time.Second)
   387  		_, err := c.AuthGET(AccessPublic, "/access-public-email")
   388  		c.expectOK(err)
   389  	}
   390  
   391  	// Break the limit.
   392  	c.advanceTime(time.Millisecond)
   393  	_, err := c.AuthGET(AccessPublic, "/access-public-email")
   394  	var httpErr *HTTPError
   395  	c.expectTrue(errors.As(err, &httpErr))
   396  	c.expectEQ(httpErr.Code, http.StatusTooManyRequests)
   397  
   398  	// Still too frequent requests.
   399  	c.advanceTime(time.Millisecond)
   400  	_, err = c.AuthGET(AccessPublic, "/access-public-email")
   401  	c.expectTrue(err != nil)
   402  
   403  	// Wait a bit.
   404  	c.advanceTime(3 * time.Second)
   405  	_, err = c.AuthGET(AccessPublic, "/access-public-email")
   406  	c.expectOK(err)
   407  }
   408  
   409  func TestManagerPage(t *testing.T) {
   410  	c := NewCtx(t)
   411  	defer c.Close()
   412  
   413  	const firstManager = "manager-name"
   414  	const secondManager = "another-manager-name"
   415  
   416  	client := c.makeClient(clientPublicEmail, keyPublicEmail, true)
   417  	build1 := testBuild(1)
   418  	build1.Manager = firstManager
   419  	c.expectOK(client.UploadBuild(build1))
   420  
   421  	c.advanceTime(time.Hour)
   422  	build2 := testBuild(2)
   423  	build2.Manager = firstManager
   424  	buildErrorReq := &dashapi.BuildErrorReq{
   425  		Build: *build2,
   426  		Crash: dashapi.Crash{
   427  			Title:  "failed build 1",
   428  			Report: []byte("report\n"),
   429  			Log:    []byte("log\n"),
   430  		},
   431  	}
   432  	c.expectOK(client.ReportBuildError(buildErrorReq))
   433  	c.pollEmailBug()
   434  
   435  	c.advanceTime(time.Hour)
   436  	build3 := testBuild(3)
   437  	build3.Manager = firstManager
   438  	c.expectOK(client.UploadBuild(build3))
   439  
   440  	// And one more build from a different manager.
   441  	c.advanceTime(time.Hour)
   442  	build4 := testBuild(4)
   443  	build4.Manager = secondManager
   444  	c.expectOK(client.UploadBuild(build4))
   445  
   446  	// Query the first manager.
   447  	reply, err := c.AuthGET(AccessPublic, "/access-public-email/manager/"+firstManager)
   448  	c.expectOK(err)
   449  	assert.Contains(t, string(reply), "kernel_commit_title1")
   450  	assert.NotContains(t, string(reply), "kernel_commit_title2") // build error
   451  	assert.Contains(t, string(reply), "kernel_commit_title3")
   452  	assert.NotContains(t, string(reply), "kernel_commit_title4") // another manager
   453  
   454  	// Query the second manager.
   455  	reply, err = c.AuthGET(AccessPublic, "/access-public-email/manager/"+secondManager)
   456  	c.expectOK(err)
   457  	assert.NotContains(t, string(reply), "kernel_commit_title1")
   458  	assert.NotContains(t, string(reply), "kernel_commit_title2")
   459  	assert.NotContains(t, string(reply), "kernel_commit_title3")
   460  	assert.Contains(t, string(reply), "kernel_commit_title4") // another manager
   461  
   462  	// Query unknown manager.
   463  	_, err = c.AuthGET(AccessPublic, "/access-public-email/manager/abcd")
   464  	var httpErr *HTTPError
   465  	c.expectTrue(errors.As(err, &httpErr))
   466  	c.expectEQ(httpErr.Code, http.StatusBadRequest)
   467  }