diff --git a/.golangci.yml b/.golangci.yml index 3141168..7054222 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,13 @@ linters: - errorlint - govet - ineffassign + - makezero + - misspell + - nilerr + - nilnil + - prealloc - staticcheck + - unparam - unused settings: diff --git a/cmd/gog/main_test.go b/cmd/gog/main_test.go index fe2b1fb..8d34050 100644 --- a/cmd/gog/main_test.go +++ b/cmd/gog/main_test.go @@ -8,6 +8,8 @@ import ( ) func TestMainHelpDoesNotExit(t *testing.T) { + t.Helper() + origArgs := os.Args defer func() { os.Args = origArgs }() diff --git a/internal/cmd/calendar_time_test.go b/internal/cmd/calendar_time_test.go index e3af6fc..cd10654 100644 --- a/internal/cmd/calendar_time_test.go +++ b/internal/cmd/calendar_time_test.go @@ -3,6 +3,7 @@ package cmd import ( "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "strings" @@ -132,7 +133,7 @@ func TestCalendarTimeCmd_WithTimezoneFlag(t *testing.T) { // No server needed since we're using --timezone flag newCalendarService = func(context.Context, string) (*calendar.Service, error) { t.Fatal("should not call calendar service when --timezone is provided") - return nil, nil + return nil, errors.New("unexpected calendar service call") } out := captureStdout(t, func() { @@ -174,7 +175,7 @@ func TestCalendarTimeCmd_InvalidTimezone(t *testing.T) { // No server needed since we're testing error case newCalendarService = func(context.Context, string) (*calendar.Service, error) { t.Fatal("should not call calendar service when invalid timezone is provided") - return nil, nil + return nil, errors.New("unexpected calendar service call") } stderr := captureStderr(t, func() { diff --git a/internal/cmd/execute_export_cmds_test.go b/internal/cmd/execute_export_cmds_test.go index 4ea1295..fd94c95 100644 --- a/internal/cmd/execute_export_cmds_test.go +++ b/internal/cmd/execute_export_cmds_test.go @@ -3,6 +3,7 @@ package cmd import ( "context" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -252,7 +253,7 @@ func TestExecute_DocsExport_RejectsNonDoc(t *testing.T) { called := false driveExportDownload = func(context.Context, *drive.Service, string, string) (*http.Response, error) { called = true - return nil, nil + return nil, errors.New("unexpected export call") } err = Execute([]string{"--json", "--account", "a@b.com", "docs", "export", "x", "--out", filepath.Join(t.TempDir(), "out")}) diff --git a/internal/cmd/execute_gmail_attachment_test.go b/internal/cmd/execute_gmail_attachment_test.go index 0d243b9..7d02524 100644 --- a/internal/cmd/execute_gmail_attachment_test.go +++ b/internal/cmd/execute_gmail_attachment_test.go @@ -54,7 +54,7 @@ func TestExecute_GmailAttachment_OutPath_JSON(t *testing.T) { outPath := filepath.Join(t.TempDir(), "a.bin") - run := func() (string, map[string]any) { + run := func() map[string]any { out := captureStdout(t, func() { _ = captureStderr(t, func() { if execErr := Execute([]string{ @@ -71,10 +71,10 @@ func TestExecute_GmailAttachment_OutPath_JSON(t *testing.T) { if unmarshalErr := json.Unmarshal([]byte(out), &parsed); unmarshalErr != nil { t.Fatalf("json parse: %v\nout=%q", unmarshalErr, out) } - return out, parsed + return parsed } - _, parsed1 := run() + parsed1 := run() if atomic.LoadInt32(&attachmentCalls) != 1 { t.Fatalf("attachmentCalls=%d", attachmentCalls) } @@ -96,7 +96,7 @@ func TestExecute_GmailAttachment_OutPath_JSON(t *testing.T) { t.Fatalf("content=%q", string(b)) } - _, parsed2 := run() + parsed2 := run() if atomic.LoadInt32(&attachmentCalls) != 1 { t.Fatalf("attachmentCalls=%d", attachmentCalls) } diff --git a/internal/cmd/gmail_watch_cmds.go b/internal/cmd/gmail_watch_cmds.go index 2411754..e6a77d8 100644 --- a/internal/cmd/gmail_watch_cmds.go +++ b/internal/cmd/gmail_watch_cmds.go @@ -18,8 +18,9 @@ import ( ) var ( - newOIDCValidator = idtoken.NewValidator - listenAndServe = func(srv *http.Server) error { return srv.ListenAndServe() } + newOIDCValidator = idtoken.NewValidator + listenAndServe = func(srv *http.Server) error { return srv.ListenAndServe() } + errNoHookConfigured = errors.New("no hook configured") ) type GmailWatchCmd struct { @@ -55,7 +56,11 @@ func (c *GmailWatchStartCmd) Run(ctx context.Context, kctx *kong.Context, flags maxChanged := flagProvided(kctx, "max-bytes") hook, err := hookFromFlags(c.HookURL, c.HookToken, c.IncludeBody, c.MaxBytes, maxChanged, false) if err != nil { - return err + if errors.Is(err, errNoHookConfigured) { + hook = nil + } else { + return err + } } svc, err := newGmailService(ctx, account) @@ -248,7 +253,11 @@ func (c *GmailWatchServeCmd) Run(ctx context.Context, kctx *kong.Context, flags maxChanged := flagProvided(kctx, "max-bytes") hook, err := hookFromFlags(hookURL, hookToken, includeBody, maxBytes, maxChanged, true) if err != nil { - return err + if errors.Is(err, errNoHookConfigured) { + hook = nil + } else { + return err + } } if c.SaveHook && hook != nil { if updateErr := store.Update(func(s *gmailWatchState) error { @@ -405,7 +414,7 @@ func hookFromFlags(url, token string, includeBody bool, maxBytes int, maxBytesCh if !allowNoHook && (includeBody || maxBytesChanged) { return nil, usage("--hook-url required when setting hook options") } - return nil, nil + return nil, errNoHookConfigured } if maxBytes <= 0 { if includeBody { diff --git a/internal/cmd/gmail_watch_helpers_test.go b/internal/cmd/gmail_watch_helpers_test.go index 5d541f5..2fa314a 100644 --- a/internal/cmd/gmail_watch_helpers_test.go +++ b/internal/cmd/gmail_watch_helpers_test.go @@ -3,6 +3,7 @@ package cmd import ( "context" "encoding/json" + "errors" "io" "os" "strings" @@ -87,8 +88,8 @@ func TestHookFromFlags(t *testing.T) { t.Run("allow no hook", func(t *testing.T) { hook, err := hookFromFlags("", "", false, 0, false, true) - if err != nil { - t.Fatalf("unexpected error: %v", err) + if err == nil || !errors.Is(err, errNoHookConfigured) { + t.Fatalf("expected no hook error, got: %v", err) } if hook != nil { t.Fatalf("expected nil hook") diff --git a/internal/cmd/gmail_watch_server.go b/internal/cmd/gmail_watch_server.go index fd77c6e..74ec2d3 100644 --- a/internal/cmd/gmail_watch_server.go +++ b/internal/cmd/gmail_watch_server.go @@ -18,6 +18,8 @@ import ( "google.golang.org/api/idtoken" ) +var errNoNewMessages = errors.New("no new messages") + type gmailWatchServer struct { cfg gmailWatchServeConfig store *gmailWatchStore @@ -63,6 +65,10 @@ func (s *gmailWatchServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { result, err := s.handlePush(r.Context(), payload) if err != nil { + if errors.Is(err, errNoNewMessages) { + w.WriteHeader(http.StatusAccepted) + return + } s.warnf("watch: handle push failed: %v", err) w.WriteHeader(http.StatusInternalServerError) return @@ -141,7 +147,7 @@ func (s *gmailWatchServer) handlePush(ctx context.Context, payload gmailPushPayl return nil, err } if startID == 0 { - return nil, nil + return nil, errNoNewMessages } svc, err := s.newService(ctx, s.cfg.Account) diff --git a/internal/cmd/tasks_validation_test.go b/internal/cmd/tasks_validation_test.go index 4042f7f..8b79ae9 100644 --- a/internal/cmd/tasks_validation_test.go +++ b/internal/cmd/tasks_validation_test.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "strings" "testing" @@ -13,7 +14,7 @@ func TestExecute_TasksAdd_RequiresTitle(t *testing.T) { t.Cleanup(func() { newTasksService = origNew }) newTasksService = func(context.Context, string) (*tasks.Service, error) { t.Fatalf("expected validation to fail before creating service") - return nil, nil + return nil, errors.New("unexpected tasks service call") } _ = captureStderr(t, func() { @@ -29,7 +30,7 @@ func TestExecute_TasksUpdate_RequiresFields(t *testing.T) { t.Cleanup(func() { newTasksService = origNew }) newTasksService = func(context.Context, string) (*tasks.Service, error) { t.Fatalf("expected validation to fail before creating service") - return nil, nil + return nil, errors.New("unexpected tasks service call") } _ = captureStderr(t, func() { @@ -45,7 +46,7 @@ func TestExecute_TasksUpdate_RejectsInvalidStatus(t *testing.T) { t.Cleanup(func() { newTasksService = origNew }) newTasksService = func(context.Context, string) (*tasks.Service, error) { t.Fatalf("expected validation to fail before creating service") - return nil, nil + return nil, errors.New("unexpected tasks service call") } _ = captureStderr(t, func() { diff --git a/internal/googleauth/oauth_flow_authorize_test.go b/internal/googleauth/oauth_flow_authorize_test.go index 16381a1..4a46bfd 100644 --- a/internal/googleauth/oauth_flow_authorize_test.go +++ b/internal/googleauth/oauth_flow_authorize_test.go @@ -16,7 +16,7 @@ import ( "golang.org/x/oauth2" ) -func newTokenServer(t *testing.T, refreshToken string) *httptest.Server { +func newTokenServer(t *testing.T) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -39,7 +39,7 @@ func newTokenServer(t *testing.T, refreshToken string) *httptest.Server { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "access_token": "at", - "refresh_token": refreshToken, + "refresh_token": "rt", "token_type": "Bearer", "expires_in": 3600, }) @@ -68,7 +68,7 @@ func TestAuthorize_Manual_Success(t *testing.T) { } randomStateFn = func() (string, error) { return "state123", nil } - tokenSrv := newTokenServer(t, "rt") + tokenSrv := newTokenServer(t) defer tokenSrv.Close() oauthEndpoint = oauth2EndpointForTest(tokenSrv.URL) @@ -110,7 +110,7 @@ func TestAuthorize_Manual_StateMismatch(t *testing.T) { } randomStateFn = func() (string, error) { return "state123", nil } - tokenSrv := newTokenServer(t, "rt") + tokenSrv := newTokenServer(t) defer tokenSrv.Close() oauthEndpoint = oauth2EndpointForTest(tokenSrv.URL) @@ -148,7 +148,7 @@ func TestAuthorize_ServerFlow_Success(t *testing.T) { return config.ClientCredentials{ClientID: "id", ClientSecret: "secret"}, nil } - tokenSrv := newTokenServer(t, "rt") + tokenSrv := newTokenServer(t) defer tokenSrv.Close() oauthEndpoint = oauth2EndpointForTest(tokenSrv.URL) @@ -210,7 +210,7 @@ func TestAuthorize_ServerFlow_CallbackErrors(t *testing.T) { return config.ClientCredentials{ClientID: "id", ClientSecret: "secret"}, nil } - tokenSrv := newTokenServer(t, "rt") + tokenSrv := newTokenServer(t) defer tokenSrv.Close() oauthEndpoint = oauth2EndpointForTest(tokenSrv.URL)