Skip to content

Commit dbe378f

Browse files
committed
test(plans): real DB test for Unassigned bucket query (refs #973)
Add a testcontainers-backed integration test (TestPostgresStoreDB_ListPurchasePlans_UnassignedBucket) that exercises the LEFT JOIN + OR NOT EXISTS query introduced in #973 against a real Postgres instance. Seed layout: - planA assigned to accountX: must appear with Unassigned=false - planB with zero plan_accounts rows: must appear with Unassigned=true - planC assigned to accountY only: must NOT appear in accountX filter The discriminating assertion (planB present in accountX-filtered result) fails on the pre-fix INNER JOIN code and passes with the LEFT JOIN fix, so a regression back to INNER JOIN will be caught by CI. Also fix two pre-existing compile errors in store_postgres_test.go (package config_test): add the config. qualifier to PurchasePlanFilter and inline the unexported pf() helper that was inaccessible from the external test package.
1 parent 575e1d1 commit dbe378f

2 files changed

Lines changed: 170 additions & 2 deletions

File tree

‎internal/config/store_postgres_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func TestPostgresStore_PurchasePlans(t *testing.T) {
265265
}
266266

267267
// List all plans
268-
retrieved, err := store.ListPurchasePlans(ctx, PurchasePlanFilter{})
268+
retrieved, err := store.ListPurchasePlans(ctx, config.PurchasePlanFilter{})
269269
require.NoError(t, err)
270270
assert.GreaterOrEqual(t, len(retrieved), 2)
271271
})
@@ -351,7 +351,7 @@ func TestPostgresStore_PurchaseHistory(t *testing.T) {
351351
Term: 3,
352352
Payment: "all-upfront",
353353
UpfrontCost: 2250.00,
354-
MonthlyCost: pf(0),
354+
MonthlyCost: func() *float64 { v := 0.0; return &v }(),
355355
EstimatedSavings: 450.00,
356356
// PlanID intentionally left empty since it needs to be a valid UUID
357357
PlanName: "Test Plan",
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//go:build integration
2+
// +build integration
3+
4+
package config
5+
6+
// TestPostgresStoreDB_ListPurchasePlans_UnassignedBucket is a real-DB
7+
// integration test that exercises the LEFT JOIN + OR NOT EXISTS query
8+
// introduced in #973. It is the discriminating regression guard that
9+
// would FAIL on the pre-fix INNER JOIN code, where plan B (zero
10+
// plan_accounts rows) would be absent from the filtered result entirely.
11+
//
12+
// Seed layout:
13+
// - accountX: a cloud_accounts row whose UUID is used as the filter.
14+
// - accountY: a second cloud_accounts row.
15+
// - planA: assigned to accountX via plan_accounts -> must appear with Unassigned=false.
16+
// - planB: ZERO plan_accounts rows (legacy/unassigned) -> must appear with Unassigned=true.
17+
// - planC: assigned only to accountY -> must NOT appear when filtering by [accountX].
18+
//
19+
// Three assertions prove the SQL is correct:
20+
// 1. planB is present in the accountX-filtered result (fails on INNER JOIN).
21+
// 2. planB.Unassigned == true.
22+
// 3. planC is absent from the accountX-filtered result.
23+
24+
import (
25+
"context"
26+
"testing"
27+
"time"
28+
29+
"github.com/LeanerCloud/CUDly/internal/database/postgres/migrations"
30+
"github.com/LeanerCloud/CUDly/internal/database/postgres/testhelpers"
31+
"github.com/google/uuid"
32+
"github.com/stretchr/testify/assert"
33+
"github.com/stretchr/testify/require"
34+
)
35+
36+
func TestPostgresStoreDB_ListPurchasePlans_UnassignedBucket(t *testing.T) {
37+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
38+
defer cancel()
39+
40+
container, err := testhelpers.SetupPostgresContainer(ctx, t)
41+
if err != nil {
42+
t.Skipf("Skipping integration test: cannot start PostgreSQL container: %v", err)
43+
return
44+
}
45+
t.Cleanup(func() { container.Cleanup(context.Background()) })
46+
47+
err = migrations.RunMigrations(ctx, container.DB.Pool(), getTestMigrationsPath(), "", "")
48+
if err != nil {
49+
t.Skipf("Skipping integration test: migrations failed: %v", err)
50+
return
51+
}
52+
53+
conn := container.DB
54+
store := NewPostgresStore(conn)
55+
56+
// ---- Seed cloud_accounts ----
57+
// These are inserted directly so we control the UUIDs used in plan_accounts.
58+
accountXID := uuid.New().String()
59+
accountYID := uuid.New().String()
60+
insertAccount := func(id, name, externalID string) {
61+
t.Helper()
62+
_, err := conn.Exec(ctx, `
63+
INSERT INTO cloud_accounts
64+
(id, name, enabled, provider, external_id, aws_is_org_root, created_at, updated_at)
65+
VALUES ($1, $2, true, 'aws', $3, false, now(), now())
66+
`, id, name, externalID)
67+
require.NoError(t, err, "seed cloud_accounts: %s", name)
68+
}
69+
insertAccount(accountXID, "Account X", "111111111111")
70+
insertAccount(accountYID, "Account Y", "222222222222")
71+
72+
t.Cleanup(func() {
73+
// plan_accounts rows cascade on purchase_plan delete; cloud_accounts rows do not.
74+
conn.Exec(context.Background(), "DELETE FROM cloud_accounts WHERE id = ANY($1::uuid[])",
75+
[]string{accountXID, accountYID})
76+
})
77+
78+
// ---- Seed purchase_plans ----
79+
makePlan := func(name string) *PurchasePlan {
80+
return &PurchasePlan{
81+
Name: name,
82+
Enabled: true,
83+
NotificationDaysBefore: 3,
84+
Services: map[string]ServiceConfig{},
85+
RampSchedule: PresetRampSchedules["immediate"],
86+
}
87+
}
88+
planA := makePlan("Plan A - assigned to X")
89+
planB := makePlan("Plan B - unassigned (legacy)")
90+
planC := makePlan("Plan C - assigned to Y only")
91+
92+
require.NoError(t, store.CreatePurchasePlan(ctx, planA))
93+
require.NoError(t, store.CreatePurchasePlan(ctx, planB))
94+
require.NoError(t, store.CreatePurchasePlan(ctx, planC))
95+
96+
t.Cleanup(func() {
97+
conn.Exec(context.Background(), "DELETE FROM purchase_plans WHERE id = ANY($1::uuid[])",
98+
[]string{planA.ID, planB.ID, planC.ID})
99+
})
100+
101+
// ---- Seed plan_accounts ----
102+
insertPlanAccount := func(planID, accountID string) {
103+
t.Helper()
104+
_, err := conn.Exec(ctx, `
105+
INSERT INTO plan_accounts (plan_id, account_id)
106+
VALUES ($1::uuid, $2::uuid)
107+
ON CONFLICT DO NOTHING
108+
`, planID, accountID)
109+
require.NoError(t, err, "seed plan_accounts (%s -> %s)", planID, accountID)
110+
}
111+
insertPlanAccount(planA.ID, accountXID) // planA -> accountX
112+
insertPlanAccount(planC.ID, accountYID) // planC -> accountY only
113+
// planB gets NO plan_accounts row -- that is the legacy case.
114+
115+
// ====================================================================
116+
// Sub-test 1: account-filtered query (filter = [accountX])
117+
// ====================================================================
118+
t.Run("account-filtered result includes unassigned plan and excludes wrong-account plan", func(t *testing.T) {
119+
plans, err := store.ListPurchasePlans(ctx, PurchasePlanFilter{AccountIDs: []string{accountXID}})
120+
require.NoError(t, err)
121+
122+
byID := make(map[string]PurchasePlan, len(plans))
123+
for _, p := range plans {
124+
byID[p.ID] = p
125+
}
126+
127+
// planA must appear, assigned (not unassigned).
128+
if pa, ok := byID[planA.ID]; assert.True(t, ok, "planA (assigned to accountX) must be in result") {
129+
assert.False(t, pa.Unassigned, "planA is assigned; Unassigned must be false")
130+
}
131+
132+
// planB must appear, flagged as unassigned.
133+
// DISCRIMINATING ASSERTION: on pre-fix INNER JOIN this row is absent.
134+
if pb, ok := byID[planB.ID]; assert.True(t, ok,
135+
"planB (zero plan_accounts rows) must be included in filtered result (regression guard for #973 INNER JOIN bug)") {
136+
assert.True(t, pb.Unassigned, "planB has no plan_accounts rows; Unassigned must be true")
137+
}
138+
139+
// planC must NOT appear (it belongs only to accountY).
140+
_, planCPresent := byID[planC.ID]
141+
assert.False(t, planCPresent, "planC (assigned to accountY only) must NOT appear when filtering by accountX")
142+
})
143+
144+
// ====================================================================
145+
// Sub-test 2: no-filter query returns all three plans
146+
// ====================================================================
147+
t.Run("no-filter result returns all plans; assigned plans have Unassigned=false", func(t *testing.T) {
148+
plans, err := store.ListPurchasePlans(ctx, PurchasePlanFilter{})
149+
require.NoError(t, err)
150+
151+
byID := make(map[string]PurchasePlan, len(plans))
152+
for _, p := range plans {
153+
byID[p.ID] = p
154+
}
155+
156+
// All three plans must be present.
157+
assert.Contains(t, byID, planA.ID, "planA must appear in no-filter result")
158+
assert.Contains(t, byID, planB.ID, "planB must appear in no-filter result")
159+
assert.Contains(t, byID, planC.ID, "planC must appear in no-filter result")
160+
161+
// In the no-filter path, all plans are returned with Unassigned=false
162+
// (the query hard-codes `false AS unassigned`).
163+
for _, p := range []PurchasePlan{byID[planA.ID], byID[planB.ID], byID[planC.ID]} {
164+
assert.False(t, p.Unassigned,
165+
"no-filter path hard-codes false AS unassigned; plan %s must have Unassigned=false", p.ID)
166+
}
167+
})
168+
}

0 commit comments

Comments
 (0)