Skip to content

Commit 8edef47

Browse files
authored
test(e2e): run invoice edit flows with credits enabled (#4589)
1 parent db3581c commit 8edef47

14 files changed

Lines changed: 662 additions & 318 deletions

.github/workflows/ci.yaml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,8 @@ jobs:
729729
image: $DEPOT_IMAGE_URL
730730
sink-worker:
731731
image: $DEPOT_IMAGE_URL
732+
billing-worker:
733+
image: $DEPOT_IMAGE_URL
732734
EOF
733735
734736
cat e2e/docker-compose.override.yaml
@@ -749,6 +751,8 @@ jobs:
749751
build: ..
750752
sink-worker:
751753
build: ..
754+
billing-worker:
755+
build: ..
752756
EOF
753757
754758
cat e2e/docker-compose.override.yaml
@@ -794,15 +798,35 @@ jobs:
794798

795799
- name: Wait for worker to become ready
796800
run: |
797-
curl --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30000/healthz
801+
curl --fail --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30000/healthz
802+
curl --fail --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30001/healthz
798803
docker ps
799804
800-
- name: Run tests
805+
- name: Run base tests
806+
env:
807+
OPENMETER_ADDRESS: http://localhost:38888
808+
TZ: UTC
809+
run: |
810+
nix develop --impure .#ci -c make -C e2e test-base
811+
812+
- name: Restart Docker Compose infra for credits-disabled tests
813+
run: |
814+
docker compose -f docker-compose.infra.yaml -f docker-compose.openmeter.yaml -f docker-compose.override.yaml down -v
815+
docker compose -f docker-compose.infra.yaml -f docker-compose.openmeter.yaml -f docker-compose.override.yaml -f docker-compose.credits-disabled.yaml up -d
816+
working-directory: e2e
817+
818+
- name: Wait for worker to become ready with credits disabled
819+
run: |
820+
curl --fail --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30000/healthz
821+
curl --fail --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30001/healthz
822+
docker ps
823+
824+
- name: Run credits-disabled tests
801825
env:
802826
OPENMETER_ADDRESS: http://localhost:38888
803827
TZ: UTC
804828
run: |
805-
nix develop --impure .#ci -c go test -v -count=1 -timeout 3m ./e2e/
829+
nix develop --impure .#ci -c make -C e2e test-credits-disabled
806830
807831
- name: Cleanup Docker Compose
808832
run: docker compose -f docker-compose.infra.yaml -f docker-compose.openmeter.yaml -f docker-compose.override.yaml down -v

e2e/Makefile

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ OPENMETER_ADDRESS ?= http://localhost:38888
88
BENCHTIME ?= 20x
99
# Number of times to repeat the whole benchmark (feed >1 into benchstat for variance).
1010
COUNT ?= 1
11+
OPENMETER_E2E_EXTRA_COMPOSE_FILES ?=
1112

1213
.PHONY: bench-governance
1314
bench-governance: ## Benchmark governance query latency (1x1 baseline + 10/50/100 diagonal)
@@ -23,16 +24,43 @@ bench-governance-matrix: ## Benchmark governance query latency (full 3x3 custome
2324

2425
.PHONY: test-local
2526
test-local: ## Run tests against local openmeter
27+
$(call print-target)
28+
$(MAKE) test-local-base
29+
$(MAKE) test-local-credits-disabled
30+
31+
.PHONY: test-local-credits-disabled
32+
test-local-credits-disabled: ## Run credits-disabled e2e tests against local openmeter
2633
$(call print-target)
2734
$(MAKE) env-local-down
28-
$(MAKE) env-local-up
35+
set -e; trap '$(MAKE) env-local-down' EXIT; \
36+
OPENMETER_E2E_EXTRA_COMPOSE_FILES="-f docker-compose.credits-disabled.yaml" $(MAKE) env-local-up; \
37+
$(MAKE) wait-local; \
38+
$(MAKE) test-credits-disabled
2939

30-
# wait for sink-worker to be ready
31-
curl --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30000/healthz
40+
.PHONY: test-local-base
41+
test-local-base: ## Run default e2e tests against local openmeter
42+
$(call print-target)
43+
$(MAKE) env-local-down
44+
set -e; trap '$(MAKE) env-local-down' EXIT; \
45+
$(MAKE) env-local-up; \
46+
$(MAKE) wait-local; \
47+
$(MAKE) test-base
3248

33-
TZ=UTC OPENMETER_ADDRESS=http://localhost:38888 go test -count=1 -v ./...
49+
.PHONY: test-credits-disabled
50+
test-credits-disabled: ## Run e2e tests that require credits.enabled=false
51+
$(call print-target)
52+
TZ=UTC OPENMETER_ADDRESS=$(OPENMETER_ADDRESS) go test -count=1 -v ./creditsdisabled
3453

35-
$(MAKE) env-local-down
54+
.PHONY: test-base
55+
test-base: ## Run the default e2e test package
56+
$(call print-target)
57+
TZ=UTC OPENMETER_ADDRESS=$(OPENMETER_ADDRESS) go test -count=1 -v ./
58+
59+
.PHONY: wait-local
60+
wait-local:
61+
$(call print-target)
62+
curl --fail --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30000/healthz
63+
curl --fail --retry 10 --retry-max-time 120 --retry-all-errors http://localhost:30001/healthz
3664

3765
.PHONY: env-local-down
3866
env-local-down:
@@ -52,6 +80,7 @@ env-local-up:
5280
-f docker-compose.debug-ports.yaml \
5381
-f docker-compose.openmeter.yaml \
5482
-f docker-compose.openmeter-local.yaml \
83+
$(OPENMETER_E2E_EXTRA_COMPOSE_FILES) \
5584
up -d --build --force-recreate
5685

5786
.PHONY: help

e2e/billinginvoices_v3_test.go

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package e2e
22

33
import (
44
"net/http"
5+
"slices"
56
"testing"
67
"time"
78

@@ -20,7 +21,7 @@ import (
2021
// - Create a meter and feature for usage-based billing (v3)
2122
// - Create and publish a plan with a unit rate card backed by the feature (v3)
2223
// - Create a subscription for the customer (v3)
23-
// - Advance the gathering invoice via the v1 InvoicePendingLinesAction to create a standard invoice
24+
// - Create a charge-backed pending line and wait for charges to advance it into a standard invoice
2425
// - List the customer's invoices via the v1 SDK; separate standard from gathering
2526
// - GET the standard invoice via the v3 endpoint and assert the response shape
2627
// - GET the gathering invoice via the v3 endpoint → 404 (gathering invoices are not exposed)
@@ -135,15 +136,16 @@ func TestV3GetBillingInvoice(t *testing.T) {
135136
PaymentTerm: lo.ToPtr(api.PricePaymentTermInAdvance),
136137
}))
137138

139+
invoiceAt := now.Add(time.Hour)
138140
lineResp, err := v1.CreatePendingInvoiceLineWithResponse(t.Context(), customerID, api.InvoicePendingLineCreateInput{
139141
Currency: "USD",
140142
Lines: []api.InvoicePendingLineCreate{
141143
{
142144
Name: uniqueKey("inv_gathering_line_name"),
143-
InvoiceAt: now.Add(-10 * time.Hour),
145+
InvoiceAt: invoiceAt,
144146
Period: api.Period{
145147
From: now.Add(-24 * time.Hour),
146-
To: now.Add(time.Hour),
148+
To: invoiceAt,
147149
},
148150
Price: &price,
149151
},
@@ -176,7 +178,7 @@ func TestV3GetBillingInvoice(t *testing.T) {
176178
InvoiceAt: now.Add(-10 * time.Hour),
177179
Period: api.Period{
178180
From: now.Add(-24 * time.Hour),
179-
To: now.Add(time.Hour),
181+
To: now.Add(-2 * time.Hour),
180182
},
181183
Price: &price,
182184
},
@@ -186,17 +188,24 @@ func TestV3GetBillingInvoice(t *testing.T) {
186188
require.Equal(t, http.StatusCreated, lineResp.StatusCode(), "line: %s", string(lineResp.Body))
187189
require.NotNil(t, lineResp.JSON201)
188190

189-
resp, err := v1.InvoicePendingLinesActionWithResponse(t.Context(), api.InvoicePendingLinesActionInput{
190-
CustomerId: customerID,
191-
ProgressiveBillingOverride: lo.ToPtr(true),
192-
AsOf: lo.ToPtr(now.Add(-1 * time.Hour)),
193-
})
194-
require.NoError(t, err)
195-
require.Equal(t, http.StatusCreated, resp.StatusCode(), "advance: %s", string(resp.Body))
196-
require.NotNil(t, resp.JSON201)
197-
require.NotEmpty(t, *resp.JSON201, "expected at least one standard invoice to be created")
198-
199-
invoiceID = (*resp.JSON201)[0].Id
191+
ctx := t.Context()
192+
customers := api.InvoiceListParamsCustomers{customerID}
193+
assert.EventuallyWithT(t, func(c *assert.CollectT) {
194+
listResp, err := v1.ListInvoicesWithResponse(ctx, &api.ListInvoicesParams{
195+
Customers: &customers,
196+
PageSize: lo.ToPtr(api.PaginationPageSize(100)),
197+
})
198+
require.NoError(c, err)
199+
require.Equal(c, http.StatusOK, listResp.StatusCode(), "list invoices: %s", string(listResp.Body))
200+
require.NotNil(c, listResp.JSON200)
201+
202+
standardInvoiceIdx := slices.IndexFunc(listResp.JSON200.Items, func(inv api.Invoice) bool {
203+
return inv.Status != api.InvoiceStatusGathering
204+
})
205+
require.NotEqual(c, -1, standardInvoiceIdx, "expected charges to advance a pending line into a standard invoice")
206+
207+
invoiceID = listResp.JSON200.Items[standardInvoiceIdx].Id
208+
}, time.Minute, time.Second)
200209
require.NotEmpty(t, invoiceID)
201210
})
202211

@@ -212,10 +221,10 @@ func TestV3GetBillingInvoice(t *testing.T) {
212221
require.NotNil(t, listResp.JSON200)
213222
require.NotEmpty(t, listResp.JSON200.Items, "expected at least one invoice for customer %s (key: %s)", customerID, customerKey)
214223

215-
_, foundStandard := lo.Find(listResp.JSON200.Items, func(inv api.Invoice) bool {
224+
standardInvoiceIdx := slices.IndexFunc(listResp.JSON200.Items, func(inv api.Invoice) bool {
216225
return inv.Status != api.InvoiceStatusGathering
217226
})
218-
require.True(t, foundStandard, "expected at least one non-gathering invoice in the list")
227+
require.NotEqual(t, -1, standardInvoiceIdx, "expected at least one non-gathering invoice in the list")
219228
})
220229

221230
t.Run("Should return the invoice via v3 GET", func(t *testing.T) {

e2e/config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ postgres:
4141
url: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
4242
autoMigrate: migration
4343

44-
# Credits configuration - disabled for e2e tests.
45-
# Test cases in productcatalog_test.go (TestSettlementMode) depend on this setting
46-
# to verify that credit_only settlement mode is rejected when credits are disabled.
44+
# Credits are enabled for the default e2e suite so billing flows run in the same
45+
# mode as production-like credit deployments. Credits-disabled compatibility
46+
# tests run from e2e/creditsdisabled with CREDITS_* env overrides.
4747
credits:
48-
enabled: false
49-
enableCreditThenInvoice: false
48+
enabled: true
49+
enableCreditThenInvoice: true
5050

5151
notification:
5252
lock:

e2e/creditsdisabled/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Credits Disabled E2E Tests
2+
3+
This package contains temporary pre-credit to credit upgrade compatibility tests.
4+
5+
The default e2e suite runs with credits enabled. Tests in this package exercise
6+
behavior that must remain valid for installations that started before credits
7+
were enabled, or that still run with credits disabled during the upgrade window.
8+
9+
Remove this package once credits are fully ready and credits-disabled deployments
10+
are no longer supported by the e2e contract.

e2e/ledger_backfill_test.go renamed to e2e/creditsdisabled/ledger_backfill_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package e2e
1+
package creditsdisabled
22

33
import (
44
"bytes"
@@ -21,8 +21,8 @@ func TestLedgerBackfillAccountsJob(t *testing.T) {
2121
pool := initE2EPostgresPool(t)
2222

2323
suffix := strings.ToLower(ulid.Make().String())
24-
customerA := CreateCustomerWithSubject(t, client, "ledger-backfill-"+suffix+"-a", "ledger-backfill-subject-"+suffix+"-a")
25-
customerB := CreateCustomerWithSubject(t, client, "ledger-backfill-"+suffix+"-b", "ledger-backfill-subject-"+suffix+"-b")
24+
customerA := createCustomerWithSubject(t, client, "ledger-backfill-"+suffix+"-a", "ledger-backfill-subject-"+suffix+"-a")
25+
customerB := createCustomerWithSubject(t, client, "ledger-backfill-"+suffix+"-b", "ledger-backfill-subject-"+suffix+"-b")
2626

2727
customerIDs := []string{customerA.Id, customerB.Id}
2828
namespace := getCustomerNamespace(t, pool, customerA.Id)
@@ -112,7 +112,7 @@ func runJobsBackfillAccounts(t *testing.T, args ...string) string {
112112
base = append(base, args...)
113113

114114
cmd := exec.CommandContext(t.Context(), "docker", base...)
115-
cmd.Dir = "."
115+
cmd.Dir = ".."
116116

117117
var out bytes.Buffer
118118
cmd.Stdout = &out

0 commit comments

Comments
 (0)