From 89a5da6dbbcdce96db7aac4fff779292e29142a5 Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Fri, 8 May 2026 08:45:26 -0700 Subject: [PATCH 1/2] invert email-messages update behavior --- cmd/email_messages.go | 23 +++++----- cmd/email_messages_update_test.go | 70 ++++++++++++++++++------------- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/cmd/email_messages.go b/cmd/email_messages.go index c367475..1e16ba7 100644 --- a/cmd/email_messages.go +++ b/cmd/email_messages.go @@ -94,10 +94,7 @@ func runEmailMessagesUpdate(cfg *config.Config, id string, req loops.UpdateEmail return newAPIClient(cfg).UpdateEmailMessage(id, req) } -func resolveExpectedRevisionID(cfg *config.Config, id, supplied string) (string, error) { - if supplied != "" { - return supplied, nil - } +func fetchLatestRevisionID(cfg *config.Config, id string) (string, error) { msg, err := newAPIClient(cfg).GetEmailMessage(id) if err != nil { return "", fmt.Errorf("fetch current revision: %w", err) @@ -144,16 +141,19 @@ var emailMessagesUpdateCmd = &cobra.Command{ return err } - suppliedRevisionID, _ := cmd.Flags().GetString("expected-revision-id") + revisionID, _ := cmd.Flags().GetString("expected-revision-id") + force, _ := cmd.Flags().GetBool("force") cfg, err := loadConfig() if err != nil { return err } - expectedRevisionID, err := resolveExpectedRevisionID(cfg, args[0], suppliedRevisionID) - if err != nil { - return err + if force { + revisionID, err = fetchLatestRevisionID(cfg, args[0]) + if err != nil { + return err + } } req := loops.UpdateEmailMessageRequest{ @@ -166,7 +166,7 @@ var emailMessagesUpdateCmd = &cobra.Command{ LMX: params.LMX, }, Set: params.Set, - ExpectedRevisionID: expectedRevisionID, + ExpectedRevisionID: revisionID, } msg, err := runEmailMessagesUpdate(cfg, args[0], req) @@ -260,7 +260,10 @@ func init() { emailMessagesCmd.AddCommand(emailMessagesGetCmd) addEmailMessageFieldFlags(emailMessagesUpdateCmd) - emailMessagesUpdateCmd.Flags().StringP("expected-revision-id", "r", "", "Last-seen contentRevisionId. If omitted, the CLI fetches the current revision before posting.") + emailMessagesUpdateCmd.Flags().StringP("expected-revision-id", "r", "", "Last-seen contentRevisionId. Get this from a prior 'email-messages get'. Mutually exclusive with --force.") + emailMessagesUpdateCmd.Flags().BoolP("force", "f", false, "Fetch the current revision and use it (overwrites any concurrent edits). Mutually exclusive with --expected-revision-id.") + emailMessagesUpdateCmd.MarkFlagsMutuallyExclusive("expected-revision-id", "force") + emailMessagesUpdateCmd.MarkFlagsOneRequired("expected-revision-id", "force") emailMessagesUpdateCmd.MarkFlagsOneRequired("subject", "preview-text", "from-name", "from-email", "reply-to", "lmx", "lmx-file") emailMessagesCmd.AddCommand(emailMessagesUpdateCmd) diff --git a/cmd/email_messages_update_test.go b/cmd/email_messages_update_test.go index f7e0b10..be0b8e7 100644 --- a/cmd/email_messages_update_test.go +++ b/cmd/email_messages_update_test.go @@ -1,15 +1,14 @@ package cmd import ( + "io" "net/http" - "net/http/httptest" "os" "path/filepath" "testing" "github.com/loops-so/loops-go" "github.com/spf13/cobra" - "github.com/zalando/go-keyring" ) func TestRunEmailMessagesUpdate(t *testing.T) { @@ -168,32 +167,8 @@ func TestEmailMessageFieldParamsFromCmd(t *testing.T) { }) } -func TestResolveExpectedRevisionID(t *testing.T) { - t.Run("supplied value is returned without any HTTP call", func(t *testing.T) { - var called bool - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - called = true - w.WriteHeader(http.StatusInternalServerError) - })) - t.Cleanup(srv.Close) - keyring.MockInit() - t.Setenv("LOOPS_CONFIG_DIR", t.TempDir()) - t.Setenv("LOOPS_API_KEY", "test-key") - t.Setenv("LOOPS_ENDPOINT_URL", srv.URL) - - got, err := resolveExpectedRevisionID(cfg(t), "em_abc123", "rev_supplied") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != "rev_supplied" { - t.Errorf("got %q, want rev_supplied", got) - } - if called { - t.Error("expected no HTTP call when revision id supplied") - } - }) - - t.Run("empty supplied triggers GET and returns current contentRevisionId", func(t *testing.T) { +func TestFetchLatestRevisionID(t *testing.T) { + t.Run("returns current contentRevisionId from GET", func(t *testing.T) { body := `{ "success": true, "emailMessageId": "em_abc123", @@ -209,7 +184,7 @@ func TestResolveExpectedRevisionID(t *testing.T) { }` serveJSON(t, http.StatusOK, body) - got, err := resolveExpectedRevisionID(cfg(t), "em_abc123", "") + got, err := fetchLatestRevisionID(cfg(t), "em_abc123") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -221,9 +196,44 @@ func TestResolveExpectedRevisionID(t *testing.T) { t.Run("GET failure surfaces a wrapped error", func(t *testing.T) { serveJSON(t, http.StatusNotFound, `{"success":false,"message":"Email message not found"}`) - _, err := resolveExpectedRevisionID(cfg(t), "em_missing", "") + _, err := fetchLatestRevisionID(cfg(t), "em_missing") if err == nil { t.Fatal("expected error, got nil") } }) } + +func TestEmailMessagesUpdateRevisionFlagValidation(t *testing.T) { + newCmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { return nil }, + } + addEmailMessageFieldFlags(cmd) + cmd.Flags().StringP("expected-revision-id", "r", "", "") + cmd.Flags().BoolP("force", "f", false, "") + cmd.MarkFlagsMutuallyExclusive("expected-revision-id", "force") + cmd.MarkFlagsOneRequired("expected-revision-id", "force") + cmd.MarkFlagsOneRequired("subject", "preview-text", "from-name", "from-email", "reply-to", "lmx", "lmx-file") + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + return cmd + } + + t.Run("neither flag errors", func(t *testing.T) { + cmd := newCmd() + cmd.SetArgs([]string{"em_abc123", "--subject", "x"}) + if err := cmd.Execute(); err == nil { + t.Fatal("expected error when neither --expected-revision-id nor --force is set, got nil") + } + }) + + t.Run("both flags together errors", func(t *testing.T) { + cmd := newCmd() + cmd.SetArgs([]string{"em_abc123", "--subject", "x", "-r", "rev1", "-f"}) + if err := cmd.Execute(); err == nil { + t.Fatal("expected error when both --expected-revision-id and --force are set, got nil") + } + }) +} From fd4156df2961ac2ade70fea9c1132072d3ba0eba Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Fri, 8 May 2026 08:56:46 -0700 Subject: [PATCH 2/2] go v1.26.3 to fix govulncheck --- go.mod | 2 +- mise.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 68805a6..7d1c3c0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/loops-so/cli -go 1.26.2 +go 1.26.3 require ( charm.land/fang/v2 v2.0.1 diff --git a/mise.toml b/mise.toml index 361f5d4..b28c656 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,5 @@ [tools] -go = "1.26.2" +go = "1.26.3" # goreleaser-pro is required, but not available via mise. try, # brew install --cask goreleaser/tap/goreleaser-pro