From 5d25fcd0f1987c17788b3a460e2a8aa101de1a92 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 23 Oct 2026 05:13:36 +0000 Subject: [PATCH 1/2] chore(test): fix the 2026-07-28 review's mock, CI-doc, known-issues and e2e findings - internal/mocks: a mistyped .Return value no longer panics when the test registered itself with the mock's Test method; it fails that test (Errorf + FailNow) under the method's name. 82 of the 83 type-assertion sites in MockConfigStore, MockAuthStore and the Secrets Manager, SES and SNS mocks go through testReporter (GetPendingExecutionsTx is left alone: open PR #1217 deletes it). Without a registered test the mock still panics, now naming the method. - frontend-build.yml: drop the paths filter so `Build frontend` reports on every PR (a path-filtered required check blocks every PR outside its paths); a fail-closed first step skips the build when nothing under frontend/ changed. docs/ci-required-checks.md explains why. - known-issues.md: the #731 entry named a role the ARM template does not define and a stale branch; correct the name and move it to Resolved, pointing re-deploys at the #1545 revoke-first steps. - tests/e2e: TestVersionEndpoint asserts version, git_sha and build_time instead of any non-empty JSON object. Refs #353 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/frontend-build.yml | 39 +++- docs/ci-required-checks.md | 30 ++- internal/mocks/report.go | 79 ++++++++ internal/mocks/report_test.go | 286 +++++++++++++++++++++++++++ internal/mocks/secretsmanager.go | 10 +- internal/mocks/ses.go | 6 +- internal/mocks/sns.go | 6 +- internal/mocks/stores.go | 168 ++++++++-------- known-issues.md | 42 ++-- tests/e2e/e2e_test.go | 18 +- 10 files changed, 568 insertions(+), 116 deletions(-) create mode 100644 internal/mocks/report.go create mode 100644 internal/mocks/report_test.go diff --git a/.github/workflows/frontend-build.yml b/.github/workflows/frontend-build.yml index c589e35c3..106267faf 100644 --- a/.github/workflows/frontend-build.yml +++ b/.github/workflows/frontend-build.yml @@ -18,13 +18,16 @@ name: Frontend build (PR) # # See also: #177 (sentinel), #191 (this PR gate). +# No `paths:` filter, on purpose. docs/ci-required-checks.md asks for +# `Build frontend` to be a required status check, and a path-filtered workflow +# reports no status at all on a PR that touches none of its paths: such a PR +# would wait forever on "Expected - Waiting for status to be reported". So +# the job runs on every PR and its first step decides whether the build steps +# are needed. Do not restore the filter. on: pull_request: branches: - main - paths: - - "frontend/**" - - ".github/workflows/frontend-build.yml" permissions: contents: read @@ -48,8 +51,33 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false + # The pull_request checkout is the test-merge commit; depth 2 brings + # in its first parent (the base branch tip) for the diff below. + fetch-depth: 2 + + # Replaces the old `paths:` filter (see the comment on `on:`). It fails + # closed: if the changed files cannot be listed, the build runs. + - name: Detect frontend changes + id: changes + working-directory: . + run: | + set -euo pipefail + pattern='^(frontend/|\.github/workflows/frontend-build\.yml$)' + if ! git rev-parse -q --verify 'HEAD^2' >/dev/null || + ! changed="$(git diff --name-only 'HEAD^1' HEAD)"; then + echo "::warning::Could not list the files this PR changes; running the frontend build anyway." + echo "frontend=true" >>"$GITHUB_OUTPUT" + exit 0 + fi + if grep -qE "$pattern" <<<"$changed"; then + echo "frontend=true" >>"$GITHUB_OUTPUT" + else + echo "No frontend changes in this PR; skipping the frontend build." + echo "frontend=false" >>"$GITHUB_OUTPUT" + fi - name: Set up Node.js + if: steps.changes.outputs.frontend == 'true' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "24" @@ -57,18 +85,23 @@ jobs: cache-dependency-path: frontend/package-lock.json - name: Install dependencies + if: steps.changes.outputs.frontend == 'true' run: npm ci - name: Lint + if: steps.changes.outputs.frontend == 'true' run: npm run lint - name: TypeScript typecheck + if: steps.changes.outputs.frontend == 'true' run: npm run typecheck - name: Build + if: steps.changes.outputs.frontend == 'true' run: npm run build # Same invocation as frontend-build-sentinel.yml; the sentinel only # fires post-merge on main / feat/**, so this is the pre-merge jest gate. - name: Run frontend tests + if: steps.changes.outputs.frontend == 'true' run: npx jest --no-coverage --silent diff --git a/docs/ci-required-checks.md b/docs/ci-required-checks.md index 019b9bd23..19c97fd26 100644 --- a/docs/ci-required-checks.md +++ b/docs/ci-required-checks.md @@ -6,7 +6,7 @@ CUDly repository and the admin commands needed to enforce them. ## Context PR #232 (merged 2026-05-03) added `.github/workflows/frontend-build.yml`, -which runs `npm ci`, `npm run typecheck`, and `npm run build` on every PR +which runs `npm ci`, `npm run typecheck`, and `npm run build` for PRs touching `frontend/**`. The workflow shipped, but the corresponding `Build frontend` check was never added to the branch-protection required-checks list. A PR that breaks the frontend build can currently be @@ -22,6 +22,28 @@ trigger was removed in the commit that merged that branch into `main` > so the context GitHub reports (and that must be entered here) is > `Build frontend`. +### Why the workflow has no `paths:` filter + +A required check must report a status on **every** PR. A workflow with a +`paths:` filter reports nothing at all on a PR that touches none of its +paths, so with `Build frontend` required, every backend-only, +Terraform-only or docs-only PR would sit at "Expected - Waiting for status +to be reported" and could never be merged, with no failing check to point +at. + +`frontend-build.yml` therefore triggers on every PR to `main`, and the +`Build frontend` job's first step (`Detect frontend changes`) diffs the +PR's test-merge commit against its base. When nothing under `frontend/` +(or the workflow file itself) changed, the job skips its build steps and +passes in seconds; otherwise it runs the full build. If the changed files +cannot be listed, it runs the build rather than skipping it. + +Do not "optimize" this back into a `paths:` filter, and do not add a second +job or workflow named `Build frontend` to fill the gap: on a PR that touches +both frontend and other files, both would report under the same name, and +the requirement must never be satisfiable by the stand-in's pass while the +real build is failing. + ## Required checks -- canonical list `main` should require: @@ -89,6 +111,12 @@ The output should include `"Build frontend"`. Open a draft PR with a deliberate TypeScript error in `frontend/` and confirm that the `Build frontend` check turns red and the merge button is blocked. +Then confirm the other half: on any PR that touches nothing under +`frontend/` (a docs-only change will do), `Build frontend` must still +report, pass, and log "No frontend changes in this PR; skipping the +frontend build." A PR stuck at "Expected - Waiting for status to be +reported" means the trigger has been path-filtered again. + ## Notes - The `strict` flag in Step 3 requires the branch to be up to date before diff --git a/internal/mocks/report.go b/internal/mocks/report.go new file mode 100644 index 000000000..01cbb8646 --- /dev/null +++ b/internal/mocks/report.go @@ -0,0 +1,79 @@ +package mocks + +import ( + "sync" + + "github.com/stretchr/testify/mock" +) + +// testReporter routes a mock's own test-authoring errors to the test that owns +// the mock, so they fail that test instead of crashing the test binary. +// +// The error it handles today is a .Return(...) value whose type does not match +// what the mocked method returns (wrongReturnType, in stores.go). That used to +// be a bare panic, which ends the whole test binary: every remaining test in +// the package goes unrun, TestMain teardown (a shared Postgres container, for +// example) is skipped, and the output is a stack trace rather than a failure +// filed under the test's name. +// +// The test registers itself with the mock's Test method, the same hook testify +// already uses to fail an unexpected call through the test instead of +// panicking. A mock with no registered test keeps the old panic, because there +// is nothing else to report to. +type testReporter struct { + mu sync.Mutex + t mock.TestingT +} + +func (r *testReporter) set(t mock.TestingT) { + r.mu.Lock() + defer r.mu.Unlock() + r.t = t +} + +// fail fails the registered test with msg (Errorf plus FailNow, as testify +// does for an unexpected call), or panics with msg when no test is registered. +func (r *testReporter) fail(msg string) { + r.mu.Lock() + t := r.t + r.mu.Unlock() + if t == nil { + panic(msg) + } + if h, ok := t.(interface{ Helper() }); ok { + h.Helper() + } + fatalf(t, "%s", msg) +} + +// Test registers t with the mock: an unexpected call (testify's own check) and +// a mistyped .Return value (see testReporter) then fail t instead of +// panicking. It shadows the promoted mock.Mock.Test and calls it. +func (m *MockConfigStore) Test(t mock.TestingT) { + m.Mock.Test(t) + m.reporter.set(t) +} + +// Test registers t with the mock. See MockConfigStore.Test. +func (m *MockAuthStore) Test(t mock.TestingT) { + m.Mock.Test(t) + m.reporter.set(t) +} + +// Test registers t with the mock. See MockConfigStore.Test. +func (m *MockSecretsManagerClient) Test(t mock.TestingT) { + m.Mock.Test(t) + m.reporter.set(t) +} + +// Test registers t with the mock. See MockConfigStore.Test. +func (m *MockSESClient) Test(t mock.TestingT) { + m.Mock.Test(t) + m.reporter.set(t) +} + +// Test registers t with the mock. See MockConfigStore.Test. +func (m *MockSNSClient) Test(t mock.TestingT) { + m.Mock.Test(t) + m.reporter.set(t) +} diff --git a/internal/mocks/report_test.go b/internal/mocks/report_test.go new file mode 100644 index 000000000..986c547f4 --- /dev/null +++ b/internal/mocks/report_test.go @@ -0,0 +1,286 @@ +package mocks + +import ( + "context" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "reflect" + "runtime" + "sort" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// fatalT is a mock.TestingT whose FailNow stops the calling goroutine, like a +// real *testing.T, and which keeps the formatted failure messages so a test can +// check what was reported. +type fatalT struct { + mu sync.Mutex + msgs []string +} + +func (f *fatalT) Logf(string, ...interface{}) {} + +func (f *fatalT) Errorf(format string, args ...interface{}) { + f.mu.Lock() + defer f.mu.Unlock() + f.msgs = append(f.msgs, fmt.Sprintf(format, args...)) +} + +func (f *fatalT) FailNow() { runtime.Goexit() } + +func (f *fatalT) messages() string { + f.mu.Lock() + defer f.mu.Unlock() + return strings.Join(f.msgs, "\n") +} + +var _ mock.TestingT = (*fatalT)(nil) + +// callOutcome is what happened when a mock method ran in its own goroutine. +type callOutcome struct { + returned bool // the call returned normally + panicked interface{} // the recovered panic value, if it panicked +} + +// runIsolated runs fn in its own goroutine so a FailNow (runtime.Goexit) or a +// panic inside a mock ends that goroutine rather than the test. +func runIsolated(fn func()) callOutcome { + var out callOutcome + done := make(chan struct{}) + go func() { + defer close(done) + defer func() { out.panicked = recover() }() + fn() + out.returned = true + }() + <-done + return out +} + +// wrongType is a .Return value no mocked method returns. +type wrongType struct{} + +// TestMockConfigStore_WrongReturnTypeFailsRegisteredTest pins the #353 fix on +// one method of each dispatch style: GetGlobalConfig serves a default unless an +// expectation is registered, GetServiceConfig always dispatches. +func TestMockConfigStore_WrongReturnTypeFailsRegisteredTest(t *testing.T) { + for _, tc := range []struct { + method string + call func(*MockConfigStore) + }{ + {"GetGlobalConfig", func(m *MockConfigStore) { _, _ = m.GetGlobalConfig(context.Background()) }}, + {"GetServiceConfig", func(m *MockConfigStore) { _, _ = m.GetServiceConfig(context.Background(), "aws", "ec2") }}, + } { + t.Run(tc.method, func(t *testing.T) { + ft := &fatalT{} + m := &MockConfigStore{} + m.Test(ft) + m.On(tc.method, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(wrongType{}, nil) + m.On(tc.method, mock.Anything).Maybe().Return(wrongType{}, nil) + + out := runIsolated(func() { tc.call(m) }) + + assert.Nil(t, out.panicked, "a mistyped .Return must fail the registered test, not panic") + assert.False(t, out.returned, "the failure must stop the test goroutine, as t.Fatal does") + assert.Contains(t, ft.messages(), "mock: "+tc.method+" returned mocks.wrongType") + }) + } +} + +// TestMock_WrongReturnTypePanicsWithoutRegisteredTest documents the fallback: +// with no test registered there is nowhere to report to, so the mock still +// panics, now with a message that names the method. +func TestMock_WrongReturnTypePanicsWithoutRegisteredTest(t *testing.T) { + m := &MockConfigStore{} + m.On("GetGlobalConfig", mock.Anything).Return(wrongType{}, nil) + + out := runIsolated(func() { _, _ = m.GetGlobalConfig(context.Background()) }) + + require.NotNil(t, out.panicked) + assert.Contains(t, fmt.Sprint(out.panicked), "mock: GetGlobalConfig returned mocks.wrongType") +} + +// TestMock_TestStillRegistersWithTestify checks the shadowing Test method still +// hands t to testify: an unexpected call fails the test instead of panicking. +func TestMock_TestStillRegistersWithTestify(t *testing.T) { + ft := &fatalT{} + m := &MockAuthStore{} + m.Test(ft) + + out := runIsolated(func() { _, _ = m.GetUserByID(context.Background(), "user-1") }) + + assert.Nil(t, out.panicked, "testify must fail the registered test on an unexpected call") + assert.False(t, out.returned) + assert.Contains(t, ft.messages(), "GetUserByID") +} + +// TestMocks_EveryWrongReturnTypeSiteReportsItsOwnMethod drives every mock +// method that calls wrongReturnType with a mistyped .Return and checks the +// failure reaches the registered test under that method's name. It catches a +// site whose method-name argument was copied from a neighboring method, and a mock type +// whose Test method does not wire up its reporter. +// +// The sites are found by parsing the package source, so methods added later +// are covered without editing this test. +func TestMocks_EveryWrongReturnTypeSiteReportsItsOwnMethod(t *testing.T) { + sites := wrongReturnTypeSites(t) + require.GreaterOrEqual(t, len(sites), 80, + "found only %d wrongReturnType call sites; the source scan has broken", len(sites)) + + newMock := map[string]func() (mockUnderTest, interface{}){ + "MockConfigStore": func() (mockUnderTest, interface{}) { + m := &MockConfigStore{} + return m, m + }, + "MockAuthStore": func() (mockUnderTest, interface{}) { + m := &MockAuthStore{} + return m, m + }, + "MockSecretsManagerClient": func() (mockUnderTest, interface{}) { + m := &MockSecretsManagerClient{} + return m, m + }, + "MockSESClient": func() (mockUnderTest, interface{}) { + m := &MockSESClient{} + return m, m + }, + "MockSNSClient": func() (mockUnderTest, interface{}) { + m := &MockSNSClient{} + return m, m + }, + } + + for _, s := range sites { + t.Run(s.recvType+"."+s.method, func(t *testing.T) { + ctor, ok := newMock[s.recvType] + require.True(t, ok, "no constructor for mock type %s; add it to this test", s.recvType) + m, recv := ctor() + ft := &fatalT{} + m.Test(ft) + matchers := make([]interface{}, s.calledArity) + for i := range matchers { + matchers[i] = mock.Anything + } + m.On(s.method, matchers...).Return(wrongType{}, nil, nil, nil) + + method := reflect.ValueOf(recv).MethodByName(s.method) + require.True(t, method.IsValid(), "method %s not found", s.method) + in := zeroArgs(method.Type()) + + out := runIsolated(func() { method.Call(in) }) + + assert.Nil(t, out.panicked, "%s panicked instead of failing the registered test", s.method) + assert.False(t, out.returned, "%s returned instead of failing the registered test", s.method) + assert.Contains(t, ft.messages(), "mock: "+s.method+" returned mocks.wrongType", + "%s must report the mistyped .Return under its own name", s.method) + }) + } +} + +// mockUnderTest is the part of a testify mock this file drives. +type mockUnderTest interface { + Test(mock.TestingT) + On(methodName string, arguments ...interface{}) *mock.Call +} + +// zeroArgs builds an argument list for a method: a live context, a non-nil +// pointer for each pointer parameter (so a method that reads a field before +// dispatching does not crash first), and zero values otherwise. +func zeroArgs(ft reflect.Type) []reflect.Value { + ctxType := reflect.TypeOf((*context.Context)(nil)).Elem() + n := ft.NumIn() + if ft.IsVariadic() { + n-- + } + in := make([]reflect.Value, 0, n) + for i := 0; i < n; i++ { + p := ft.In(i) + switch { + case p == ctxType: + in = append(in, reflect.ValueOf(context.Background())) + case p.Kind() == reflect.Ptr: + in = append(in, reflect.New(p.Elem())) + default: + in = append(in, reflect.Zero(p)) + } + } + return in +} + +// wrongReturnTypeSite is one mock method that reports a mistyped .Return. +type wrongReturnTypeSite struct { + recvType string + method string + calledArity int // number of arguments the method passes to m.Called +} + +// wrongReturnTypeSites parses the package's non-test files for methods whose +// body calls .reporter.wrongReturnType. +func wrongReturnTypeSites(t *testing.T) []wrongReturnTypeSite { + t.Helper() + entries, err := os.ReadDir(".") + require.NoError(t, err) + fset := token.NewFileSet() + var sites []wrongReturnTypeSite + for _, e := range entries { + name := e.Name() + if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + f, err := parser.ParseFile(fset, name, nil, 0) + require.NoError(t, err) + for _, decl := range f.Decls { + if site, ok := siteFromDecl(decl); ok { + sites = append(sites, site) + } + } + } + sort.Slice(sites, func(i, j int) bool { + return sites[i].recvType+"."+sites[i].method < sites[j].recvType+"."+sites[j].method + }) + return sites +} + +func siteFromDecl(decl ast.Decl) (wrongReturnTypeSite, bool) { + fd, ok := decl.(*ast.FuncDecl) + if !ok || fd.Recv == nil || len(fd.Recv.List) != 1 || fd.Body == nil { + return wrongReturnTypeSite{}, false + } + star, ok := fd.Recv.List[0].Type.(*ast.StarExpr) + if !ok { + return wrongReturnTypeSite{}, false + } + recvIdent, ok := star.X.(*ast.Ident) + if !ok { + return wrongReturnTypeSite{}, false + } + site := wrongReturnTypeSite{recvType: recvIdent.Name, method: fd.Name.Name, calledArity: -1} + reports := false + ast.Inspect(fd.Body, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + switch sel.Sel.Name { + case "wrongReturnType": + reports = true + case "Called": + site.calledArity = len(call.Args) + } + return true + }) + return site, reports && site.calledArity >= 0 +} diff --git a/internal/mocks/secretsmanager.go b/internal/mocks/secretsmanager.go index f44b94586..6c26f4b44 100644 --- a/internal/mocks/secretsmanager.go +++ b/internal/mocks/secretsmanager.go @@ -2,7 +2,6 @@ package mocks import ( "context" - "fmt" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/stretchr/testify/mock" @@ -11,6 +10,9 @@ import ( // MockSecretsManagerClient is a mock implementation of Secrets Manager client. type MockSecretsManagerClient struct { mock.Mock + // reporter fails the registered test on a mistyped .Return value. See + // report.go. + reporter testReporter } // GetSecretValue mocks the GetSecretValue operation. @@ -21,7 +23,7 @@ func (m *MockSecretsManagerClient) GetSecretValue(ctx context.Context, input *se } v, ok := args.Get(0).(*secretsmanager.GetSecretValueOutput) if !ok { - panic(fmt.Sprintf("mock: expected *secretsmanager.GetSecretValueOutput, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetSecretValue", "*secretsmanager.GetSecretValueOutput", args.Get(0)) } return v, args.Error(1) } @@ -34,7 +36,7 @@ func (m *MockSecretsManagerClient) CreateSecret(ctx context.Context, input *secr } v, ok := args.Get(0).(*secretsmanager.CreateSecretOutput) if !ok { - panic(fmt.Sprintf("mock: expected *secretsmanager.CreateSecretOutput, got %T", args.Get(0))) + m.reporter.wrongReturnType("CreateSecret", "*secretsmanager.CreateSecretOutput", args.Get(0)) } return v, args.Error(1) } @@ -47,7 +49,7 @@ func (m *MockSecretsManagerClient) UpdateSecret(ctx context.Context, input *secr } v, ok := args.Get(0).(*secretsmanager.UpdateSecretOutput) if !ok { - panic(fmt.Sprintf("mock: expected *secretsmanager.UpdateSecretOutput, got %T", args.Get(0))) + m.reporter.wrongReturnType("UpdateSecret", "*secretsmanager.UpdateSecretOutput", args.Get(0)) } return v, args.Error(1) } diff --git a/internal/mocks/ses.go b/internal/mocks/ses.go index caf445f9f..38edd0b08 100644 --- a/internal/mocks/ses.go +++ b/internal/mocks/ses.go @@ -2,7 +2,6 @@ package mocks import ( "context" - "fmt" "github.com/aws/aws-sdk-go-v2/service/sesv2" "github.com/stretchr/testify/mock" @@ -11,6 +10,9 @@ import ( // MockSESClient is a mock implementation of SES client. type MockSESClient struct { //nolint:revive // exported: doc comment style intentional mock.Mock + // reporter fails the registered test on a mistyped .Return value. See + // report.go. + reporter testReporter } // SendEmail mocks the SendEmail operation. @@ -21,7 +23,7 @@ func (m *MockSESClient) SendEmail(ctx context.Context, input *sesv2.SendEmailInp } v, ok := args.Get(0).(*sesv2.SendEmailOutput) if !ok { - panic(fmt.Sprintf("mock: expected *sesv2.SendEmailOutput, got %T", args.Get(0))) + m.reporter.wrongReturnType("SendEmail", "*sesv2.SendEmailOutput", args.Get(0)) } return v, args.Error(1) } diff --git a/internal/mocks/sns.go b/internal/mocks/sns.go index ad4151f03..7e977a817 100644 --- a/internal/mocks/sns.go +++ b/internal/mocks/sns.go @@ -2,7 +2,6 @@ package mocks import ( "context" - "fmt" "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/stretchr/testify/mock" @@ -11,6 +10,9 @@ import ( // MockSNSClient is a mock implementation of SNS client. type MockSNSClient struct { //nolint:revive // exported: doc comment style intentional mock.Mock + // reporter fails the registered test on a mistyped .Return value. See + // report.go. + reporter testReporter } // Publish mocks the Publish operation. @@ -21,7 +23,7 @@ func (m *MockSNSClient) Publish(ctx context.Context, input *sns.PublishInput, op } v, ok := args.Get(0).(*sns.PublishOutput) if !ok { - panic(fmt.Sprintf("mock: expected *sns.PublishOutput, got %T", args.Get(0))) + m.reporter.wrongReturnType("Publish", "*sns.PublishOutput", args.Get(0)) } return v, args.Error(1) } diff --git a/internal/mocks/stores.go b/internal/mocks/stores.go index e946ed5f8..b8d9de8e7 100644 --- a/internal/mocks/stores.go +++ b/internal/mocks/stores.go @@ -63,6 +63,9 @@ type MockConfigStore struct { // fanOutShapes backs PinExecutionFanOutShape's default: first writer wins // per lineage key, like the real table. fanOutShapes fanOutShapePins + // reporter fails the registered test on a mistyped .Return value. See + // report.go. + reporter testReporter } // GetGlobalConfig mocks the GetGlobalConfig operation. Returns an empty @@ -79,7 +82,7 @@ func (m *MockConfigStore) GetGlobalConfig(ctx context.Context) (*config.GlobalCo } v, ok := args.Get(0).(*config.GlobalConfig) if !ok { - panic(fmt.Sprintf("mock: expected *config.GlobalConfig, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetGlobalConfig", "*config.GlobalConfig", args.Get(0)) } return v, args.Error(1) } @@ -105,7 +108,7 @@ func (m *MockConfigStore) UpdateGlobalConfigAtomic(ctx context.Context, apply fu } v, ok := args.Get(0).(*config.GlobalConfig) if !ok { - panic(fmt.Sprintf("mock: expected *config.GlobalConfig, got %T", args.Get(0))) + m.reporter.wrongReturnType("UpdateGlobalConfigAtomic", "*config.GlobalConfig", args.Get(0)) } return v, args.Error(1) } @@ -136,7 +139,7 @@ func (m *MockConfigStore) GetServiceConfig(ctx context.Context, provider, servic } v, ok := args.Get(0).(*config.ServiceConfig) if !ok { - panic(fmt.Sprintf("mock: expected *config.ServiceConfig, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetServiceConfig", "*config.ServiceConfig", args.Get(0)) } return v, args.Error(1) } @@ -157,7 +160,7 @@ func (m *MockConfigStore) ListServiceConfigs(ctx context.Context) ([]config.Serv } v, ok := args.Get(0).([]config.ServiceConfig) if !ok { - panic(fmt.Sprintf("mock: expected []config.ServiceConfig, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListServiceConfigs", "[]config.ServiceConfig", args.Get(0)) } return v, args.Error(1) } @@ -187,7 +190,7 @@ func (m *MockConfigStore) GetPurchasePlan(ctx context.Context, planID string) (* } v, ok := args.Get(0).(*config.PurchasePlan) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchasePlan, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPurchasePlan", "*config.PurchasePlan", args.Get(0)) } return v, args.Error(1) } @@ -226,7 +229,7 @@ func (m *MockConfigStore) GetStuckRampSteps(ctx context.Context) (map[string]con } v, ok := args.Get(0).(map[string]config.RampStepBlock) if !ok { - panic(fmt.Sprintf("mock: expected map[string]config.RampStepBlock, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetStuckRampSteps", "map[string]config.RampStepBlock", args.Get(0)) } return v, args.Error(1) } @@ -247,7 +250,7 @@ func (m *MockConfigStore) LockPurchasePlanTx(ctx context.Context, tx pgx.Tx, pla } v, ok := args.Get(0).(*config.PurchasePlan) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchasePlan, got %T", args.Get(0))) + m.reporter.wrongReturnType("LockPurchasePlanTx", "*config.PurchasePlan", args.Get(0)) } return v, args.Error(1) } @@ -279,7 +282,7 @@ func (m *MockConfigStore) ListExecutionsForPlanStepTx(ctx context.Context, tx pg } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListExecutionsForPlanStepTx", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -306,7 +309,7 @@ func (m *MockConfigStore) GetPlanRampBaselineTx(ctx context.Context, tx pgx.Tx, } v, ok := args.Get(0).(config.PlanRampBaseline) if !ok { - panic(fmt.Sprintf("mock: expected config.PlanRampBaseline, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPlanRampBaselineTx", "config.PlanRampBaseline", args.Get(0)) } return v, args.Error(1) } @@ -322,7 +325,7 @@ func (m *MockConfigStore) ListPlanCommittedTx(ctx context.Context, tx pgx.Tx, pl } v, ok := args.Get(0).(map[string]config.PlanRampQuantity) if !ok { - panic(fmt.Sprintf("mock: expected map[string]config.PlanRampQuantity, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListPlanCommittedTx", "map[string]config.PlanRampQuantity", args.Get(0)) } return v, args.Error(1) } @@ -355,7 +358,7 @@ func (m *MockConfigStore) OccupiedRampStepsInRangeTx(ctx context.Context, tx pgx } v, ok := args.Get(0).([]int) if !ok { - panic(fmt.Sprintf("mock: expected []int, got %T", args.Get(0))) + m.reporter.wrongReturnType("OccupiedRampStepsInRangeTx", "[]int", args.Get(0)) } return v, args.Error(1) } @@ -392,7 +395,7 @@ func (m *MockConfigStore) ListPurchasePlans(ctx context.Context, filter config.P } v, ok := args.Get(0).([]config.PurchasePlan) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchasePlan, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListPurchasePlans", "[]config.PurchasePlan", args.Get(0)) } return v, args.Error(1) } @@ -459,7 +462,7 @@ func (m *MockConfigStore) TransitionExecutionStatus(ctx context.Context, executi } v, ok := args.Get(0).(*config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("TransitionExecutionStatus", "*config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -479,7 +482,7 @@ func (m *MockConfigStore) TransitionExecutionStatusPinned(ctx context.Context, e } v, ok := args.Get(0).(*config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("TransitionExecutionStatusPinned", "*config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -499,7 +502,7 @@ func (m *MockConfigStore) TransitionExecutionStatusPinnedTx(ctx context.Context, } v, ok := args.Get(0).(*config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("TransitionExecutionStatusPinnedTx", "*config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -516,7 +519,7 @@ func (m *MockConfigStore) TransitionExecutionStatusTokenPinned(ctx context.Conte } v, ok := args.Get(0).(*config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("TransitionExecutionStatusTokenPinned", "*config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -569,7 +572,7 @@ func (m *MockConfigStore) LockLadderTrancheForRetryTx(ctx context.Context, tx pg args := m.Called(ctx, tx, idempotencyKey) status, ok := args.Get(0).(ladder.TrancheStatus) if !ok { - panic(fmt.Sprintf("mock: expected ladder.TrancheStatus, got %T", args.Get(0))) + m.reporter.wrongReturnType("LockLadderTrancheForRetryTx", "ladder.TrancheStatus", args.Get(0)) } return status, args.Bool(1), args.Error(2) } @@ -587,7 +590,7 @@ func (m *MockConfigStore) GetLadderExecutionGate(ctx context.Context, idempotenc } gate, ok := args.Get(0).(*config.LadderExecutionGate) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderExecutionGate, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetLadderExecutionGate", "*config.LadderExecutionGate", args.Get(0)) } return gate, args.Error(1) } @@ -631,7 +634,7 @@ func (m *MockConfigStore) GetPendingExecutions(ctx context.Context) ([]config.Pu } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPendingExecutions", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -645,7 +648,7 @@ func (m *MockConfigStore) GetExecutionByID(ctx context.Context, executionID stri } v, ok := args.Get(0).(*config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetExecutionByID", "*config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -659,7 +662,7 @@ func (m *MockConfigStore) GetExecutionByPlanAndDate(ctx context.Context, planID } v, ok := args.Get(0).(*config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetExecutionByPlanAndDate", "*config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -724,7 +727,7 @@ func (m *MockConfigStore) ListPendingExecutionIDsForAccount(ctx context.Context, } v, ok := args.Get(0).([]string) if !ok { - panic(fmt.Sprintf("mock: expected []string, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListPendingExecutionIDsForAccount", "[]string", args.Get(0)) } return v, args.Error(1) } @@ -745,7 +748,7 @@ func (m *MockConfigStore) GetPurchaseHistory(ctx context.Context, accountID stri } v, ok := args.Get(0).([]config.PurchaseHistoryRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseHistoryRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPurchaseHistory", "[]config.PurchaseHistoryRecord", args.Get(0)) } return v, args.Error(1) } @@ -759,7 +762,7 @@ func (m *MockConfigStore) GetAllPurchaseHistory(ctx context.Context, limit int) } v, ok := args.Get(0).([]config.PurchaseHistoryRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseHistoryRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetAllPurchaseHistory", "[]config.PurchaseHistoryRecord", args.Get(0)) } return v, args.Error(1) } @@ -773,7 +776,7 @@ func (m *MockConfigStore) GetActivePurchaseHistory(ctx context.Context, asOf tim } v, ok := args.Get(0).([]config.PurchaseHistoryRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseHistoryRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetActivePurchaseHistory", "[]config.PurchaseHistoryRecord", args.Get(0)) } return v, args.Error(1) } @@ -787,7 +790,7 @@ func (m *MockConfigStore) GetPurchaseHistoryFiltered(ctx context.Context, filter } v, ok := args.Get(0).([]config.PurchaseHistoryRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseHistoryRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPurchaseHistoryFiltered", "[]config.PurchaseHistoryRecord", args.Get(0)) } return v, args.Error(1) } @@ -801,7 +804,7 @@ func (m *MockConfigStore) GetPurchaseHistoryByPurchaseID(ctx context.Context, pu } v, ok := args.Get(0).(*config.PurchaseHistoryRecord) if !ok { - panic(fmt.Sprintf("mock: expected *config.PurchaseHistoryRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPurchaseHistoryByPurchaseID", "*config.PurchaseHistoryRecord", args.Get(0)) } return v, args.Error(1) } @@ -846,7 +849,7 @@ func (m *MockConfigStore) GetPurchaseHistoryInFlight(ctx context.Context) ([]*co } v, ok := args.Get(0).([]*config.PurchaseHistoryRecord) if !ok { - panic(fmt.Sprintf("mock: expected []*config.PurchaseHistoryRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPurchaseHistoryInFlight", "[]*config.PurchaseHistoryRecord", args.Get(0)) } return v, args.Error(1) } @@ -873,7 +876,7 @@ func (m *MockConfigStore) ClaimMarketplaceListingSlot(ctx context.Context, purch if v := args.Get(0); v != nil { c, ok := v.(*config.MarketplaceListingClaim) if !ok { - panic(fmt.Sprintf("mock: expected *config.MarketplaceListingClaim, got %T", v)) + m.reporter.wrongReturnType("ClaimMarketplaceListingSlot", "*config.MarketplaceListingClaim", v) } claim = c } @@ -927,7 +930,7 @@ func (m *MockConfigStore) GetRIExchangeRecord(ctx context.Context, id string) (* } v, ok := args.Get(0).(*config.RIExchangeRecord) if !ok { - panic(fmt.Sprintf("mock: expected *config.RIExchangeRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetRIExchangeRecord", "*config.RIExchangeRecord", args.Get(0)) } return v, args.Error(1) } @@ -940,7 +943,7 @@ func (m *MockConfigStore) GetRIExchangeRecordByToken(ctx context.Context, token } v, ok := args.Get(0).(*config.RIExchangeRecord) if !ok { - panic(fmt.Sprintf("mock: expected *config.RIExchangeRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetRIExchangeRecordByToken", "*config.RIExchangeRecord", args.Get(0)) } return v, args.Error(1) } @@ -953,7 +956,7 @@ func (m *MockConfigStore) GetRIExchangeHistory(ctx context.Context, since time.T } v, ok := args.Get(0).([]config.RIExchangeRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.RIExchangeRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetRIExchangeHistory", "[]config.RIExchangeRecord", args.Get(0)) } return v, args.Error(1) } @@ -966,7 +969,7 @@ func (m *MockConfigStore) GetRIExchangeHistoryForAccounts(ctx context.Context, s } v, ok := args.Get(0).([]config.RIExchangeRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.RIExchangeRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetRIExchangeHistoryForAccounts", "[]config.RIExchangeRecord", args.Get(0)) } return v, args.Error(1) } @@ -979,7 +982,7 @@ func (m *MockConfigStore) TransitionRIExchangeStatus(ctx context.Context, id, fr } v, ok := args.Get(0).(*config.RIExchangeRecord) if !ok { - panic(fmt.Sprintf("mock: expected *config.RIExchangeRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("TransitionRIExchangeStatus", "*config.RIExchangeRecord", args.Get(0)) } return v, args.Error(1) } @@ -1013,7 +1016,7 @@ func (m *MockConfigStore) CancelAllPendingExchanges(ctx context.Context) (int64, args := m.Called(ctx) v, ok := args.Get(0).(int64) if !ok { - panic(fmt.Sprintf("mock: expected int64, got %T", args.Get(0))) + m.reporter.wrongReturnType("CancelAllPendingExchanges", "int64", args.Get(0)) } return v, args.Error(1) } @@ -1023,7 +1026,7 @@ func (m *MockConfigStore) CancelPendingExchangesByOrigin(ctx context.Context, or args := m.Called(ctx, origin) v, ok := args.Get(0).(int64) if !ok { - panic(fmt.Sprintf("mock: expected int64, got %T", args.Get(0))) + m.reporter.wrongReturnType("CancelPendingExchangesByOrigin", "int64", args.Get(0)) } return v, args.Error(1) } @@ -1036,7 +1039,7 @@ func (m *MockConfigStore) GetStaleProcessingExchanges(ctx context.Context, older } v, ok := args.Get(0).([]config.RIExchangeRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.RIExchangeRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetStaleProcessingExchanges", "[]config.RIExchangeRecord", args.Get(0)) } return v, args.Error(1) } @@ -1068,6 +1071,9 @@ func (m *MockConfigStore) FinalizeRIExchangeDirectExecution(ctx context.Context, // MockAuthStore is a mock implementation of auth.Store. type MockAuthStore struct { mock.Mock + // reporter fails the registered test on a mistyped .Return value. See + // report.go. + reporter testReporter } // GetUserByID mocks the GetUserByID operation. @@ -1078,7 +1084,7 @@ func (m *MockAuthStore) GetUserByID(ctx context.Context, userID string) (*auth.U } v, ok := args.Get(0).(*auth.User) if !ok { - panic(fmt.Sprintf("mock: expected *auth.User, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetUserByID", "*auth.User", args.Get(0)) } return v, args.Error(1) } @@ -1091,7 +1097,7 @@ func (m *MockAuthStore) GetUserByEmail(ctx context.Context, email string) (*auth } v, ok := args.Get(0).(*auth.User) if !ok { - panic(fmt.Sprintf("mock: expected *auth.User, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetUserByEmail", "*auth.User", args.Get(0)) } return v, args.Error(1) } @@ -1179,7 +1185,7 @@ func (m *MockAuthStore) ListUsers(ctx context.Context) ([]auth.User, error) { } v, ok := args.Get(0).([]auth.User) if !ok { - panic(fmt.Sprintf("mock: expected []auth.User, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListUsers", "[]auth.User", args.Get(0)) } return v, args.Error(1) } @@ -1192,7 +1198,7 @@ func (m *MockAuthStore) GetUserByResetToken(ctx context.Context, token string) ( } v, ok := args.Get(0).(*auth.User) if !ok { - panic(fmt.Sprintf("mock: expected *auth.User, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetUserByResetToken", "*auth.User", args.Get(0)) } return v, args.Error(1) } @@ -1217,7 +1223,7 @@ func (m *MockAuthStore) GetGroup(ctx context.Context, groupID string) (*auth.Gro } v, ok := args.Get(0).(*auth.Group) if !ok { - panic(fmt.Sprintf("mock: expected *auth.Group, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetGroup", "*auth.Group", args.Get(0)) } return v, args.Error(1) } @@ -1248,7 +1254,7 @@ func (m *MockAuthStore) ListGroups(ctx context.Context) ([]auth.Group, error) { } v, ok := args.Get(0).([]auth.Group) if !ok { - panic(fmt.Sprintf("mock: expected []auth.Group, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListGroups", "[]auth.Group", args.Get(0)) } return v, args.Error(1) } @@ -1273,7 +1279,7 @@ func (m *MockAuthStore) GetSession(ctx context.Context, token string) (*auth.Ses } v, ok := args.Get(0).(*auth.Session) if !ok { - panic(fmt.Sprintf("mock: expected *auth.Session, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetSession", "*auth.Session", args.Get(0)) } return v, args.Error(1) } @@ -1312,7 +1318,7 @@ func (m *MockAuthStore) GetAPIKeyByID(ctx context.Context, keyID string) (*auth. } v, ok := args.Get(0).(*auth.UserAPIKey) if !ok { - panic(fmt.Sprintf("mock: expected *auth.UserAPIKey, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetAPIKeyByID", "*auth.UserAPIKey", args.Get(0)) } return v, args.Error(1) } @@ -1325,7 +1331,7 @@ func (m *MockAuthStore) GetAPIKeyByHash(ctx context.Context, keyHash string) (*a } v, ok := args.Get(0).(*auth.UserAPIKey) if !ok { - panic(fmt.Sprintf("mock: expected *auth.UserAPIKey, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetAPIKeyByHash", "*auth.UserAPIKey", args.Get(0)) } return v, args.Error(1) } @@ -1338,7 +1344,7 @@ func (m *MockAuthStore) ListAPIKeysByUser(ctx context.Context, userID string) ([ } v, ok := args.Get(0).([]*auth.UserAPIKey) if !ok { - panic(fmt.Sprintf("mock: expected []*auth.UserAPIKey, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListAPIKeysByUser", "[]*auth.UserAPIKey", args.Get(0)) } return v, args.Error(1) } @@ -1404,7 +1410,7 @@ func (m *MockConfigStore) GetCloudAccount(ctx context.Context, id string) (*conf } v, ok := args.Get(0).(*config.CloudAccount) if !ok { - panic(fmt.Sprintf("mock: expected *config.CloudAccount, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetCloudAccount", "*config.CloudAccount", args.Get(0)) } return v, args.Error(1) } @@ -1423,7 +1429,7 @@ func (m *MockConfigStore) GetCloudAccountByExternalID(ctx context.Context, provi } v, ok := args.Get(0).(*config.CloudAccount) if !ok { - panic(fmt.Sprintf("mock: expected *config.CloudAccount, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetCloudAccountByExternalID", "*config.CloudAccount", args.Get(0)) } return v, args.Error(1) } @@ -1463,7 +1469,7 @@ func (m *MockConfigStore) ListCloudAccounts(ctx context.Context, filter config.C } v, ok := args.Get(0).([]config.CloudAccount) if !ok { - panic(fmt.Sprintf("mock: expected []config.CloudAccount, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListCloudAccounts", "[]config.CloudAccount", args.Get(0)) } return v, args.Error(1) } @@ -1519,7 +1525,7 @@ func (m *MockConfigStore) GetAccountServiceOverride(ctx context.Context, account } v, ok := args.Get(0).(*config.AccountServiceOverride) if !ok { - panic(fmt.Sprintf("mock: expected *config.AccountServiceOverride, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetAccountServiceOverride", "*config.AccountServiceOverride", args.Get(0)) } return v, args.Error(1) } @@ -1556,7 +1562,7 @@ func (m *MockConfigStore) ListAccountServiceOverrides(ctx context.Context, accou } v, ok := args.Get(0).([]config.AccountServiceOverride) if !ok { - panic(fmt.Sprintf("mock: expected []config.AccountServiceOverride, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListAccountServiceOverrides", "[]config.AccountServiceOverride", args.Get(0)) } return v, args.Error(1) } @@ -1589,7 +1595,7 @@ func (m *MockConfigStore) GetPlanAccounts(ctx context.Context, planID string) ([ } v, ok := args.Get(0).([]config.CloudAccount) if !ok { - panic(fmt.Sprintf("mock: expected []config.CloudAccount, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPlanAccounts", "[]config.CloudAccount", args.Get(0)) } return v, args.Error(1) } @@ -1600,7 +1606,7 @@ func (m *MockConfigStore) CleanupOldExecutions(ctx context.Context, retentionDay args := m.Called(ctx, retentionDays) v, ok := args.Get(0).(int64) if !ok { - panic(fmt.Sprintf("mock: expected int64, got %T", args.Get(0))) + m.reporter.wrongReturnType("CleanupOldExecutions", "int64", args.Get(0)) } return v, args.Error(1) } @@ -1640,7 +1646,7 @@ func (m *MockConfigStore) ListStoredRecommendations(ctx context.Context, filter } v, ok := args.Get(0).([]config.RecommendationRecord) if !ok { - panic(fmt.Sprintf("mock: expected []config.RecommendationRecord, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListStoredRecommendations", "[]config.RecommendationRecord", args.Get(0)) } return v, args.Error(1) } @@ -1656,7 +1662,7 @@ func (m *MockConfigStore) GetRecommendationsFreshness(ctx context.Context) (*con } v, ok := args.Get(0).(*config.RecommendationsFreshness) if !ok { - panic(fmt.Sprintf("mock: expected *config.RecommendationsFreshness, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetRecommendationsFreshness", "*config.RecommendationsFreshness", args.Get(0)) } return v, args.Error(1) } @@ -1690,7 +1696,7 @@ func (m *MockConfigStore) GetRIUtilizationCache(ctx context.Context, region stri } v, ok := args.Get(0).(*config.RIUtilizationCacheEntry) if !ok { - panic(fmt.Sprintf("mock: expected *config.RIUtilizationCacheEntry, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetRIUtilizationCache", "*config.RIUtilizationCacheEntry", args.Get(0)) } return v, args.Error(1) } @@ -1718,7 +1724,7 @@ func (m *MockConfigStore) GetAccountRegistration(ctx context.Context, id string) } v, ok := args.Get(0).(*config.AccountRegistration) if !ok { - panic(fmt.Sprintf("mock: expected *config.AccountRegistration, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetAccountRegistration", "*config.AccountRegistration", args.Get(0)) } return v, args.Error(1) } @@ -1731,7 +1737,7 @@ func (m *MockConfigStore) GetAccountRegistrationByToken(ctx context.Context, tok } v, ok := args.Get(0).(*config.AccountRegistration) if !ok { - panic(fmt.Sprintf("mock: expected *config.AccountRegistration, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetAccountRegistrationByToken", "*config.AccountRegistration", args.Get(0)) } return v, args.Error(1) } @@ -1744,7 +1750,7 @@ func (m *MockConfigStore) ListAccountRegistrations(ctx context.Context, filter c } v, ok := args.Get(0).([]config.AccountRegistration) if !ok { - panic(fmt.Sprintf("mock: expected []config.AccountRegistration, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListAccountRegistrations", "[]config.AccountRegistration", args.Get(0)) } return v, args.Error(1) } @@ -1818,7 +1824,7 @@ func (m *MockConfigStore) ListActiveSuppressions(ctx context.Context) ([]config. } v, ok := args.Get(0).([]config.PurchaseSuppression) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseSuppression, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListActiveSuppressions", "[]config.PurchaseSuppression", args.Get(0)) } return v, args.Error(1) } @@ -1838,7 +1844,7 @@ func (m *MockConfigStore) ListRecentSubmitsTx(ctx context.Context, tx pgx.Tx, cr } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListRecentSubmitsTx", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -1895,7 +1901,7 @@ func (m *MockConfigStore) GetExecutionsByStatuses(ctx context.Context, statuses } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetExecutionsByStatuses", "[]config.PurchaseExecution", args.Get(0)) } return filterExecutionsLikeStore(v, statuses, limit), args.Error(1) } @@ -1951,7 +1957,7 @@ func (m *MockConfigStore) CountExecutionsByPlanAndStatus(ctx context.Context, st } v, ok := args.Get(0).(map[string]config.ExecutionStatusCounts) if !ok { - panic(fmt.Sprintf("mock: expected map[string]config.ExecutionStatusCounts, got %T", args.Get(0))) + m.reporter.wrongReturnType("CountExecutionsByPlanAndStatus", "map[string]config.ExecutionStatusCounts", args.Get(0)) } return v, args.Error(1) } @@ -1965,7 +1971,7 @@ func (m *MockConfigStore) GetPlannedExecutions(ctx context.Context, statuses []s } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetPlannedExecutions", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -1979,7 +1985,7 @@ func (m *MockConfigStore) GetStaleApprovedExecutions(ctx context.Context, olderT } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetStaleApprovedExecutions", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -1993,7 +1999,7 @@ func (m *MockConfigStore) ListStuckExecutions(ctx context.Context, statuses []st } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("ListStuckExecutions", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -2012,7 +2018,7 @@ func (m *MockConfigStore) GetScheduledExecutionsDue(ctx context.Context) ([]conf } v, ok := args.Get(0).([]config.PurchaseExecution) if !ok { - panic(fmt.Sprintf("mock: expected []config.PurchaseExecution, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetScheduledExecutionsDue", "[]config.PurchaseExecution", args.Get(0)) } return v, args.Error(1) } @@ -2059,7 +2065,7 @@ func (m *MockConfigStore) GetLadderConfigs(ctx context.Context) ([]config.Ladder } v, ok := args.Get(0).([]config.LadderConfigDB) if !ok { - panic(fmt.Sprintf("mock: expected []config.LadderConfigDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetLadderConfigs", "[]config.LadderConfigDB", args.Get(0)) } return v, args.Error(1) } @@ -2077,7 +2083,7 @@ func (m *MockConfigStore) GetLadderConfig(ctx context.Context, cloudAccountID, p } v, ok := args.Get(0).(*config.LadderConfigDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderConfigDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetLadderConfig", "*config.LadderConfigDB", args.Get(0)) } return v, args.Error(1) } @@ -2091,7 +2097,7 @@ func (m *MockConfigStore) UpsertLadderConfig(ctx context.Context, cfg *config.La } v, ok := args.Get(0).(*config.LadderConfigDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderConfigDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("UpsertLadderConfig", "*config.LadderConfigDB", args.Get(0)) } return v, args.Error(1) } @@ -2109,7 +2115,7 @@ func (m *MockConfigStore) SaveLadderRun(ctx context.Context, run *config.LadderR } v, ok := args.Get(0).(*config.LadderRunDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderRunDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("SaveLadderRun", "*config.LadderRunDB", args.Get(0)) } return v, args.Error(1) } @@ -2130,7 +2136,7 @@ func (m *MockConfigStore) PlanAndSaveLadderRun(ctx context.Context, configID str } v, ok := args.Get(0).(*config.LadderRunDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderRunDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("PlanAndSaveLadderRun", "*config.LadderRunDB", args.Get(0)) } return v, args.Error(1) } @@ -2148,7 +2154,7 @@ func (m *MockConfigStore) GetLadderRun(ctx context.Context, id string) (*config. } v, ok := args.Get(0).(*config.LadderRunDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderRunDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("GetLadderRun", "*config.LadderRunDB", args.Get(0)) } return v, args.Error(1) } @@ -2176,7 +2182,7 @@ func (m *MockConfigStore) LatestLadderRunStartedAt(ctx context.Context, configID } v, ok := args.Get(0).(*time.Time) if !ok { - panic(fmt.Sprintf("mock: expected *time.Time, got %T", args.Get(0))) + m.reporter.wrongReturnType("LatestLadderRunStartedAt", "*time.Time", args.Get(0)) } return v, args.Error(1) } @@ -2194,7 +2200,7 @@ func (m *MockConfigStore) CancelLadderRun(ctx context.Context, id string, fromSt } v, ok := args.Get(0).(*config.LadderRunDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderRunDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("CancelLadderRun", "*config.LadderRunDB", args.Get(0)) } return v, args.Error(1) } @@ -2212,7 +2218,7 @@ func (m *MockConfigStore) TransitionLadderRunStatus(ctx context.Context, id stri } v, ok := args.Get(0).(*config.LadderRunDB) if !ok { - panic(fmt.Sprintf("mock: expected *config.LadderRunDB, got %T", args.Get(0))) + m.reporter.wrongReturnType("TransitionLadderRunStatus", "*config.LadderRunDB", args.Get(0)) } return v, args.Error(1) } @@ -2248,6 +2254,14 @@ func isExpected(m *mock.Mock, method string) bool { return false } +// wrongReturnType reports that method's registered .Return value got is not of +// type want. It fails the test registered through the mock's Test method, or +// panics when there is none. See report.go. +func (r *testReporter) wrongReturnType(method, want string, got interface{}) { + r.fail(fmt.Sprintf("mock: %s returned %T from its .Return(...), but the method returns %s; "+ + "fix the value passed to .Return for this expectation", method, got, want)) +} + // Compile-time interface compliance checks. var _ config.StoreInterface = (*MockConfigStore)(nil) var _ auth.StoreInterface = (*MockAuthStore)(nil) diff --git a/known-issues.md b/known-issues.md index 28abb8511..dcdef5610 100644 --- a/known-issues.md +++ b/known-issues.md @@ -128,35 +128,27 @@ as an explicitly consented step (the `az role assignment create` mirror of step 1) and must not be reintroduced into the template. `scripts/check-azure-role-parity.sh` fails CI if it is. -### Azure ARM template re-deployment required for purchase support (issue #731) - -The built-in "Reservation Purchaser" role (f7b75c60-3036-4b75-91c3-6b41c27c1689) -does not include `Microsoft.Capacity/calculateprice/action`, -`Microsoft.Capacity/reservationorders/write`, or -`Microsoft.BillingBenefits/savingsPlanOrderAliases/write`. Without these, -the live purchase API returns 403. - -`arm/CUDly-CrossSubscription/template.json` has been updated (fix/731-arm-roles) -to add a custom role "CUDly Reservation and Savings Plan Purchaser" that enumerates -all required actions explicitly. Existing tenants who applied the ARM template before -this fix MUST re-deploy it: - -```bash -az deployment sub create \ - --location eastus \ - --template-file arm/CUDly-CrossSubscription/template.json \ - --parameters servicePrincipalObjectId= \ - --name CUDly-CrossSubscription \ - --no-prompt -``` - -Until re-deployed, `PurchaseCommitment` and `ValidateOffering` for savings plans -will continue to return 403. - ## Resolved ### Azure +- **ARM template lacked the purchase actions (issue #731)**: Resolved. + The built-in "Reservation Purchaser" role + (f7b75c60-3036-4b75-91c3-6b41c27c1689) does not include + `Microsoft.Capacity/calculatePrice/action`, + `Microsoft.Capacity/reservationOrders/write`, or + `Microsoft.BillingBenefits/savingsPlanOrderAliases/write`, so the live + purchase API returned 403. `arm/CUDly-CrossSubscription/template.json` + now defines and assigns a custom role named + **"CUDly Reservation Purchaser (custom)"** that lists every required + action explicitly; that is the name to look for in the Azure portal or + `az role definition list --custom-role-only true`. Tenants that applied + the template before the fix keep getting 403 from `PurchaseCommitment` + and `ValidateOffering` for savings plans until they re-deploy it. Do not + re-deploy with a bare `az deployment sub create`: follow the issue #1545 + entry under Outstanding, which first revokes the tenant-wide grant older + templates left behind and then re-deploys. + - **ARM template role-definition scoping + `Reservation Reader` tenant gap**: Resolved. `arm/CUDly-CrossSubscription/template.json` now uses unscoped global role-definition paths diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 845e0cbc0..0ba9bc3d7 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -123,8 +123,22 @@ func TestVersionEndpoint(t *testing.T) { if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("GET /version: decode body: %v", err) } - if len(body) == 0 { - t.Error("GET /version: empty JSON body") + // Name the fields (internal/api.VersionResponse) rather than accepting any + // non-empty object: a stub such as {"ok":true} or {"error":"not + // configured"} must fail. git_sha is what an operator compares against the + // branch HEAD to confirm which build is deployed. The compose stack builds + // without a GIT_COMMIT build arg, so its value here is the "unknown" + // sentinel; only presence and type are asserted for it and build_time. + for _, key := range []string{"version", "git_sha", "build_time"} { + v, ok := body[key].(string) + if !ok || v == "" { + t.Errorf("GET /version: %q must be a non-empty string; got body %v", key, body) + } + } + // docker-compose.test.yml sets VERSION=test on cudly-app; seeing it here + // proves the field carries the deployed build's value, not a fallback. + if got := body["version"]; got != "test" { + t.Errorf("GET /version: version = %v, want %q (VERSION in docker-compose.test.yml)", got, "test") } } From cefef1f5d7c0fd8a493500345f4872a96a8e787f Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Sun, 25 Oct 2026 18:29:20 +0000 Subject: [PATCH 2/2] test(mocks): fail the test on a mistyped HistorySummaryAggregate return #1200 added HistorySummaryAggregate with the old panic on a mistyped .Return, which ends the whole test binary. Route it through wrongReturnType like the other sites so a registered test fails under the method's name instead. Refs #353 Co-Authored-By: Claude Opus 5.5 --- internal/mocks/stores.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mocks/stores.go b/internal/mocks/stores.go index c6de039fd..d1d0284ad 100644 --- a/internal/mocks/stores.go +++ b/internal/mocks/stores.go @@ -1976,7 +1976,7 @@ func (m *MockConfigStore) HistorySummaryAggregate(ctx context.Context, filter co } v, ok := args.Get(0).(*config.HistorySummaryGroups) if !ok { - panic(fmt.Sprintf("mock: expected *config.HistorySummaryGroups, got %T", args.Get(0))) + m.reporter.wrongReturnType("HistorySummaryAggregate", "*config.HistorySummaryGroups", args.Get(0)) } return v, args.Error(1) }