Skip to content

Commit 3180bce

Browse files
Michael McNeesclaude
authored andcommitted
fix(connector): Okta activate param, Mailchimp upsert endpoint
Okta: always pass ?activate=<bool> — omitting it when false causes Okta to activate the user by default, triggering unintended provisioning side effects. Mailchimp: switch add_member from POST /members to PUT /members/{hash} (MD5 of lowercased email). The POST endpoint returns a duplicate-member error on reruns; the PUT upsert endpoint is idempotent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b37d9fe commit 3180bce

4 files changed

Lines changed: 39 additions & 7 deletions

File tree

packages/engine/internal/connector/mailchimp.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package connector
33
import (
44
"bytes"
55
"context"
6+
"crypto/md5"
7+
"encoding/hex"
68
"encoding/json"
79
"fmt"
810
"io"
@@ -122,8 +124,11 @@ func (c *MailchimpAddMemberConnector) Execute(ctx context.Context, params map[st
122124
return nil, fmt.Errorf("mailchimp/add_member: marshaling request: %w", err)
123125
}
124126

125-
req, err := http.NewRequestWithContext(ctx, "POST",
126-
c.apiURL(dc, fmt.Sprintf("/lists/%s/members", listID)),
127+
hash := md5.Sum([]byte(strings.ToLower(email)))
128+
subscriberHash := hex.EncodeToString(hash[:])
129+
130+
req, err := http.NewRequestWithContext(ctx, "PUT",
131+
c.apiURL(dc, fmt.Sprintf("/lists/%s/members/%s", listID, subscriberHash)),
127132
bytes.NewReader(reqJSON),
128133
)
129134
if err != nil {

packages/engine/internal/connector/mailchimp_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ func TestMailchimpListMembersConnector_InvalidAPIKeyFormat(t *testing.T) {
7373
}
7474

7575
func TestMailchimpAddMemberConnector_AddsMember(t *testing.T) {
76+
// md5("bob@example.com") = 4b9bb80620f03eb3719e0a061c14283d
77+
const wantPath = "/3.0/lists/list1/members/4b9bb80620f03eb3719e0a061c14283d"
7678
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
77-
if r.Method != "POST" || r.URL.Path != "/3.0/lists/list1/members" {
79+
if r.Method != "PUT" || r.URL.Path != wantPath {
7880
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
7981
}
8082
var body map[string]any

packages/engine/internal/connector/okta.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,7 @@ func (c *OktaCreateUserConnector) Execute(ctx context.Context, params map[string
121121
return nil, fmt.Errorf("okta/create_user: marshaling request: %w", err)
122122
}
123123

124-
endpoint := c.apiURL(domain, "/api/v1/users")
125-
if activate {
126-
endpoint += "?activate=true"
127-
}
124+
endpoint := fmt.Sprintf("%s?activate=%v", c.apiURL(domain, "/api/v1/users"), activate)
128125

129126
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(reqJSON))
130127
if err != nil {

packages/engine/internal/connector/okta_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func TestOktaCreateUserConnector_CreatesUser(t *testing.T) {
7878
if r.Method != "POST" {
7979
t.Errorf("expected POST, got %s", r.Method)
8080
}
81+
if r.URL.Query().Get("activate") != "true" {
82+
t.Errorf("expected activate=true, got %q", r.URL.Query().Get("activate"))
83+
}
8184
var body map[string]any
8285
json.NewDecoder(r.Body).Decode(&body)
8386
profile, _ := body["profile"].(map[string]any)
@@ -108,6 +111,31 @@ func TestOktaCreateUserConnector_CreatesUser(t *testing.T) {
108111
}
109112
}
110113

114+
func TestOktaCreateUserConnector_ActivateFalse(t *testing.T) {
115+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116+
if r.URL.Query().Get("activate") != "false" {
117+
t.Errorf("expected activate=false, got %q", r.URL.Query().Get("activate"))
118+
}
119+
w.Header().Set("Content-Type", "application/json")
120+
w.WriteHeader(http.StatusOK)
121+
json.NewEncoder(w).Encode(map[string]any{"id": "00u2", "status": "STAGED"})
122+
}))
123+
defer srv.Close()
124+
125+
c := &OktaCreateUserConnector{baseURL: srv.URL}
126+
out, err := c.Execute(t.Context(), map[string]any{
127+
"_credential": map[string]string{"domain": "dev.okta.com", "token": "tok"},
128+
"activate": false,
129+
"profile": map[string]any{"login": "staged@example.com", "email": "staged@example.com"},
130+
})
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
if out["id"] != "00u2" {
135+
t.Errorf("expected id=00u2, got %v", out["id"])
136+
}
137+
}
138+
111139
func TestOktaCreateUserConnector_MissingProfile(t *testing.T) {
112140
c := &OktaCreateUserConnector{baseURL: "http://unused"}
113141
_, err := c.Execute(t.Context(), map[string]any{

0 commit comments

Comments
 (0)