Merge pull request 'Restore signup, email delivery and local recovery' (#59) from identity-facts-57 into main

Reviewed-on: #59
This commit was merged in pull request #59.
This commit is contained in:
tech committed 2026-10-20 18:00:23 +00:00
commit e84d2d1ad3
33 files changed
+1243 -72

No files matched your search

+4 -4
View File
@@ -12,7 +12,7 @@ import (
"go.mongodb.org/mongo-driver/v2/bson"
)
func (c *Client) post(ctx context.Context, resource, requestRef string, payload any) (receiptDTO, error) {
func (c *Client) post(ctx context.Context, resource, requestRef string, payload any, expectedStatus int) (receiptDTO, error) {
var receipt receiptDTO
token, err := c.token()
if err != nil {
@@ -38,7 +38,7 @@ func (c *Client) post(ctx context.Context, resource, requestRef string, payload
}
defer func() { _ = response.Body.Close() }()
// Never decode or expose upstream error bodies, which can contain assertions.
if response.StatusCode != http.StatusOK {
if response.StatusCode != expectedStatus {
return receipt, merrors.ServiceUnavailable("Sendico request was not accepted")
}
decoder := json.NewDecoder(io.LimitReader(response.Body, 16385))
@@ -54,14 +54,14 @@ func (c *Client) post(ctx context.Context, resource, requestRef string, payload
}
func (c *Client) Register(ctx context.Context, requestRef string) (string, error) {
dto, err := c.post(ctx, "individuals", requestRef, nil)
dto, err := c.post(ctx, "individuals", requestRef, nil, http.StatusCreated)
if err != nil {
return "", err
}
return mapSubject(dto)
}
func (c *Client) Submit(ctx context.Context, subject, source string, facts identity.Facts) (identity.Receipt, error) {
dto, err := c.post(ctx, "identity_facts", bson.NewObjectID().Hex(), mapFacts(subject, source, facts))
dto, err := c.post(ctx, "identity_facts", bson.NewObjectID().Hex(), mapFacts(subject, source, facts), http.StatusOK)
if err != nil {
return identity.Receipt{}, err
}
@@ -48,6 +48,7 @@ func TestSignupWireContractAndCredentialRotation(t *testing.T) {
if strings.Contains(r.URL.Path, "/individuals/") {
require.Equal(t, "registration-request", r.Header.Get("X-Request-ID"))
require.Empty(t, body)
w.WriteHeader(http.StatusCreated)
_, _ = fmt.Fprintf(w, `{"status":"success","data":{"subject_ref":%q,"registered_at":"2026-10-07T10:30:00.123Z"}}`, subject)
} else {
require.JSONEq(t, `{"subject_ref":"`+subject+`","source_submission_ref":"signup-1","facts":{"full_name":"Ada Lovelace","email":"ada@example.com","external_customer_ref":"local-user"}}`, string(body))
@@ -95,6 +96,36 @@ func TestRejectInvalidConfiguration(t *testing.T) {
require.Error(t, err)
}
func TestSignupReceiptsRequireOperationStatus(t *testing.T) {
for _, status := range []int{200, 201, 202, 204, 206, 300, 403, 409, 503} {
t.Run(http.StatusText(status), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
_, _ = fmt.Fprintf(w, `{"status":"success","data":{"subject_ref":%q,"registered_at":"2026-10-07T10:30:00Z","submission_ref":%q,"accepted_at":"2026-10-07T10:30:00Z"}}`, subject, submission)
}))
defer server.Close()
client, err := Open(config(t, server.URL))
require.NoError(t, err)
ref, err := client.Register(context.Background(), "registration-request")
if status == http.StatusCreated {
require.NoError(t, err)
require.Equal(t, subject, ref)
} else {
require.Error(t, err)
require.Empty(t, ref)
}
receipt, err := client.Submit(context.Background(), subject, "key", identity.Facts{})
if status == http.StatusOK {
require.NoError(t, err)
require.Equal(t, submission, receipt.SubmissionRef)
} else {
require.Error(t, err)
require.Empty(t, receipt)
}
})
}
}
func TestUpstreamFailuresNeverProduceReceiptsOrLeakBodies(t *testing.T) {
for _, tc := range []struct {
name string
@@ -113,7 +144,11 @@ func TestUpstreamFailuresNeverProduceReceiptsOrLeakBodies(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.status)
status := tc.status
if status == http.StatusOK && strings.Contains(r.URL.Path, "/individuals/") {
status = http.StatusCreated
}
w.WriteHeader(status)
_, _ = io.WriteString(w, tc.body)
}))
defer server.Close()