| // Copyright 2023 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. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "net/http" |
| "testing" |
| "time" |
| |
| "github.com/google/syzkaller/dashboard/dashapi" |
| "github.com/stretchr/testify/assert" |
| ) |
| |
| func TestOnlyManagerFilter(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client |
| build1 := testBuild(1) |
| client.UploadBuild(build1) |
| build2 := testBuild(2) |
| client.UploadBuild(build2) |
| |
| crash1 := testCrash(build1, 1) |
| crash1.Title = "only the first manager" |
| client.ReportCrash(crash1) |
| |
| crash2 := testCrash(build2, 2) |
| crash2.Title = "only the second manager" |
| client.ReportCrash(crash2) |
| |
| crashBoth1 := testCrash(build1, 3) |
| crashBoth1.Title = "both managers" |
| client.ReportCrash(crashBoth1) |
| |
| crashBoth2 := testCrash(build2, 4) |
| crashBoth2.Title = "both managers" |
| client.ReportCrash(crashBoth2) |
| |
| // Make sure all those bugs are present on the main page. |
| reply, err := c.AuthGET(AccessAdmin, "/test1") |
| c.expectOK(err) |
| for _, title := range []string{crash1.Title, crash2.Title, crashBoth1.Title} { |
| if !bytes.Contains(reply, []byte(title)) { |
| t.Fatalf("%#v is not contained on the main page", title) |
| } |
| } |
| |
| // Check that filtering on the main page works. |
| reply, err = c.AuthGET(AccessAdmin, "/test1?only_manager="+build1.Manager) |
| c.expectOK(err) |
| for _, title := range []string{crash2.Title, crashBoth1.Title} { |
| if bytes.Contains(reply, []byte(title)) { |
| t.Fatalf("%#v is contained on the main page", title) |
| } |
| } |
| if !bytes.Contains(reply, []byte(crash1.Title)) { |
| t.Fatalf("%#v is not contained on the main page", crash1.Title) |
| } |
| |
| // Invalidate all these bugs. |
| polledBugs := client.pollBugs(3) |
| for _, bug := range polledBugs { |
| client.updateBug(bug.ID, dashapi.BugStatusInvalid, "") |
| } |
| |
| // Verify that the filtering works on the invalid bugs page. |
| reply, err = c.AuthGET(AccessAdmin, "/test1/invalid?only_manager="+build2.Manager) |
| c.expectOK(err) |
| for _, title := range []string{crash1.Title, crashBoth1.Title} { |
| if bytes.Contains(reply, []byte(title)) { |
| t.Fatalf("%#v is contained on the invalid bugs page", title) |
| } |
| } |
| if !bytes.Contains(reply, []byte(crash2.Title)) { |
| t.Fatalf("%#v is not contained on the invalid bugs page", crash2.Title) |
| } |
| } |
| |
| const ( |
| subsystemA = "subsystemA" |
| subsystemB = "subsystemB" |
| ) |
| |
| func TestSubsystemFilterMain(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client |
| build := testBuild(1) |
| client.UploadBuild(build) |
| |
| crash1 := testCrash(build, 1) |
| crash1.Title = "first bug" |
| crash1.GuiltyFiles = []string{"a.c"} |
| client.ReportCrash(crash1) |
| |
| crash2 := testCrash(build, 2) |
| crash2.Title = "second bug" |
| crash2.GuiltyFiles = []string{"b.c"} |
| client.ReportCrash(crash2) |
| |
| client.pollBugs(2) |
| // Make sure all those bugs are present on the main page. |
| reply, err := c.AuthGET(AccessAdmin, "/test1") |
| c.expectOK(err) |
| for _, title := range []string{crash1.Title, crash2.Title} { |
| if !bytes.Contains(reply, []byte(title)) { |
| t.Fatalf("%#v is not contained on the main page", title) |
| } |
| } |
| // Check that filtering on the main page works. |
| reply, err = c.AuthGET(AccessAdmin, "/test1?label=subsystems:"+subsystemA) |
| c.expectOK(err) |
| for _, title := range []string{crash2.Title} { |
| if bytes.Contains(reply, []byte(title)) { |
| t.Fatalf("%#v is contained on the main page", title) |
| } |
| } |
| if !bytes.Contains(reply, []byte(crash1.Title)) { |
| t.Fatalf("%#v is not contained on the main page", crash2.Title) |
| } |
| } |
| |
| func TestSubsystemFilterTerminal(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client |
| build := testBuild(1) |
| client.UploadBuild(build) |
| |
| crash1 := testCrash(build, 1) |
| crash1.Title = "first bug" |
| crash1.GuiltyFiles = []string{"a.c"} |
| client.ReportCrash(crash1) |
| |
| crash2 := testCrash(build, 2) |
| crash2.Title = "second bug" |
| crash2.GuiltyFiles = []string{"b.c"} |
| client.ReportCrash(crash2) |
| |
| // Invalidate all these bugs. |
| polledBugs := client.pollBugs(2) |
| for _, bug := range polledBugs { |
| client.updateBug(bug.ID, dashapi.BugStatusInvalid, "") |
| } |
| |
| // Verify that the filtering works on the invalid bugs page. |
| reply, err := c.AuthGET(AccessAdmin, "/test1/invalid?label=subsystems:"+subsystemB) |
| c.expectOK(err) |
| for _, title := range []string{crash1.Title} { |
| if bytes.Contains(reply, []byte(title)) { |
| t.Fatalf("%#v is contained on the invalid bugs page", title) |
| } |
| } |
| if !bytes.Contains(reply, []byte(crash2.Title)) { |
| t.Fatalf("%#v is not contained on the invalid bugs page", crash2.Title) |
| } |
| } |
| |
| func TestMainBugFilters(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client |
| build1 := testBuild(1) |
| build1.Manager = "manager-name-123" |
| client.UploadBuild(build1) |
| |
| crash1 := testCrash(build1, 1) |
| crash1.Title = "my-crash-title" |
| client.ReportCrash(crash1) |
| client.pollBugs(1) |
| |
| // The normal main page. |
| reply, err := c.AuthGET(AccessAdmin, "/test1") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), build1.Manager) |
| assert.NotContains(t, string(reply), "Applied filters") |
| |
| reply, err = c.AuthGET(AccessAdmin, "/test1?label=subsystems:abcd") |
| c.expectOK(err) |
| assert.NotContains(t, string(reply), build1.Manager) // managers are hidden |
| assert.Contains(t, string(reply), "Applied filters") // we're seeing a prompt to disable the filter |
| assert.NotContains(t, string(reply), crash1.Title) // the bug does not belong to the subsystem |
| |
| reply, err = c.AuthGET(AccessAdmin, "/test1?no_subsystem=true") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), crash1.Title) // the bug has no subsystems |
| } |
| |
| func TestSubsystemsList(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client |
| build := testBuild(1) |
| client.UploadBuild(build) |
| |
| crash1 := testCrash(build, 1) |
| crash1.GuiltyFiles = []string{"a.c"} |
| client.ReportCrash(crash1) |
| client.pollBug() |
| |
| crash2 := testCrash(build, 2) |
| crash2.GuiltyFiles = []string{"b.c"} |
| client.ReportCrash(crash2) |
| client.updateBug(client.pollBug().ID, dashapi.BugStatusInvalid, "") |
| |
| _, err := c.AuthGET(AccessUser, "/cron/refresh_subsystems") |
| c.expectOK(err) |
| |
| reply, err := c.AuthGET(AccessAdmin, "/test1/subsystems") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), "subsystemA") |
| assert.NotContains(t, string(reply), "subsystemB") |
| |
| reply, err = c.AuthGET(AccessAdmin, "/test1/subsystems?all=true") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), "subsystemA") |
| assert.Contains(t, string(reply), "subsystemB") |
| } |
| |
| func TestSubsystemPage(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client |
| build := testBuild(1) |
| client.UploadBuild(build) |
| |
| crash1 := testCrash(build, 1) |
| crash1.Title = "test crash title" |
| crash1.GuiltyFiles = []string{"a.c"} |
| client.ReportCrash(crash1) |
| client.pollBug() |
| |
| crash2 := testCrash(build, 2) |
| crash2.GuiltyFiles = []string{"b.c"} |
| client.ReportCrash(crash2) |
| crash2.Title = "crash that must not be present" |
| client.updateBug(client.pollBug().ID, dashapi.BugStatusInvalid, "") |
| |
| reply, err := c.AuthGET(AccessAdmin, "/test1/s/subsystemA") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), crash1.Title) |
| assert.NotContains(t, string(reply), crash2.Title) |
| } |
| |
| func TestMultiLabelFilter(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.makeClient(clientPublicEmail, keyPublicEmail, true) |
| mailingList := c.config().Namespaces["access-public-email"].Reporting[0].Config.(*EmailConfig).Email |
| |
| build1 := testBuild(1) |
| build1.Manager = "manager-name-123" |
| client.UploadBuild(build1) |
| |
| crash1 := testCrash(build1, 1) |
| crash1.GuiltyFiles = []string{"a.c"} |
| crash1.Title = "crash-with-subsystem-A" |
| client.ReportCrash(crash1) |
| c.pollEmailBug() |
| |
| crash2 := testCrash(build1, 2) |
| crash2.GuiltyFiles = []string{"a.c"} |
| crash2.Title = "prio-crash-subsystem-A" |
| client.ReportCrash(crash2) |
| |
| c.incomingEmail(c.pollEmailBug().Sender, "#syz set prio: low\n", |
| EmailOptFrom("test@requester.com"), EmailOptCC([]string{mailingList})) |
| |
| // The normal main page. |
| reply, err := c.AuthGET(AccessAdmin, "/access-public-email") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), build1.Manager) |
| assert.NotContains(t, string(reply), "Applied filters") |
| |
| reply, err = c.AuthGET(AccessAdmin, "/access-public-email?label=subsystems:subsystemA") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), "Applied filters") // we're seeing a prompt to disable the filter |
| assert.Contains(t, string(reply), crash1.Title) |
| assert.Contains(t, string(reply), crash2.Title) |
| |
| // Test filters together. |
| reply, err = c.AuthGET(AccessAdmin, "/access-public-email?label=subsystems:subsystemA&&label=prio:low") |
| c.expectOK(err) |
| assert.NotContains(t, string(reply), crash1.Title) |
| assert.Contains(t, string(reply), crash2.Title) |
| |
| // Ensure we provide links that drop labels. |
| assert.NotContains(t, string(reply), "/access-public-email?label=subsystems:subsystemA\"") |
| assert.NotContains(t, string(reply), "/access-public-email?label=prop:low\"") |
| } |
| |
| func TestAdminJobList(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| client := c.client2 |
| build := testBuild(1) |
| client.UploadBuild(build) |
| |
| crash := testCrash(build, 1) |
| crash.Title = "some bug title" |
| crash.GuiltyFiles = []string{"a.c"} |
| crash.ReproOpts = []byte("repro opts") |
| crash.ReproSyz = []byte("repro syz") |
| crash.ReproC = []byte("repro C") |
| client.ReportCrash(crash) |
| client.pollEmailBug() |
| |
| c.advanceTime(24 * time.Hour) |
| |
| pollResp := client.pollSpecificJobs(build.Manager, dashapi.ManagerJobs{BisectCause: true}) |
| c.expectNE(pollResp.ID, "") |
| |
| causeJobsLink := "/admin?job_type=1" |
| fixJobsLink := "/admin?job_type=2" |
| reply, err := c.AuthGET(AccessAdmin, "/admin") |
| c.expectOK(err) |
| assert.Contains(t, string(reply), causeJobsLink) |
| assert.Contains(t, string(reply), fixJobsLink) |
| |
| // Verify the bug is in the bisect cause jobs list. |
| reply, err = c.AuthGET(AccessAdmin, causeJobsLink) |
| c.expectOK(err) |
| assert.Contains(t, string(reply), crash.Title) |
| |
| // Verify the bug is NOT in the fix jobs list. |
| reply, err = c.AuthGET(AccessAdmin, fixJobsLink) |
| c.expectOK(err) |
| assert.NotContains(t, string(reply), crash.Title) |
| } |
| |
| func TestSubsystemsPageRedirect(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| // Verify that the normal subsystem page works. |
| _, err := c.AuthGET(AccessAdmin, "/access-public-email/s/subsystemA") |
| c.expectOK(err) |
| |
| // Verify that the old subsystem name points to the new one. |
| _, err = c.AuthGET(AccessAdmin, "/access-public-email/s/oldSubsystem") |
| var httpErr *HTTPError |
| c.expectTrue(errors.As(err, &httpErr)) |
| c.expectEQ(httpErr.Code, http.StatusMovedPermanently) |
| c.expectEQ(httpErr.Headers["Location"], []string{"/access-public-email/s/subsystemA"}) |
| } |
| |
| func TestNoThrottle(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| assert.True(t, c.config().Throttle.Empty()) |
| for i := 0; i < 10; i++ { |
| c.advanceTime(time.Millisecond) |
| _, err := c.AuthGET(AccessPublic, "/access-public-email") |
| c.expectOK(err) |
| } |
| } |
| |
| func TestThrottle(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| c.transformContext = func(c context.Context) context.Context { |
| newConfig := *getConfig(c) |
| newConfig.Throttle = ThrottleConfig{ |
| Window: 10 * time.Second, |
| Limit: 10, |
| } |
| return contextWithConfig(c, &newConfig) |
| } |
| |
| // Adhere to the limit. |
| for i := 0; i < 15; i++ { |
| c.advanceTime(time.Second) |
| _, err := c.AuthGET(AccessPublic, "/access-public-email") |
| c.expectOK(err) |
| } |
| |
| // Break the limit. |
| c.advanceTime(time.Millisecond) |
| _, err := c.AuthGET(AccessPublic, "/access-public-email") |
| var httpErr *HTTPError |
| c.expectTrue(errors.As(err, &httpErr)) |
| c.expectEQ(httpErr.Code, http.StatusTooManyRequests) |
| |
| // Still too frequent requests. |
| c.advanceTime(time.Millisecond) |
| _, err = c.AuthGET(AccessPublic, "/access-public-email") |
| c.expectTrue(err != nil) |
| |
| // Wait a bit. |
| c.advanceTime(3 * time.Second) |
| _, err = c.AuthGET(AccessPublic, "/access-public-email") |
| c.expectOK(err) |
| } |
| |
| func TestManagerPage(t *testing.T) { |
| c := NewCtx(t) |
| defer c.Close() |
| |
| const firstManager = "manager-name" |
| const secondManager = "another-manager-name" |
| |
| client := c.makeClient(clientPublicEmail, keyPublicEmail, true) |
| build1 := testBuild(1) |
| build1.Manager = firstManager |
| c.expectOK(client.UploadBuild(build1)) |
| |
| c.advanceTime(time.Hour) |
| build2 := testBuild(2) |
| build2.Manager = firstManager |
| buildErrorReq := &dashapi.BuildErrorReq{ |
| Build: *build2, |
| Crash: dashapi.Crash{ |
| Title: "failed build 1", |
| Report: []byte("report\n"), |
| Log: []byte("log\n"), |
| }, |
| } |
| c.expectOK(client.ReportBuildError(buildErrorReq)) |
| c.pollEmailBug() |
| |
| c.advanceTime(time.Hour) |
| build3 := testBuild(3) |
| build3.Manager = firstManager |
| c.expectOK(client.UploadBuild(build3)) |
| |
| // And one more build from a different manager. |
| c.advanceTime(time.Hour) |
| build4 := testBuild(4) |
| build4.Manager = secondManager |
| c.expectOK(client.UploadBuild(build4)) |
| |
| // Query the first manager. |
| reply, err := c.AuthGET(AccessPublic, "/access-public-email/manager/"+firstManager) |
| c.expectOK(err) |
| assert.Contains(t, string(reply), "kernel_commit_title1") |
| assert.NotContains(t, string(reply), "kernel_commit_title2") // build error |
| assert.Contains(t, string(reply), "kernel_commit_title3") |
| assert.NotContains(t, string(reply), "kernel_commit_title4") // another manager |
| |
| // Query the second manager. |
| reply, err = c.AuthGET(AccessPublic, "/access-public-email/manager/"+secondManager) |
| c.expectOK(err) |
| assert.NotContains(t, string(reply), "kernel_commit_title1") |
| assert.NotContains(t, string(reply), "kernel_commit_title2") |
| assert.NotContains(t, string(reply), "kernel_commit_title3") |
| assert.Contains(t, string(reply), "kernel_commit_title4") // another manager |
| |
| // Query unknown manager. |
| _, err = c.AuthGET(AccessPublic, "/access-public-email/manager/abcd") |
| var httpErr *HTTPError |
| c.expectTrue(errors.As(err, &httpErr)) |
| c.expectEQ(httpErr.Code, http.StatusBadRequest) |
| } |