Connect sign-up to Sendico Facts intake (#46)
This commit is contained in:
1 parent
30f0455122
commit
a584c8189a
40 files changed
+1616
-35
No files matched your search
@@ -0,0 +1,155 @@
|
||||
package sendico
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/identity"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const org = "507f1f77bcf86cd799439011"
|
||||
const source = "507f1f77bcf86cd799439012"
|
||||
const subject = "507f1f77bcf86cd799439013"
|
||||
const submission = "507f1f77bcf86cd799439014"
|
||||
|
||||
func credential(t *testing.T, id string, expiry int64) string {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(map[string]any{"id": id, "exp": expiry})
|
||||
require.NoError(t, err)
|
||||
return "header." + base64.RawURLEncoding.EncodeToString(raw) + ".signature"
|
||||
}
|
||||
func config(t *testing.T, endpoint string) Config {
|
||||
t.Helper()
|
||||
file := filepath.Join(t.TempDir(), "token")
|
||||
require.NoError(t, os.WriteFile(file, []byte(credential(t, source, time.Now().Add(time.Hour).Unix())), 0600))
|
||||
return Config{APIURL: endpoint + "/api/v1", OrganizationID: org, SourceID: source, TokenFile: file}
|
||||
}
|
||||
|
||||
func TestSignupWireContractAndCredentialRotation(t *testing.T) {
|
||||
paths := []string{}
|
||||
var token string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
require.Equal(t, "Bearer "+token, r.Header.Get("Authorization"))
|
||||
paths = append(paths, r.URL.Path)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
if strings.Contains(r.URL.Path, "/individuals/") {
|
||||
require.Equal(t, "registration-request", r.Header.Get("X-Request-ID"))
|
||||
require.Empty(t, body)
|
||||
_, _ = 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))
|
||||
_, _ = fmt.Fprintf(w, `{"status":"success","data":{"subject_ref":%q,"submission_ref":%q,"accepted_at":"2026-10-07T10:30:00.123Z"}}`, subject, submission)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
cfg := config(t, server.URL)
|
||||
token = credential(t, source, time.Now().Add(time.Hour).Unix())
|
||||
client, err := Open(cfg)
|
||||
require.NoError(t, err)
|
||||
ref, err := client.Register(context.Background(), "registration-request")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subject, ref)
|
||||
// File rotation takes effect without restarting; account identity stays fixed.
|
||||
token = credential(t, source, time.Now().Add(2*time.Hour).Unix())
|
||||
require.NoError(t, os.WriteFile(cfg.TokenFile, []byte(token), 0600))
|
||||
receipt, err := client.Submit(context.Background(), subject, "signup-1", identity.Facts{FullName: "Ada Lovelace", Email: "ada@example.com", CustomerRef: "local-user"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, submission, receipt.SubmissionRef)
|
||||
require.Equal(t, subject, receipt.SubjectRef)
|
||||
require.Equal(t, []string{"/api/v1/individuals/" + org, "/api/v1/identity_facts/" + org}, paths)
|
||||
require.NoError(t, os.WriteFile(cfg.TokenFile, []byte(credential(t, org, time.Now().Add(time.Hour).Unix())), 0600))
|
||||
_, err = client.Register(context.Background(), "registration-request")
|
||||
require.Error(t, err)
|
||||
require.Len(t, paths, 2)
|
||||
}
|
||||
|
||||
func TestRejectInvalidConfiguration(t *testing.T) {
|
||||
for _, endpoint := range []string{"http://remote.example", "https://user:pass@api.example", "https://api.example?secret=x", "https://api.example/#fragment", "//api.example", "ftp://localhost"} {
|
||||
_, err := Open(config(t, endpoint))
|
||||
require.Error(t, err, endpoint)
|
||||
}
|
||||
cfg := config(t, "https://api.example")
|
||||
cfg.OrganizationID = "../other"
|
||||
_, err := Open(cfg)
|
||||
require.Error(t, err)
|
||||
cfg = config(t, "https://api.example")
|
||||
cfg.SourceID = ""
|
||||
_, err = Open(cfg)
|
||||
require.Error(t, err)
|
||||
cfg = config(t, "https://api.example")
|
||||
require.NoError(t, os.WriteFile(cfg.TokenFile, []byte(credential(t, source, 1)), 0600))
|
||||
_, err = Open(cfg)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUpstreamFailuresNeverProduceReceiptsOrLeakBodies(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
status int
|
||||
body string
|
||||
}{
|
||||
{"denied", 403, `{"secret":"private-assertion"}`},
|
||||
{"conflict", 409, `{"secret":"private-assertion"}`},
|
||||
{"unavailable", 503, `{"secret":"private-assertion"}`},
|
||||
{"malformed", 200, `{"private-assertion":`},
|
||||
{"empty", 200, `{"status":"success","data":{}}`},
|
||||
{"wrong-status", 200, `{"status":"error","data":{}}`},
|
||||
{"oversized", 200, `{"padding":"` + strings.Repeat("x", 17000) + `"}`},
|
||||
{"wrong-subject", 200, `{"status":"success","data":{"subject_ref":"` + org + `","submission_ref":"` + submission + `","accepted_at":"2026-10-07T10:30:00Z"}}`},
|
||||
{"trailing", 200, `{"status":"success","data":{}} {}`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tc.status)
|
||||
_, _ = io.WriteString(w, tc.body)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := Open(config(t, server.URL))
|
||||
require.NoError(t, err)
|
||||
_, err = client.Submit(context.Background(), subject, "key", identity.Facts{})
|
||||
require.Error(t, err)
|
||||
require.NotContains(t, err.Error(), "private-assertion")
|
||||
_, err = client.Register(context.Background(), "registration-request")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectAndTimeoutDoNotRepeatRegistration(t *testing.T) {
|
||||
var calls int
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
http.Redirect(w, r, "/redirected", http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := Open(config(t, server.URL))
|
||||
require.NoError(t, err)
|
||||
_, err = client.Register(context.Background(), "registration-request")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 1, calls)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err = client.Register(ctx, "registration-request")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 1, calls)
|
||||
timeoutServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-r.Context().Done() }))
|
||||
defer timeoutServer.Close()
|
||||
client, err = Open(config(t, timeoutServer.URL))
|
||||
require.NoError(t, err)
|
||||
client.http.Timeout = 20 * time.Millisecond
|
||||
_, err = client.Register(context.Background(), "timeout-request")
|
||||
require.Error(t, err)
|
||||
}
|
||||
Reference in new issue
Block a user