github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/email-reporter/handler_test.go (about)

     1  // Copyright 2025 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  	"context"
     8  	"testing"
     9  
    10  	"github.com/google/syzkaller/pkg/email"
    11  	"github.com/google/syzkaller/syz-cluster/pkg/api"
    12  	"github.com/google/syzkaller/syz-cluster/pkg/app"
    13  	"github.com/google/syzkaller/syz-cluster/pkg/controller"
    14  	"github.com/google/syzkaller/syz-cluster/pkg/emailclient"
    15  	"github.com/google/syzkaller/syz-cluster/pkg/reporter"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  var testEmailConfig = emailclient.TestEmailConfig()
    21  
    22  func TestModerationReportFlow(t *testing.T) {
    23  	env, ctx := app.TestEnvironment(t)
    24  	testSeries := controller.DummySeries()
    25  	handler, _, emailServer := setupHandlerTest(t, env, ctx, testSeries)
    26  
    27  	report, err := handler.PollAndReport(ctx)
    28  	assert.NoError(t, err)
    29  
    30  	receivedEmail := emailServer.email()
    31  	assert.NotNil(t, receivedEmail, "a moderation email must be sent")
    32  	receivedEmail.Body = nil // for now don't validate the body
    33  	assert.Equal(t, &emailclient.Email{
    34  		To:      []string{testEmailConfig.ModerationList},
    35  		Cc:      []string{testEmailConfig.ArchiveList},
    36  		Subject: "[moderation/CI] Re: " + testSeries.Title,
    37  		BugID:   report.ID,
    38  		// Note that InReplyTo and Cc are nil.
    39  	}, receivedEmail)
    40  
    41  	// Emulate an "upstream" command.
    42  	err = handler.IncomingEmail(ctx, &email.Email{
    43  		BugIDs: []string{report.ID},
    44  		Commands: []*email.SingleCommand{
    45  			{
    46  				Command: email.CmdUpstream,
    47  			},
    48  		},
    49  	})
    50  	assert.NoError(t, err)
    51  
    52  	// The report must be sent upstream.
    53  	report, err = handler.PollAndReport(ctx)
    54  	assert.NoError(t, err)
    55  
    56  	receivedEmail = emailServer.email()
    57  	assert.NotNil(t, receivedEmail, "an email must be sent upstream")
    58  	receivedEmail.Body = nil
    59  	assert.Equal(t, &emailclient.Email{
    60  		To:        testSeries.Cc,
    61  		Cc:        append([]string{testEmailConfig.ArchiveList}, testEmailConfig.ReportCC...),
    62  		Subject:   "[name] Re: " + testSeries.Title,
    63  		InReplyTo: testSeries.ExtID,
    64  		BugID:     report.ID,
    65  	}, receivedEmail)
    66  }
    67  
    68  func TestReportInvalidationFlow(t *testing.T) {
    69  	env, ctx := app.TestEnvironment(t)
    70  	testSeries := controller.DummySeries()
    71  	handler, _, emailServer := setupHandlerTest(t, env, ctx, testSeries)
    72  
    73  	report, err := handler.PollAndReport(ctx)
    74  	require.NoError(t, err)
    75  
    76  	receivedEmail := emailServer.email()
    77  	require.NotNil(t, receivedEmail, "a moderation email must be sent")
    78  	receivedEmail.Body = nil // for now don't validate the body
    79  
    80  	// Emulate an "upstream" command.
    81  	err = handler.IncomingEmail(ctx, &email.Email{
    82  		BugIDs: []string{report.ID},
    83  		Commands: []*email.SingleCommand{
    84  			{
    85  				Command: email.CmdInvalid,
    86  			},
    87  		},
    88  	})
    89  	require.NoError(t, err)
    90  
    91  	// The report must be not sent upstream.
    92  	report, err = handler.PollAndReport(ctx)
    93  	require.NoError(t, err)
    94  	assert.Nil(t, report)
    95  
    96  	receivedEmail = emailServer.email()
    97  	assert.Nil(t, receivedEmail, "an email must not be sent upstream")
    98  }
    99  
   100  func TestInvalidReply(t *testing.T) {
   101  	env, ctx := app.TestEnvironment(t)
   102  	testSeries := controller.DummySeries()
   103  	handler, _, emailServer := setupHandlerTest(t, env, ctx, testSeries)
   104  
   105  	report, err := handler.PollAndReport(ctx)
   106  	assert.NoError(t, err)
   107  
   108  	receivedEmail := emailServer.email()
   109  	assert.NotNil(t, receivedEmail, "a moderation email must be sent")
   110  	receivedEmail.Body = nil
   111  
   112  	t.Run("unrelated email", func(t *testing.T) {
   113  		err = handler.IncomingEmail(ctx, &email.Email{
   114  			Commands: []*email.SingleCommand{
   115  				{
   116  					Command: email.CmdUpstream,
   117  				},
   118  			},
   119  		})
   120  		assert.NoError(t, err)
   121  		_, err = handler.PollAndReport(ctx)
   122  		assert.NoError(t, err)
   123  		// No email must be sent in reply.
   124  		assert.Nil(t, emailServer.email())
   125  	})
   126  
   127  	t.Run("unsupported command", func(t *testing.T) {
   128  		err := handler.IncomingEmail(ctx, &email.Email{
   129  			Author:    "user@email.com",
   130  			Subject:   "Command",
   131  			BugIDs:    []string{report.ID},
   132  			Cc:        []string{"a@a.com", "b@b.com"},
   133  			MessageID: "user-reply-msg-id",
   134  			Commands: []*email.SingleCommand{
   135  				{
   136  					Command: email.CmdFix,
   137  				},
   138  			},
   139  			Body: `#syz fix: abcd`,
   140  		})
   141  		assert.NoError(t, err)
   142  		reply := emailServer.email()
   143  		assert.NotNil(t, reply)
   144  		assert.Equal(t, &emailclient.Email{
   145  			To:        []string{"user@email.com"},
   146  			Cc:        []string{"a@a.com", "b@b.com"},
   147  			Subject:   "Re: Command",
   148  			InReplyTo: "user-reply-msg-id",
   149  			Body: []byte(`> #syz fix: abcd
   150  
   151  Unknown command
   152  
   153  `),
   154  		}, reply)
   155  	})
   156  
   157  	t.Run("own email", func(t *testing.T) {
   158  		err = handler.IncomingEmail(ctx, &email.Email{
   159  			OwnEmail: true,
   160  			BugIDs:   []string{report.ID},
   161  			Commands: []*email.SingleCommand{
   162  				{
   163  					Command: email.CmdUpstream,
   164  				},
   165  			},
   166  		})
   167  		assert.NoError(t, err)
   168  		_, err = handler.PollAndReport(ctx)
   169  		assert.NoError(t, err)
   170  		// No email must be sent in reply.
   171  		assert.Nil(t, emailServer.email())
   172  	})
   173  
   174  	t.Run("forwarded email", func(t *testing.T) {
   175  		err = handler.IncomingEmail(ctx, &email.Email{
   176  			Subject:  email.ForwardedPrefix + "abcd",
   177  			OwnEmail: true,
   178  			BugIDs:   []string{report.ID},
   179  			Commands: []*email.SingleCommand{
   180  				{
   181  					Command: email.CmdUpstream,
   182  				},
   183  			},
   184  		})
   185  		assert.NoError(t, err)
   186  		_, err = handler.PollAndReport(ctx)
   187  		assert.NoError(t, err)
   188  		assert.NotNil(t, emailServer.email())
   189  	})
   190  }
   191  
   192  func setupHandlerTest(t *testing.T, env *app.AppEnvironment, ctx context.Context,
   193  	series *api.Series) (*Handler, *api.ReporterClient, *fakeSender) {
   194  	client := controller.TestServer(t, env)
   195  	controller.FakeSeriesWithFindings(t, ctx, env, client, series)
   196  
   197  	generator := reporter.NewGenerator(env)
   198  	err := generator.Process(ctx, 1)
   199  	assert.NoError(t, err)
   200  
   201  	emailServer := makeFakeSender()
   202  	reporterClient := reporter.TestServer(t, env)
   203  	handler := &Handler{
   204  		reporter:    api.LKMLReporter,
   205  		apiClient:   reporterClient,
   206  		emailConfig: testEmailConfig,
   207  		sender:      emailServer.send,
   208  	}
   209  	return handler, reporterClient, emailServer
   210  }
   211  
   212  type fakeSender struct {
   213  	ch chan *emailclient.Email
   214  }
   215  
   216  func makeFakeSender() *fakeSender {
   217  	return &fakeSender{
   218  		ch: make(chan *emailclient.Email, 16),
   219  	}
   220  }
   221  
   222  func (f *fakeSender) send(ctx context.Context, e *emailclient.Email) (string, error) {
   223  	f.ch <- e
   224  	return "email-id", nil
   225  }
   226  
   227  func (f *fakeSender) email() *emailclient.Email {
   228  	select {
   229  	case e := <-f.ch:
   230  		return e
   231  	default:
   232  		return nil
   233  	}
   234  }