From 14cb4039caa20850c3f32c28ee6994aea7c3870e Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Mon, 17 Aug 2026 21:25:35 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20SDK=20conformance=20harness=20?= =?UTF-8?q?=E2=80=94=20real=20provider=20SDKs=20against=20the=20adapters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A nested Go module (conformance/, just conformance, CI step) that boots stunt adapters and drives them with the official SDKs: - stripe-go: form+bracket creates, PI create+confirm state machine, iterator pagination walking has_more, webhook verification through the SDK's own ConstructEvent HMAC validator (5 checks). - aws-sdk-go-v2: STS GetCallerIdentity/AssumeRole with REAL SigV4 from the documented synthetic credentials; full S3 lifecycle incl. binary byte-exact round-trip and the ListObjectsV2 paginator following continuations (8 checks). - go-github: issue CRUD, Link-header pagination via resp.NextPage, comments, state transitions (5 checks). First-run findings, all fixed: - stripe PaymentIntents returned amount as a JSON string — typed SDKs reject it (real Stripe returns money fields as numbers). - the router had no greedy path params: S3 keys containing slashes 404'd. A terminal {key+} segment now captures the remaining path verbatim; S3 object routes use it. - S3 XML LastModified rendered ...T05Z.000Z (millis appended after the zone) — the AWS SDK time parser rejects it; now ...T05.000Z. 18 checks, all green; full main-module suite + gates green. --- .github/workflows/ci.yml | 3 + CHANGELOG.md | 38 +++ adapters/README.md | 2 +- adapters/aws-s3-style/adapter.yaml | 10 +- adapters/aws-s3-style/scripts/lib.star | 9 +- .../stripe-style/scripts/payment_intents.star | 11 +- conformance/aws_test.go | 165 +++++++++++++ conformance/github_test.go | 118 +++++++++ conformance/go.mod | 57 +++++ conformance/go.sum | 158 ++++++++++++ conformance/harness.go | 118 +++++++++ conformance/stripe_test.go | 232 ++++++++++++++++++ internal/engine/adapter_dispatch.go | 25 ++ internal/engine/fuzz_parse_test.go | 18 ++ justfile | 7 + 15 files changed, 959 insertions(+), 12 deletions(-) create mode 100644 conformance/aws_test.go create mode 100644 conformance/github_test.go create mode 100644 conformance/go.mod create mode 100644 conformance/go.sum create mode 100644 conformance/harness.go create mode 100644 conformance/stripe_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82e7739f..2bfb2436 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,6 @@ jobs: - name: Run the canonical gate run: just ci + + - name: SDK conformance (real provider SDKs vs adapters) + run: just conformance diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cff6c89..0f73c0ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,44 @@ All notable changes to **stunt** are documented here. The format is based on ## [Unreleased] +## [0.47.0] — 2026-08-17 + +### Testing + +- **SDK conformance harness — real provider SDKs against the adapters.** + A new nested Go module (`conformance/`, wired into CI via + `just conformance`) boots stunt adapters and drives them with the + official SDKs, asserting business outcomes rather than wire shapes: + - **stripe-go** (customer create with form+bracket bodies, PaymentIntent + create+confirm → succeeded, SDK-iterator pagination walking + `has_more`, and webhook verification through the SDK's own + `ConstructEvent` HMAC validator — signature verifies AND + `data.object` parses). + - **aws-sdk-go-v2** — STS `GetCallerIdentity`/`AssumeRole` with REAL + SigV4 signatures from the documented synthetic credentials, and the + full S3 lifecycle (CreateBucket, PutObject binary body, byte-exact + GetObject incl. non-UTF-8, HeadObject, the ListObjectsV2 paginator + following continuations, DeleteObject) over path-style addressing. + - **go-github** — issue CRUD on the seeded repo, `Link`-header + pagination through `resp.NextPage`, comments, and state transitions. + - First-run findings, all fixed: **stripe PaymentIntents returned + `amount` as a JSON string** (typed SDKs reject it — real Stripe + returns money fields as numbers; coerced at create and render); the + **router had no greedy path params**, so S3 keys containing slashes + (`photos/2024/a.jpg`) 404'd — a terminal `{key+}` segment now + captures the remaining path verbatim and the S3 object routes use + it; and **S3 XML `LastModified` rendered as `...T05Z.000Z`** + (millis appended after the zone), which the AWS SDK's time parser + rejects — now `...T05.000Z` like real S3. + +### Engine + +- **Greedy route params (`{name+}`).** A route pattern whose last + segment is `{key+}` captures the rest of the path verbatim, slashes + included — the object-key shape S3- and Cloudflare-style providers + need. Whole-segment `{name}` and embedded `prefix{p}suffix` matching + are unchanged. + ## [0.46.0] — 2026-08-17 ### Adapters diff --git a/adapters/README.md b/adapters/README.md index 79047433..c9dc6d81 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -353,7 +353,7 @@ Every handler receives a `req` argument with: | `req.headers` | `dict[str, str]` | Request headers (case-insensitive lookups; `req.headers.get("authorization")` finds `Authorization`). `req` also supports dict access (`req["method"]`, `req.get("query")`) | | `req.body` | `dict` | Parsed JSON body (empty dict if no body) | | `req.raw_body` | `str` | The verbatim request body bytes (as a string). Use it for non-JSON / binary content (e.g. an S3 object upload) where the parsed `body` map is meaningless — store it via `store_blob` so it round-trips byte-exact | -| `req.params` | `dict[str, str]` | Path parameters extracted from route (e.g. `{id}` → `{"id": "..."}`) | +| `req.params` | `dict[str, str]` | Path parameters extracted from route. `{id}` matches one segment; a terminal `{key+}` is greedy and captures the remaining path verbatim, slashes included (the S3 object-key shape: `/{bucket}/{key+}` matches `photos/2024/a.jpg` as one key) | | `req.query` | `dict[str, str]` | Query parameters (first value of each key) | ## Serializing concurrent handler calls (`concurrency_key`) diff --git a/adapters/aws-s3-style/adapter.yaml b/adapters/aws-s3-style/adapter.yaml index 2ef7591e..4e43ebd9 100644 --- a/adapters/aws-s3-style/adapter.yaml +++ b/adapters/aws-s3-style/adapter.yaml @@ -41,27 +41,27 @@ endpoints: # concurrency_key serialises the read-modify-write (find existing doc + # blob id, then insert/update) against concurrent writers of the same # bucket (both simple PUTs and part uploads). - - route: /{bucket}/{key} + - route: /{bucket}/{key+} method: PUT handler: scripts/objects.star#on_put_object concurrency_key: bucket # Download object: GET /{bucket}/{key} — or ListParts with ?uploadId=... - - route: /{bucket}/{key} + - route: /{bucket}/{key+} method: GET handler: scripts/objects.star#on_get_object # Head object: HEAD /{bucket}/{key} - - route: /{bucket}/{key} + - route: /{bucket}/{key+} method: HEAD handler: scripts/objects.star#on_head_object # Delete object: DELETE /{bucket}/{key} — or AbortMultipartUpload with # ?uploadId=... (both mutate object/part state for the bucket). - - route: /{bucket}/{key} + - route: /{bucket}/{key+} method: DELETE handler: scripts/objects.star#on_delete_object concurrency_key: bucket # Multipart upload POST verb: ?uploads creates (CreateMultipartUpload), # ?uploadId=... completes (assembles the object — read-modify-write). - - route: /{bucket}/{key} + - route: /{bucket}/{key+} method: POST handler: scripts/multipart.star#on_post_multipart concurrency_key: bucket diff --git a/adapters/aws-s3-style/scripts/lib.star b/adapters/aws-s3-style/scripts/lib.star index 5cd7d6b4..9648d64b 100644 --- a/adapters/aws-s3-style/scripts/lib.star +++ b/adapters/aws-s3-style/scripts/lib.star @@ -411,9 +411,14 @@ def _as_int(v): return int(v) # _unix_to_iso8601 renders Unix seconds in S3 XML millis form -# ("2026-01-20T00:00:00.000Z"). +# ("2026-01-20T00:00:00.000Z"). unix_to_rfc3339 already ends in "Z", so +# the millis are spliced in BEFORE it — appending ".000Z" produced +# "...:05Z.000Z", which the AWS SDK's time parser rejects. def _unix_to_iso8601(u): - return clock.unix_to_rfc3339(_as_int(u)) + ".000Z" + s = clock.unix_to_rfc3339(_as_int(u)) + if s != "" and s[len(s)-1] == "Z": + s = s[:len(s)-1] + return s + ".000Z" # _unix_to_rfc1123 renders Unix seconds as an RFC 1123 Last-Modified # value ("Mon, 02 Jan 2006 15:04:05 GMT"), like real S3 headers. diff --git a/adapters/stripe-style/scripts/payment_intents.star b/adapters/stripe-style/scripts/payment_intents.star index 9fd54607..a15d704a 100644 --- a/adapters/stripe-style/scripts/payment_intents.star +++ b/adapters/stripe-style/scripts/payment_intents.star @@ -17,9 +17,9 @@ def _pi_public(doc): return { "id": doc["id"], "object": "payment_intent", - "amount": doc.get("amount", 0), - "amount_capturable": doc.get("amount_capturable", 0), - "amount_received": doc.get("amount_received", 0), + "amount": _num(doc.get("amount", 0)), + "amount_capturable": _num(doc.get("amount_capturable", 0)), + "amount_received": _num(doc.get("amount_received", 0)), "currency": doc.get("currency", "usd"), "status": doc.get("status", "requires_payment_method"), "capture_method": doc.get("capture_method", "automatic"), @@ -155,7 +155,10 @@ def on_create_payment_intent(req): if body == None: body = {} - amount = body.get("amount", 0) + # Form-encoded creates deliver amount as a STRING ("4200"); the real + # API returns money fields as JSON numbers, and typed SDKs (stripe-go) + # reject a string amount — coerce at the door. + amount = _num(body.get("amount", 0)) currency = body.get("currency", "usd") capture_method = body.get("capture_method", "automatic") if capture_method not in ["automatic", "manual"]: diff --git a/conformance/aws_test.go b/conformance/aws_test.go new file mode 100644 index 00000000..eeccc511 --- /dev/null +++ b/conformance/aws_test.go @@ -0,0 +1,165 @@ +package conformance + +import ( + "bytes" + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/sts" +) + +// TestAWSSDKConformance drives aws-sdk-go-v2 (STS + S3) against the +// aws-iam-sts-style and aws-s3-style adapters with the adapters' +// documented synthetic credentials. The SDK signs every request with REAL +// SigV4 — passing means the adapters' signature verification accepts the +// genuine algorithm output, not just hand-rolled test vectors. +func TestAWSSDKConformance(t *testing.T) { + ctx := context.Background() + + // The long-public example credentials from the AWS docs, which the + // adapters' SigV4 verification is keyed to. + cfgCreds := credentials.NewStaticCredentialsProvider( + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "") + + // ===== STS: GetCallerIdentity + AssumeRole (query protocol + SigV4) ===== + + stsBase := Boot(t, "aws-iam-sts-style") + cfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion("us-east-1"), + config.WithCredentialsProvider(cfgCreds), + ) + if err != nil { + t.Fatalf("aws config: %v", err) + } + stsClient := sts.NewFromConfig(cfg, func(o *sts.Options) { + o.BaseEndpoint = &stsBase + }) + + id, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + t.Fatalf("GetCallerIdentity (real SigV4): %v", err) + } + if id.Account == nil || *id.Account == "" { + t.Fatal("GetCallerIdentity Account empty") + } + if id.Arn == nil || *id.Arn == "" { + t.Fatal("GetCallerIdentity Arn empty") + } + Record(t, "aws-sdk-go-v2", "aws-iam-sts-style", "GetCallerIdentity with real SigV4 signature") + + role, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{ + RoleArn: id.Arn, + RoleSessionName: ptr("conformance-session"), + }) + if err != nil { + t.Fatalf("AssumeRole: %v", err) + } + if role.Credentials == nil || role.Credentials.AccessKeyId == nil { + t.Fatal("AssumeRole returned no credentials") + } + Record(t, "aws-sdk-go-v2", "aws-iam-sts-style", "AssumeRole -> credentials") + + // ===== S3: bucket + object lifecycle (path-style, SigV4, raw bytes) ===== + + s3Base := Boot(t, "aws-s3-style") + s3Client := s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = &s3Base + o.UsePathStyle = true + }) + + bucket := "conformance-bucket" + if _, err := s3Client.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: ptr(bucket), + }); err != nil { + t.Fatalf("CreateBucket: %v", err) + } + Record(t, "aws-sdk-go-v2", "aws-s3-style", "CreateBucket") + + content := []byte("stunt conformance payload \x00\xff\x01 — binary round-trip") + if _, err := s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: ptr(bucket), + Key: ptr("bin/payload.bin"), + Body: bytes.NewReader(content), + }); err != nil { + t.Fatalf("PutObject: %v", err) + } + Record(t, "aws-sdk-go-v2", "aws-s3-style", "PutObject (binary body)") + + for i := 0; i < 3; i++ { + if _, err := s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: ptr(bucket), + Key: ptr("keys/k" + string(rune('0'+i)) + ".txt"), + Body: bytes.NewReader([]byte("v")), + }); err != nil { + t.Fatalf("PutObject seed %d: %v", i, err) + } + } + + got, err := s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: ptr(bucket), + Key: ptr("bin/payload.bin"), + }) + if err != nil { + t.Fatalf("GetObject: %v", err) + } + defer got.Body.Close() + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(got.Body); err != nil { + t.Fatalf("read body: %v", err) + } + if !bytes.Equal(buf.Bytes(), content) { + t.Fatalf("GetObject round-trip mismatch: got %d bytes, want %d", buf.Len(), len(content)) + } + Record(t, "aws-sdk-go-v2", "aws-s3-style", "GetObject byte-exact round-trip (incl. non-UTF-8)") + + head, err := s3Client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: ptr(bucket), + Key: ptr("bin/payload.bin"), + }) + if err != nil { + t.Fatalf("HeadObject: %v", err) + } + if head.ContentLength == nil || *head.ContentLength != int64(len(content)) { + t.Fatalf("HeadObject ContentLength = %v, want %d", head.ContentLength, len(content)) + } + Record(t, "aws-sdk-go-v2", "aws-s3-style", "HeadObject metadata") + + // ListObjectsV2 with continuation: 4 objects, page size 2 — the SDK's + // paginator must follow IsTruncated/NextContinuationToken. + var listed int + p := s3.NewListObjectsV2Paginator(s3Client, &s3.ListObjectsV2Input{ + Bucket: ptr(bucket), + MaxKeys: ptr[int32](2), + Prefix: ptr(""), + }) + for p.HasMorePages() { + page, err := p.NextPage(ctx) + if err != nil { + t.Fatalf("ListObjectsV2 page: %v", err) + } + if page.KeyCount != nil { + listed += int(*page.KeyCount) + } else { + listed += len(page.Contents) + } + } + if listed != 4 { + t.Fatalf("ListObjectsV2 paginator listed %d objects, want 4 (continuation not followed?)", listed) + } + Record(t, "aws-sdk-go-v2", "aws-s3-style", "ListObjectsV2 paginator follows continuation (4 over MaxKeys=2)") + + if _, err := s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: ptr(bucket), + Key: ptr("keys/k0.txt"), + }); err != nil { + t.Fatalf("DeleteObject: %v", err) + } + Record(t, "aws-sdk-go-v2", "aws-s3-style", "DeleteObject") +} + +func ptr[T any](v T) *T { return &v } diff --git a/conformance/github_test.go b/conformance/github_test.go new file mode 100644 index 00000000..05cfbcc3 --- /dev/null +++ b/conformance/github_test.go @@ -0,0 +1,118 @@ +package conformance + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-github/v66/github" + "golang.org/x/oauth2" +) + +// TestGitHubSDKConformance drives go-github against the github-style +// adapter with the seeded static PAT: issue CRUD plus SDK-side pagination +// (go-github parses the Link header into resp.NextPage and walks it). +func TestGitHubSDKConformance(t *testing.T) { + ctx := context.Background() + base := Boot(t, "github-style") + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "ghp_pat_token_mock"}) + tc := oauth2.NewClient(ctx, ts) + tc.Timeout = HTTPClient().Timeout + client := github.NewClient(tc) + u := base + "/" + client.BaseURL, _ = parseURL(u) + + // The adapter serves the seeded octocat/hello-world repo and 404s + // unknown repos exactly like the real API. + const owner, repo = "octocat", "hello-world" + + // ===== Create issues via the SDK ===== + + for i := 1; i <= 5; i++ { + issue, _, err := client.Issues.Create(ctx, owner, repo, &github.IssueRequest{ + Title: github.String(fmt.Sprintf("Conformance issue %d", i)), + Body: github.String("filed by the go-github SDK against stunt"), + }) + if err != nil { + t.Fatalf("Issues.Create %d: %v", i, err) + } + if issue.GetNumber() == 0 || issue.GetTitle() == "" { + t.Fatalf("issue %d: number=%d title=%q", i, issue.GetNumber(), issue.GetTitle()) + } + } + Record(t, "go-github/v66", "github-style", "Issues.Create x5 (number assignment)") + + // ===== SDK pagination: Link header -> resp.NextPage walking ===== + + var collected []*github.Issue + opts := &github.IssueListByRepoOptions{ListOptions: github.ListOptions{PerPage: 2}} + pages := 0 + for { + page, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opts) + if err != nil { + t.Fatalf("Issues.ListByRepo: %v", err) + } + collected = append(collected, page...) + pages++ + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + // The seeded repo ships one pre-existing issue; ours are 5 more. + if len(collected) < 6 { + t.Fatalf("paginated %d issues, want >= 6 (Link header not followed?)", len(collected)) + } + if pages < 3 { + t.Fatalf("walked %d pages with PerPage=2 over %d issues — pagination not followed", pages, len(collected)) + } + ours := 0 + for _, is := range collected { + for i := 1; i <= 5; i++ { + if is.GetTitle() == fmt.Sprintf("Conformance issue %d", i) { + ours++ + } + } + } + if ours != 5 { + t.Fatalf("found %d/5 created issues across pages", ours) + } + Record(t, "go-github/v66", "github-style", fmt.Sprintf("Issues.ListByRepo walks Link headers (%d over PerPage=2, %d pages)", len(collected), pages)) + + // ===== Comment on an issue ===== + + num := collected[0].GetNumber() + cmt, _, err := client.Issues.CreateComment(ctx, owner, repo, num, &github.IssueComment{ + Body: github.String("SDK comment"), + }) + if err != nil { + t.Fatalf("Issues.CreateComment: %v", err) + } + if cmt.GetBody() != "SDK comment" || cmt.GetID() == 0 { + t.Fatalf("comment = %+v", cmt) + } + Record(t, "go-github/v66", "github-style", "Issues.CreateComment") + + listed, _, err := client.Issues.ListComments(ctx, owner, repo, num, nil) + if err != nil { + t.Fatalf("Issues.ListComments: %v", err) + } + if len(listed) == 0 || listed[len(listed)-1].GetBody() != "SDK comment" { + t.Fatalf("ListComments = %d comments", len(listed)) + } + Record(t, "go-github/v66", "github-style", "Issues.ListComments round-trip") + + // ===== Close an issue (state transition) ===== + + closed, _, err := client.Issues.Edit(ctx, owner, repo, num, &github.IssueRequest{ + State: github.String("closed"), + }) + if err != nil { + t.Fatalf("Issues.Edit close: %v", err) + } + if closed.GetState() != "closed" { + t.Fatalf("state = %q, want closed", closed.GetState()) + } + Record(t, "go-github/v66", "github-style", "Issues.Edit state transition") +} diff --git a/conformance/go.mod b/conformance/go.mod new file mode 100644 index 00000000..b9f2000c --- /dev/null +++ b/conformance/go.mod @@ -0,0 +1,57 @@ +module stuntapi.com/stunt/conformance + +go 1.24 + +require ( + github.com/aws/aws-sdk-go-v2/config v1.32.37 + github.com/aws/aws-sdk-go-v2/credentials v1.19.36 + github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 + github.com/google/go-github/v66 v66.0.0 + github.com/stripe/stripe-go/v86 v86.3.0 + golang.org/x/oauth2 v0.26.0 + stuntapi.com/stunt v0.46.0 +) + +require ( + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.43.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 // indirect + github.com/aws/smithy-go v1.27.8 // indirect + github.com/brianvoe/gofakeit/v6 v6.28.0 // indirect + github.com/coder/websocket v1.8.15 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/expr-lang/expr v1.17.8 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/vektah/gqlparser/v2 v2.5.36 // indirect + go.starlark.net v0.0.0-20240925182052-1207426daebd // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect + modernc.org/sqlite v1.36.0 // indirect +) + +replace stuntapi.com/stunt => ../ diff --git a/conformance/go.sum b/conformance/go.sum new file mode 100644 index 00000000..d3fbec7e --- /dev/null +++ b/conformance/go.sum @@ -0,0 +1,158 @@ +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/aws/aws-sdk-go-v2 v1.43.6 h1:RrmFcqCBxkJuf7g1axVo5krB4jM/AO8r5e5oujrgdoQ= +github.com/aws/aws-sdk-go-v2 v1.43.6/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 h1:LAfOuhAH331fmOjTQpAaOlH+Ftn7RzSDJ2VFwjdMMy4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18/go.mod h1:4e5xhuXHx1e4U9EthvbPP1r/DIMp5c2823OL8karzcM= +github.com/aws/aws-sdk-go-v2/config v1.32.37 h1:Ljl7LOJB6ym0liuEl0+TZ3d7f5I8MEZN1Cj9PINlj/g= +github.com/aws/aws-sdk-go-v2/config v1.32.37/go.mod h1:WJ7pe7ZPpmG8Q5kKS53zeypIV4FBGACxmte8Uc6SgUc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.36 h1:84s5xMme6ENYEdKG8rsbSFFg/8+lbHBeM9QYSO0gnDk= +github.com/aws/aws-sdk-go-v2/credentials v1.19.36/go.mod h1:c46BLdagDLIswjgt+GeQOslXgeS0E6wCacs5yZbxPGk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 h1:b5tb+CZItBkydC7r3hTNdSO3pszG1R2EtnA+7TePQPk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37/go.mod h1:ZQ+6SU9X0oz6+7MUCSswv9Mjci4eaqZr21HI2RVy/yA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 h1:lznzIOvvbqjfe8UAaciCRJgBgJsxuTROKlhZuXQWfv8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37/go.mod h1:otfkzyfQeMMLZAqX59GSXTL3o22BR/l6HFaRzzbWSqA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 h1:zCEORWo0eU0gDjG+IyApE/2B+ZGG1m+GU7B263XV8ds= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37/go.mod h1:i6c0PEl3TNOWxRbQ++KQcVenPWS/GoQeiklKhNuqzJ8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 h1:A3UAuCmx7LyUcrixBTzKJYYIUZ2yTvn6ZhT8PB+7APk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38/go.mod h1:1PDUYG9Z+JrbbsobsAZHjWOm9QBT/djiK3QbykTL5Z4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30 h1:5437eMoOwqqQpZn2XJy74mlDCuPYL81texMT3mXqgtU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30/go.mod h1:xfu2m3dOpvW8lj98wQYa8V9ku/Rta59hsbireGzhh3A= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 h1:a3D4AjrOrTrP8+d9ILBthqrElf0z1JNol09Xvnwcys8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37/go.mod h1:ky0gTu+ukvUTuUKFIpp6Wid4oninrkCyvbFkVs0kpHM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38 h1:gX8B8y3Ho30B1LPxefDKMi/HZqWEb47U9ogs3DtSG0M= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38/go.mod h1:l5WblZlcmGPe4/O7JY2HO25Z+xqTBvyfTyFbRMf8gYw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2 h1:GNU0/xtPEXMKilJZ/a8BedeuQnvu+Usi6qVm9EFfncc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2/go.mod h1:4jYWUecEsQtE73jPl7p3jrbYXH5ffcR4gegyCygagfg= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 h1:i68sFvXidKlkiSvI7d7Ilc1/UvW4CtBOaivH7jhG4fs= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.6/go.mod h1:/h7Obr9WTtzbjTHGASRQwLN7Bupw+TC3x8x7fyx39hE= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 h1:tpfGChmjUmv3W9WlRvy+stwKDTbFFdq8Zk9DbFPrfMU= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.6/go.mod h1:CSjiDzmG/lsKkTOYjbkM+duLmRlW+LOxD64Na44ijnI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 h1:49BBtY68A+KJCQ3a2F3eUe6ROsKucxUdfHKoqorc0wI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6/go.mod h1:ptG2hbs7QltE1GcQY0MpS4bfrc51KCnBXUr7OT1EEfE= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 h1:JvExZWabChDM0qJAirQYGfOYo0ndT3edXj+fqSPNjkE= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.6/go.mod h1:XZcaQkV2cItp6yEkrwljyaPOf22RuX7T43jxap/FOmM= +github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY= +github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= +github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stripe/stripe-go/v86 v86.3.0 h1:BKtYc3NtRa4EGzKAmp4jvl5q7kk2rwMZ+llF18N5vHI= +github.com/stripe/stripe-go/v86 v86.3.0/go.mod h1:Co7QRXCKGNOPTugAdvjgRo+KcMtd9hxy+pZMN0yThsQ= +github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= +github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.starlark.net v0.0.0-20240925182052-1207426daebd h1:S+EMisJOHklQxnS3kqsY8jl2y5aF0FDEdcLnOw3q22E= +go.starlark.net v0.0.0-20240925182052-1207426daebd/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= +modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/conformance/harness.go b/conformance/harness.go new file mode 100644 index 00000000..d12c1a0e --- /dev/null +++ b/conformance/harness.go @@ -0,0 +1,118 @@ +// Package conformance runs REAL provider SDKs against booted stunt +// adapters and asserts business outcomes: CRUD round-trips, SDK-driven +// pagination walking, webhook signature verification through the SDK's +// own validator, and provider error surfaces. +// +// This is a nested module (own go.mod) so the heavyweight SDK +// dependencies never touch the stunt binary's dependency graph. It +// imports the engine's internal packages, which is allowed within this +// repository tree. +package conformance + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "stuntapi.com/stunt/internal/engine" + "stuntapi.com/stunt/internal/manifest" +) + +// Boot starts a real stunt engine serving the named reference adapter on +// a free local port and returns its base URL. The engine is closed (and +// its temp state removed) when the test ends. webhookURL, when non-empty, +// is the engine-level events target (config.webhook_url) the adapter's +// signed deliveries are POSTed to. +func Boot(t *testing.T, adapter string, webhookURL ...string) string { + t.Helper() + + dir, err := filepath.Abs(filepath.Join("..", "adapters", adapter)) + if err != nil { + t.Fatal(err) + } + if _, err := os.Stat(dir); err != nil { + t.Skipf("adapter %s not present", adapter) + } + + svcCfg := map[string]any{} + if len(webhookURL) > 0 && webhookURL[0] != "" { + svcCfg["webhook_url"] = webhookURL[0] + } + stateDir := t.TempDir() + m := &manifest.Manifest{ + Path: filepath.Join(stateDir, "stunt.yaml"), + Version: 1, + Network: manifest.Network{Mode: "port", BasePort: 0}, + Services: map[string]manifest.Service{ + "svc": {Adapter: dir, Config: svcCfg}, + }, + } + e, err := engine.New(m) + if err != nil { + t.Fatalf("engine.New(%s): %v", adapter, err) + } + t.Cleanup(func() { _ = e.Close() }) + + addrs, stop, err := e.ServeForTest(context.Background()) + if err != nil { + t.Fatalf("ServeForTest(%s): %v", adapter, err) + } + t.Cleanup(stop) + return addrs["svc"] +} + +// HTTPClient is a client with a generous timeout for SDK calls. +func HTTPClient() *http.Client { + return &http.Client{Timeout: 30 * time.Second} +} + +// Check records one named conformance result so the suite can emit a +// machine-readable scoreboard (consumed later by the case-study +// generator — the SEO pipeline and the test pipeline are one thing). +type Check struct { + SDK string `json:"sdk"` + Adapter string `json:"adapter"` + Name string `json:"check"` +} + +var registered = map[string][]Check{} + +// Record notes a passed check for the scoreboard dump. +func Record(t *testing.T, sdk, adapter, name string) { + t.Helper() + t.Logf("✓ %s/%s: %s", sdk, adapter, name) + registered[sdk] = append(registered[sdk], Check{SDK: sdk, Adapter: adapter, Name: name}) +} + +// ScoreboardPath, when set (RUN_CONFORMANCE_SCOREBOARD), receives the +// result dump after the whole run (TSV: sdk, adapter, check). +const ScoreboardPath = "RUN_CONFORMANCE_SCOREBOARD" + +// TestMain dumps the scoreboard after a green run when the env var names +// a file — consumed by the case-study generator. +func TestMain(m *testing.M) { + code := m.Run() + if path := os.Getenv(ScoreboardPath); path != "" && code == 0 { + f, err := os.Create(path) + if err == nil { + defer f.Close() + for sdk, checks := range registered { + for _, c := range checks { + fmt.Fprintf(f, "%s\t%s\t%s\n", sdk, c.Adapter, c.Name) + } + } + } + } + os.Exit(code) +} + +// parseURL is net/url.Parse with the error swallowed for the test +// call-site's convenience (the URLs here are engine-provided). +func parseURL(s string) (*url.URL, error) { + return url.Parse(s) +} diff --git a/conformance/stripe_test.go b/conformance/stripe_test.go new file mode 100644 index 00000000..b8d14264 --- /dev/null +++ b/conformance/stripe_test.go @@ -0,0 +1,232 @@ +package conformance + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + stripe "github.com/stripe/stripe-go/v86" + "github.com/stripe/stripe-go/v86/customer" + "github.com/stripe/stripe-go/v86/paymentintent" + "github.com/stripe/stripe-go/v86/paymentmethod" + "github.com/stripe/stripe-go/v86/webhook" +) + +// TestStripeSDKConformance drives the official stripe-go SDK against the +// stripe-style adapter: form+bracket encoded creates (the Rails/PHP body +// shape SDKs POST), SDK-iterator pagination, and webhook verification +// through the SDK's own webhook.ConstructEvent HMAC validator. +func TestStripeSDKConformance(t *testing.T) { + var mu sync.Mutex + var deliveries []struct { + payload []byte + headers http.Header + } + sink := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + mu.Lock() + deliveries = append(deliveries, struct { + payload []byte + headers http.Header + }{b, r.Header.Clone()}) + mu.Unlock() + w.WriteHeader(200) + })) + defer sink.Close() + + base := Boot(t, "stripe-style", sink.URL) + + stripe.Key = "sk_test_conformance" + stripe.SetBackend(stripe.APIBackend, stripe.GetBackendWithConfig(stripe.APIBackend, &stripe.BackendConfig{ + URL: stripe.String(base), + HTTPClient: HTTPClient(), + })) + defer stripe.SetBackend(stripe.APIBackend, nil) + ctx := context.Background() + + // ===== Customer create (form-encoded, nested params) ===== + + cus, err := customer.New(&stripe.CustomerParams{ + Name: stripe.String("Ada Lovelace"), + Email: stripe.String("ada@synth.example"), + Metadata: map[string]string{ + "source": "stripe-go-conformance", + }, + }) + if err != nil { + t.Fatalf("customer.New: %v", err) + } + if !strings.HasPrefix(cus.ID, "cus_") { + t.Fatalf("customer ID = %q, want cus_ prefix", cus.ID) + } + if cus.Name != "Ada Lovelace" { + t.Fatalf("customer Name = %q", cus.Name) + } + Record(t, "stripe-go/v86", "stripe-style", "customer.New (form+bracket body)") + + got, err := customer.Get(cus.ID, nil) + if err != nil { + t.Fatalf("customer.Get: %v", err) + } + if got.Email != "ada@synth.example" { + t.Fatalf("customer.Get Email = %q", got.Email) + } + Record(t, "stripe-go/v86", "stripe-style", "customer.Get round-trip") + + // ===== PaymentIntent create + confirm (state machine) ===== + + pm, err := paymentmethod.New(&stripe.PaymentMethodParams{ + Type: stripe.String("card"), + Card: &stripe.PaymentMethodCardParams{ + Token: stripe.String("tok_visa"), + }, + }) + if err != nil { + t.Fatalf("paymentmethod.New: %v", err) + } + pi, err := paymentintent.New(&stripe.PaymentIntentParams{ + Amount: stripe.Int64(4200), + Currency: stripe.String("usd"), + PaymentMethod: stripe.String(pm.ID), + Confirm: stripe.Bool(true), + }) + if err != nil { + t.Fatalf("paymentintent.New+confirm: %v", err) + } + if pi.Status != stripe.PaymentIntentStatusSucceeded { + t.Fatalf("PI status = %q, want succeeded", pi.Status) + } + Record(t, "stripe-go/v86", "stripe-style", "paymentintent create+confirm -> succeeded") + + // ===== SDK-iterator pagination walks has_more pages ===== + + for i := 0; i < 3; i++ { + _, err := customer.New(&stripe.CustomerParams{ + Name: stripe.String("paging " + string(rune('a'+i))), + }) + if err != nil { + t.Fatalf("customer.New paging seed %d: %v", i, err) + } + } + + params := &stripe.CustomerListParams{ListParams: stripe.ListParams{Limit: stripe.Int64(2)}} + iter := customer.List(params) + seen := 0 + for iter.Next() { + if iter.Customer().ID == "" { + t.Fatal("iterator yielded empty customer id") + } + seen++ + } + if err := iter.Err(); err != nil { + t.Fatalf("iterator: %v", err) + } + if seen < 4 { // 4+ customers total, pages of 2 — the iterator MUST + // have followed has_more/starting_after at least twice + t.Fatalf("iterator walked only %d customers; pagination not followed", seen) + } + Record(t, "stripe-go/v86", "stripe-style", "SDK iterator walks has_more pages (4+ over limit=2)") + + // ===== Webhook delivery verified by the SDK's own HMAC validator ===== + + // Register the sink as a webhook endpoint via the real API (raw POST, + // so it carries the same bearer the SDK would). + regReq, err := http.NewRequest("POST", base+"/v1/webhook_endpoints", + strings.NewReader("url="+sink.URL+"&enabled_events[]=payment_intent.succeeded&enabled_events[]=customer.created")) + if err != nil { + t.Fatal(err) + } + regReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + regReq.Header.Set("Authorization", "Bearer "+stripe.Key) + regResp, err := HTTPClient().Do(regReq) + if err != nil { + t.Fatalf("webhook_endpoint create: %v", err) + } + if err != nil { + t.Fatalf("webhook_endpoint create: %v", err) + } + io.Copy(io.Discard, regResp.Body) + regResp.Body.Close() + if regResp.StatusCode < 200 || regResp.StatusCode >= 300 { + t.Fatalf("webhook_endpoint create -> %d", regResp.StatusCode) + } + + // Trigger a payment_intent.succeeded. + _, err = paymentintent.New(&stripe.PaymentIntentParams{ + Amount: stripe.Int64(9900), + Currency: stripe.String("usd"), + PaymentMethod: stripe.String(pm.ID), + Confirm: stripe.Bool(true), + }) + if err != nil { + t.Fatalf("paymentintent.New for webhook: %v", err) + } + + waitForDelivery(t, &mu, &deliveries, "payment_intent.succeeded") + + mu.Lock() + d := deliveries[len(deliveries)-1] + mu.Unlock() + + const secret = "whsec_stunt_mock_0123456789abcdef0123456789abcdef" + // The adapter pins the acacia API shape it was built against; the SDK + // line pins its own current version (dahlia-era). Version skew is + // expected across SDK majors — the HMAC and the payload are the + // contract, and the pinned version is asserted below. + event, err := webhook.ConstructEventWithOptions(d.payload, d.headers.Get("Stripe-Signature"), secret, webhook.ConstructEventOptions{ + IgnoreAPIVersionMismatch: true, + }) + if err != nil { + t.Fatalf("webhook.ConstructEventWithOptions (SDK HMAC verify): %v", err) + } + if event.APIVersion != "2025-01-27.acacia" { + t.Fatalf("event.APIVersion = %q, want the adapter's pin 2025-01-27.acacia", event.APIVersion) + } + if event.Type != "payment_intent.succeeded" { + t.Fatalf("event.Type = %q", event.Type) + } + var piPayload struct { + ID string `json:"id"` + Status string `json:"status"` + } + if err := json.Unmarshal(event.Data.Raw, &piPayload); err != nil { + t.Fatalf("event.Data.Raw unmarshal: %v", err) + } + if !strings.HasPrefix(piPayload.ID, "pi_") || piPayload.Status != "succeeded" { + t.Fatalf("event.data.object = %+v", piPayload) + } + Record(t, "stripe-go/v86", "stripe-style", "webhook.ConstructEvent verifies HMAC + parses data.object") + + _ = ctx +} + +// waitForDelivery polls the sink until an event of the wanted type +// arrives (delivery is async) or the deadline passes. +func waitForDelivery(t *testing.T, mu *sync.Mutex, deliveries *[]struct { + payload []byte + headers http.Header +}, eventType string) { + t.Helper() + deadline := time.Now().Add(15 * time.Second) + for time.Now().Before(deadline) { + mu.Lock() + for _, d := range *deliveries { + var probe struct { + Type string `json:"type"` + } + if json.Unmarshal(d.payload, &probe) == nil && probe.Type == eventType { + mu.Unlock() + return + } + } + mu.Unlock() + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("no %s delivery arrived at the sink", eventType) +} diff --git a/internal/engine/adapter_dispatch.go b/internal/engine/adapter_dispatch.go index 3daa7a2c..dd01d81a 100644 --- a/internal/engine/adapter_dispatch.go +++ b/internal/engine/adapter_dispatch.go @@ -267,9 +267,34 @@ func (st *serviceState) getOrLoadVM(scriptPath string) (*starlark.VM, error) { // /charges/{id} matches /charges/abc123 (params={id:abc123}) // /charges/{id}/refund matches /charges/abc123/refund // /accounts({id}) matches /accounts(abc123) (params={id:abc123}) +// +// matchRoute matches a route pattern against a request path, capturing +// params. A pattern whose LAST segment is {name+} is greedy: it captures +// the remaining path verbatim (slashes included) — the S3/Cloudflare +// object-key shape, where keys like photos/2024/a.jpg are one path param. func matchRoute(pattern, path string) (map[string]string, bool) { patSegs := splitPathSegments(pattern) pathSegs := splitPathSegments(path) + if n := len(patSegs); n > 0 { + last := patSegs[n-1] + if len(last) >= 4 && last[0] == '{' && last[len(last)-1] == '}' && last[len(last)-2] == '+' { + name := last[1 : len(last)-2] + if name == "" { + return nil, false + } + if len(pathSegs) < n { + return nil, false + } + params := map[string]string{} + for i := 0; i < n-1; i++ { + if !matchSegment(patSegs[i], pathSegs[i], params) { + return nil, false + } + } + params[name] = strings.Join(pathSegs[n-1:], "/") + return params, true + } + } if len(patSegs) != len(pathSegs) { return nil, false } diff --git a/internal/engine/fuzz_parse_test.go b/internal/engine/fuzz_parse_test.go index 58ef4b3b..566c5c94 100644 --- a/internal/engine/fuzz_parse_test.go +++ b/internal/engine/fuzz_parse_test.go @@ -86,3 +86,21 @@ func FuzzParseFormBody(f *testing.F) { } }) } + +// The greedy {name+} terminal segment (S3 object keys) is pinned here and +// in the router unit tests. +func TestMatchRouteGreedy(t *testing.T) { + params, ok := matchRoute("/{bucket}/{key+}", "/my-bucket/photos/2024/a.jpg") + if !ok { + t.Fatal("greedy route did not match a slashed key") + } + if params["bucket"] != "my-bucket" || params["key"] != "photos/2024/a.jpg" { + t.Fatalf("params = %v", params) + } + if _, ok := matchRoute("/{bucket}/{key+}", "/only-bucket"); ok { + t.Fatal("greedy route matched without a key segment") + } + if params, ok := matchRoute("/{bucket}/{key+}", "/b/flat.txt"); !ok || params["key"] != "flat.txt" { + t.Fatalf("flat key: %v %v", params, ok) + } +} diff --git a/justfile b/justfile index 01afd925..1c0e7aca 100644 --- a/justfile +++ b/justfile @@ -85,6 +85,13 @@ cross-build: test: go test -race ./... +# SDK conformance: run REAL provider SDKs (stripe-go, aws-sdk-go-v2, +# go-github) against booted adapters. Nested module under conformance/ so +# the SDK deps never touch the stunt binary's graph. Set +# RUN_CONFORMANCE_SCOREBOARD= to dump a TSV of passed checks. +conformance: + cd conformance && go test ./... -count=1 -v + # Coverage-guided fuzzing — each target for the given time (default 30s; # pass just fuzz 2m for longer rounds). The fuzz seed corpora also run as # regular tests in `just test`, so discovered inputs stay pinned forever. From 30c748e3c5a910250fa625f402f306605fb5b960 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Mon, 17 Aug 2026 21:47:19 +0300 Subject: [PATCH 2/2] fix(review): charges amount coercion + conformance hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on PR #64, all addressed: - MAJOR: charges.star had the same amount-as-string bug the conformance run caught in PaymentIntents — form-encoded creates stored "2000" and echoed it as a JSON string, which typed SDKs reject. Coerced at create; charge.New added to the conformance suite as the pin (typed int64 round-trip). - Greedy {key+} capture documented honestly: URL-decoded, outer slashes trimmed (dir/ and dir address the same key) — CHANGELOG and adapters/README now say what it actually does. - conformance/go.mod keeps go 1.24 with the constraint documented: aws-sdk-go-v2/config v1.32.37 requires it; CI's GOTOOLCHAIN=auto upgrades transparently (lowering to 1.23 breaks the build). - Pagination boundary: the iterator count alone cannot distinguish walked pages from one big page — a raw ?limit=2 page now pins len(data)==2 and has_more=true alongside the walk. - Boot fails (not skips) when the adapters dir is absent — absence in this repo is a layout bug, not a green skip; just conformance runs under -race like the root suite; dead duplicated error check removed. 19 conformance checks green; main-module suite + gates green. --- CHANGELOG.md | 8 ++-- adapters/README.md | 2 +- adapters/stripe-style/scripts/charges.star | 4 +- conformance/go.mod | 21 ++++++----- conformance/go.sum | 44 +++++++++++----------- conformance/harness.go | 4 +- conformance/stripe_test.go | 43 +++++++++++++++++++-- justfile | 2 +- 8 files changed, 86 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f73c0ad..3dafd759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,10 +39,10 @@ All notable changes to **stunt** are documented here. The format is based on ### Engine - **Greedy route params (`{name+}`).** A route pattern whose last - segment is `{key+}` captures the rest of the path verbatim, slashes - included — the object-key shape S3- and Cloudflare-style providers - need. Whole-segment `{name}` and embedded `prefix{p}suffix` matching - are unchanged. + segment is `{key+}` captures the rest of the URL-decoded path, slashes + included (outer slashes trimmed, so `dir/` and `dir` address the same + key) — the object-key shape S3-style providers need. Whole-segment + `{name}` and embedded `prefix{p}suffix` matching are unchanged. ## [0.46.0] — 2026-08-17 diff --git a/adapters/README.md b/adapters/README.md index c9dc6d81..419941d3 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -353,7 +353,7 @@ Every handler receives a `req` argument with: | `req.headers` | `dict[str, str]` | Request headers (case-insensitive lookups; `req.headers.get("authorization")` finds `Authorization`). `req` also supports dict access (`req["method"]`, `req.get("query")`) | | `req.body` | `dict` | Parsed JSON body (empty dict if no body) | | `req.raw_body` | `str` | The verbatim request body bytes (as a string). Use it for non-JSON / binary content (e.g. an S3 object upload) where the parsed `body` map is meaningless — store it via `store_blob` so it round-trips byte-exact | -| `req.params` | `dict[str, str]` | Path parameters extracted from route. `{id}` matches one segment; a terminal `{key+}` is greedy and captures the remaining path verbatim, slashes included (the S3 object-key shape: `/{bucket}/{key+}` matches `photos/2024/a.jpg` as one key) | +| `req.params` | `dict[str, str]` | Path parameters extracted from route. `{id}` matches one segment; a terminal `{key+}` is greedy and captures the remaining URL-decoded path (outer slashes trimmed, so a trailing slash collapses — `dir/` and `dir` address the same key), slashes included (the S3 object-key shape: `/{bucket}/{key+}` matches `photos/2024/a.jpg` as one key) | | `req.query` | `dict[str, str]` | Query parameters (first value of each key) | ## Serializing concurrent handler calls (`concurrency_key`) diff --git a/adapters/stripe-style/scripts/charges.star b/adapters/stripe-style/scripts/charges.star index cd594730..de95e39c 100644 --- a/adapters/stripe-style/scripts/charges.star +++ b/adapters/stripe-style/scripts/charges.star @@ -45,7 +45,9 @@ def on_create_charge(req): body = {} charge_id = _next_id("ch") - amount = body.get("amount", 0) + # Form bodies deliver amount as a STRING; money fields must render as + # JSON numbers for typed SDKs (stripe-go rejects "amount":"2000"). + amount = _num(body.get("amount", 0)) currency = body.get("currency", "usd") customer = body.get("customer", None) description = body.get("description", None) diff --git a/conformance/go.mod b/conformance/go.mod index b9f2000c..571fdc1a 100644 --- a/conformance/go.mod +++ b/conformance/go.mod @@ -1,30 +1,33 @@ module stuntapi.com/stunt/conformance -go 1.24 +// aws-sdk-go-v2/config v1.32.37 requires go >= 1.24. CI's setup-go pins +// 1.23.3 and GOTOOLCHAIN=auto upgrades transparently; a GOTOOLCHAIN=local +// environment needs a 1.24+ toolchain for `just conformance` only. +go 1.24.0 require ( github.com/aws/aws-sdk-go-v2/config v1.32.37 github.com/aws/aws-sdk-go-v2/credentials v1.19.36 - github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 github.com/google/go-github/v66 v66.0.0 github.com/stripe/stripe-go/v86 v86.3.0 - golang.org/x/oauth2 v0.26.0 + golang.org/x/oauth2 v0.30.0 stuntapi.com/stunt v0.46.0 ) require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/aws/aws-sdk-go-v2 v1.43.6 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 // indirect @@ -41,9 +44,9 @@ require ( github.com/vektah/gqlparser/v2 v2.5.36 // indirect go.starlark.net v0.0.0-20240925182052-1207426daebd // indirect golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/conformance/go.sum b/conformance/go.sum index d3fbec7e..da18779f 100644 --- a/conformance/go.sum +++ b/conformance/go.sum @@ -4,8 +4,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aws/aws-sdk-go-v2 v1.43.6 h1:RrmFcqCBxkJuf7g1axVo5krB4jM/AO8r5e5oujrgdoQ= github.com/aws/aws-sdk-go-v2 v1.43.6/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 h1:LAfOuhAH331fmOjTQpAaOlH+Ftn7RzSDJ2VFwjdMMy4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18/go.mod h1:4e5xhuXHx1e4U9EthvbPP1r/DIMp5c2823OL8karzcM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E= github.com/aws/aws-sdk-go-v2/config v1.32.37 h1:Ljl7LOJB6ym0liuEl0+TZ3d7f5I8MEZN1Cj9PINlj/g= github.com/aws/aws-sdk-go-v2/config v1.32.37/go.mod h1:WJ7pe7ZPpmG8Q5kKS53zeypIV4FBGACxmte8Uc6SgUc= github.com/aws/aws-sdk-go-v2/credentials v1.19.36 h1:84s5xMme6ENYEdKG8rsbSFFg/8+lbHBeM9QYSO0gnDk= @@ -20,14 +20,14 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 h1:A3UAuCmx7LyUcrixBTzKJYYIUZ2 github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38/go.mod h1:1PDUYG9Z+JrbbsobsAZHjWOm9QBT/djiK3QbykTL5Z4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30 h1:5437eMoOwqqQpZn2XJy74mlDCuPYL81texMT3mXqgtU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.30/go.mod h1:xfu2m3dOpvW8lj98wQYa8V9ku/Rta59hsbireGzhh3A= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24 h1:mdPwDQPqxlw9Sc62Nt15yjEcARaDbPXkjRYtXsUripo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.24/go.mod h1:ls5ytnwLTcQaUu32fMYXFI3MjpKuTwL840PAm9iqyEg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 h1:a3D4AjrOrTrP8+d9ILBthqrElf0z1JNol09Xvnwcys8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37/go.mod h1:ky0gTu+ukvUTuUKFIpp6Wid4oninrkCyvbFkVs0kpHM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38 h1:gX8B8y3Ho30B1LPxefDKMi/HZqWEb47U9ogs3DtSG0M= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.38/go.mod h1:l5WblZlcmGPe4/O7JY2HO25Z+xqTBvyfTyFbRMf8gYw= -github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2 h1:GNU0/xtPEXMKilJZ/a8BedeuQnvu+Usi6qVm9EFfncc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.107.2/go.mod h1:4jYWUecEsQtE73jPl7p3jrbYXH5ffcR4gegyCygagfg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32 h1:jWXtZdCnhXa9sGFixRaU2AxT4DIVse9HS4E2f+/KwV0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.32/go.mod h1:9JS1UpfVvyD/ZPX8GsKb/Pq8scEM+7GP5fqh9SwH7po= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0 h1:7QZWVJZWzHivHWIa+5TELLaBBkbuoj0GPwQtMlJ0sqk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.0/go.mod h1:fcvq5L7dK+5cQFicEJwpI6e6Wn8NY2i6yT5wRLYVc7s= github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 h1:i68sFvXidKlkiSvI7d7Ilc1/UvW4CtBOaivH7jhG4fs= github.com/aws/aws-sdk-go-v2/service/signin v1.5.6/go.mod h1:/h7Obr9WTtzbjTHGASRQwLN7Bupw+TC3x8x7fyx39hE= github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 h1:tpfGChmjUmv3W9WlRvy+stwKDTbFFdq8Zk9DbFPrfMU= @@ -105,21 +105,21 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= -golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= diff --git a/conformance/harness.go b/conformance/harness.go index d12c1a0e..6f554724 100644 --- a/conformance/harness.go +++ b/conformance/harness.go @@ -36,7 +36,9 @@ func Boot(t *testing.T, adapter string, webhookURL ...string) string { t.Fatal(err) } if _, err := os.Stat(dir); err != nil { - t.Skipf("adapter %s not present", adapter) + // Within this repo every reference adapter exists — absence is a + // layout/CWD bug, not a condition to skip green. + t.Fatalf("adapter %s not present (run via `just conformance` from the repo root): %v", adapter, err) } svcCfg := map[string]any{} diff --git a/conformance/stripe_test.go b/conformance/stripe_test.go index b8d14264..86b145db 100644 --- a/conformance/stripe_test.go +++ b/conformance/stripe_test.go @@ -12,6 +12,7 @@ import ( "time" stripe "github.com/stripe/stripe-go/v86" + "github.com/stripe/stripe-go/v86/charge" "github.com/stripe/stripe-go/v86/customer" "github.com/stripe/stripe-go/v86/paymentintent" "github.com/stripe/stripe-go/v86/paymentmethod" @@ -104,6 +105,21 @@ func TestStripeSDKConformance(t *testing.T) { } Record(t, "stripe-go/v86", "stripe-style", "paymentintent create+confirm -> succeeded") + // Charges are the legacy sibling entry point — this is where the + // amount-as-string bug originally survived the PI fix. + ch, err := charge.New(&stripe.ChargeParams{ + Amount: stripe.Int64(2000), + Currency: stripe.String("usd"), + Source: &stripe.PaymentSourceSourceParams{Token: stripe.String("tok_visa")}, + }) + if err != nil { + t.Fatalf("charge.New: %v", err) + } + if ch.Amount != 2000 { + t.Fatalf("charge.Amount = %d, want 2000 (typed int64)", ch.Amount) + } + Record(t, "stripe-go/v86", "stripe-style", "charge.New (typed amount round-trip)") + // ===== SDK-iterator pagination walks has_more pages ===== for i := 0; i < 3; i++ { @@ -133,6 +149,30 @@ func TestStripeSDKConformance(t *testing.T) { } Record(t, "stripe-go/v86", "stripe-style", "SDK iterator walks has_more pages (4+ over limit=2)") + // The iterator count alone cannot distinguish walked pages from one + // big page — pin the limit and has_more on a raw page too. + pageReq, err := http.NewRequest("GET", base+"/v1/customers?limit=2", nil) + if err != nil { + t.Fatal(err) + } + pageReq.Header.Set("Authorization", "Bearer "+stripe.Key) + pageResp, err := HTTPClient().Do(pageReq) + if err != nil { + t.Fatal(err) + } + pageBody, _ := io.ReadAll(pageResp.Body) + pageResp.Body.Close() + var raw struct { + Data []json.RawMessage `json:"data"` + HasMore bool `json:"has_more"` + } + if err := json.Unmarshal(pageBody, &raw); err != nil { + t.Fatalf("raw page unmarshal: %v (%s)", err, pageBody) + } + if len(raw.Data) != 2 || !raw.HasMore { + t.Fatalf("raw page: len=%d has_more=%v, want 2/true (limit must be honored)", len(raw.Data), raw.HasMore) + } + // ===== Webhook delivery verified by the SDK's own HMAC validator ===== // Register the sink as a webhook endpoint via the real API (raw POST, @@ -148,9 +188,6 @@ func TestStripeSDKConformance(t *testing.T) { if err != nil { t.Fatalf("webhook_endpoint create: %v", err) } - if err != nil { - t.Fatalf("webhook_endpoint create: %v", err) - } io.Copy(io.Discard, regResp.Body) regResp.Body.Close() if regResp.StatusCode < 200 || regResp.StatusCode >= 300 { diff --git a/justfile b/justfile index 1c0e7aca..ab439f05 100644 --- a/justfile +++ b/justfile @@ -90,7 +90,7 @@ test: # the SDK deps never touch the stunt binary's graph. Set # RUN_CONFORMANCE_SCOREBOARD= to dump a TSV of passed checks. conformance: - cd conformance && go test ./... -count=1 -v + cd conformance && go test ./... -count=1 -race -v # Coverage-guided fuzzing — each target for the given time (default 30s; # pass just fuzz 2m for longer rounds). The fuzz seed corpora also run as