carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "fmt"
5 "net/url"
6 "strings"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/economics"
10)
11
12// econRowsFor builds n synthetic rows named acct-000..acct-(n-1), all
13// "paying", for the pure pagination tests below — pageInstanceRows's own
14// rowsFor helper (listquery_test.go), rewritten for econAccountRow.
15func econRowsFor(n int) []econAccountRow {
16 out := make([]econAccountRow, n)
17 for i := range out {
18 acct := fmt.Sprintf("acct-%03d", i)
19 out[i] = econAccountRow{
20 Account: acct,
21 Meters: map[string]float64{},
22 Econ: economics.AccountEconomics{Account: acct, FreeTierPos: "paying"},
23 }
24 }
25 return out
26}
27
28func TestPageEconAccountsFirstAndSecondPage(t *testing.T) {
29 rows := econRowsFor(120)
30 p1 := pageEconAccounts(rows, econAccountQuery{}, econAccountPageSize)
31 if len(p1.Rows) != 50 || p1.NextCursor == "" {
32 t.Fatalf("page1: got %d rows, cursor %q", len(p1.Rows), p1.NextCursor)
33 }
34 if p1.Rows[0].Account != "acct-000" || p1.Rows[49].Account != "acct-049" {
35 t.Fatalf("page1 bounds: first %q last %q", p1.Rows[0].Account, p1.Rows[49].Account)
36 }
37 if p1.Total != 120 {
38 t.Fatalf("p1.Total = %d, want 120", p1.Total)
39 }
40
41 p2 := pageEconAccounts(rows, econAccountQuery{Cursor: p1.NextCursor}, econAccountPageSize)
42 if len(p2.Rows) != 50 || p2.Rows[0].Account != "acct-050" {
43 t.Fatalf("page2: got %d rows starting %q, want 50 starting acct-050", len(p2.Rows), p2.Rows[0].Account)
44 }
45
46 p3 := pageEconAccounts(rows, econAccountQuery{Cursor: p2.NextCursor}, econAccountPageSize)
47 if len(p3.Rows) != 20 || p3.NextCursor != "" {
48 t.Fatalf("page3: got %d rows, cursor %q, want 20 rows and no next cursor", len(p3.Rows), p3.NextCursor)
49 }
50 if p3.Rows[0].Account != "acct-100" || p3.Rows[19].Account != "acct-119" {
51 t.Fatalf("page3 bounds: first %q last %q", p3.Rows[0].Account, p3.Rows[19].Account)
52 }
53}
54
55func TestPageEconAccountsQFilter(t *testing.T) {
56 rows := econRowsFor(120)
57 // "acct-01" substring-matches acct-010..acct-019 (10 rows).
58 p := pageEconAccounts(rows, econAccountQuery{Q: "acct-01"}, econAccountPageSize)
59 if p.Total != 10 || len(p.Rows) != 10 {
60 t.Fatalf("Q filter: Total=%d Rows=%d, want 10/10", p.Total, len(p.Rows))
61 }
62 for _, r := range p.Rows {
63 if r.Account[:7] != "acct-01" {
64 t.Fatalf("row %q does not match Q filter", r.Account)
65 }
66 }
67}
68
69func TestPageEconAccountsPosFilter(t *testing.T) {
70 rows := econRowsFor(120)
71 rows[0].Econ.FreeTierPos = "inside"
72 rows[1].Econ.FreeTierPos = "inside"
73 rows[2].Econ.FreeTierPos = "partial"
74
75 p := pageEconAccounts(rows, econAccountQuery{Pos: "inside"}, econAccountPageSize)
76 if p.Total != 2 {
77 t.Fatalf("Pos=inside: Total = %d, want 2", p.Total)
78 }
79 for _, r := range p.Rows {
80 if r.Econ.FreeTierPos != "inside" {
81 t.Fatalf("row %q has FreeTierPos %q, want inside", r.Account, r.Econ.FreeTierPos)
82 }
83 }
84
85 // Counts totals every position across ALL rows, filtered or not.
86 if p.Counts["inside"] != 2 || p.Counts["partial"] != 1 || p.Counts["paying"] != 117 {
87 t.Fatalf("Counts = %+v, want inside:2 partial:1 paying:117", p.Counts)
88 }
89
90 var insideLink, payingLink *econPosLink
91 for i := range p.Links {
92 switch p.Links[i].Word {
93 case "inside":
94 insideLink = &p.Links[i]
95 case "paying":
96 payingLink = &p.Links[i]
97 }
98 }
99 if insideLink == nil || !insideLink.Active || insideLink.Count != 2 {
100 t.Fatalf("inside link = %+v, want Active=true Count=2", insideLink)
101 }
102 if payingLink == nil || payingLink.Active || payingLink.Count != 117 {
103 t.Fatalf("paying link = %+v, want Active=false Count=117", payingLink)
104 }
105}
106
107// TestPageEconAccountsPooledFilterAndCount: "pooled" (Task 5's fourth
108// FreeTierPos word, IMPORTANT 2 fix round) must filter and count exactly
109// like the other three positions — pos=pooled is a real, accepted filter,
110// not silently cleared by econPosFromParam, and Counts/Links include it.
111func TestPageEconAccountsPooledFilterAndCount(t *testing.T) {
112 rows := econRowsFor(120)
113 rows[0].Econ.FreeTierPos = "pooled"
114 rows[1].Econ.FreeTierPos = "pooled"
115 rows[2].Econ.FreeTierPos = "partial"
116
117 q := econAccountQuery{Pos: econPosFromParam("pooled")}
118 if q.Pos != "pooled" {
119 t.Fatalf("econPosFromParam(%q) = %q, want %q (pos=pooled must be an accepted filter)", "pooled", q.Pos, "pooled")
120 }
121
122 p := pageEconAccounts(rows, q, econAccountPageSize)
123 if p.Total != 2 {
124 t.Fatalf("Pos=pooled: Total = %d, want 2", p.Total)
125 }
126 for _, r := range p.Rows {
127 if r.Econ.FreeTierPos != "pooled" {
128 t.Fatalf("row %q has FreeTierPos %q, want pooled", r.Account, r.Econ.FreeTierPos)
129 }
130 }
131
132 if p.Counts["pooled"] != 2 || p.Counts["partial"] != 1 || p.Counts["paying"] != 117 {
133 t.Fatalf("Counts = %+v, want pooled:2 partial:1 paying:117", p.Counts)
134 }
135
136 var pooledLink *econPosLink
137 for i := range p.Links {
138 if p.Links[i].Word == "pooled" {
139 pooledLink = &p.Links[i]
140 }
141 }
142 if pooledLink == nil || !pooledLink.Active || pooledLink.Count != 2 {
143 t.Fatalf("pooled link = %+v, want Active=true Count=2", pooledLink)
144 }
145}
146
147// TestPageEconAccountsQMatchesSqidOrNameCaseInsensitive (account-names
148// batch): Q matches EITHER the sqid or the display Name, and the WHOLE match
149// is case-insensitive — a deliberate divergence from the Running tab's Q
150// (listquery.go's instanceQuery.Q, a case-sensitive substring match on
151// host), per Paul's names request implying human-friendly search rather than
152// wire-shaped exactness. econRowsFor's rows carry no Name by default (zero
153// value ""), so a Q that matches nothing's Name still falls through to the
154// sqid half exactly as before.
155func TestPageEconAccountsQMatchesSqidOrNameCaseInsensitive(t *testing.T) {
156 rows := econRowsFor(5)
157 rows[0].Name = "Ops Team"
158 rows[1].Name = "Widgets Inc"
159
160 // Matches by name.
161 p := pageEconAccounts(rows, econAccountQuery{Q: "ops"}, econAccountPageSize)
162 if p.Total != 1 || p.Rows[0].Account != rows[0].Account {
163 t.Fatalf("Q=ops (name match): Total=%d, want 1 matching %q", p.Total, rows[0].Account)
164 }
165
166 // Name match is case-insensitive.
167 p2 := pageEconAccounts(rows, econAccountQuery{Q: "OPS TEAM"}, econAccountPageSize)
168 if p2.Total != 1 || p2.Rows[0].Account != rows[0].Account {
169 t.Fatalf("Q=OPS TEAM (uppercase name match): Total=%d, want 1 matching %q", p2.Total, rows[0].Account)
170 }
171
172 // Sqid still matches, now case-insensitively too.
173 p3 := pageEconAccounts(rows, econAccountQuery{Q: "ACCT-002"}, econAccountPageSize)
174 if p3.Total != 1 || p3.Rows[0].Account != "acct-002" {
175 t.Fatalf("Q=ACCT-002 (uppercase sqid match): Total=%d, want 1 matching acct-002", p3.Total)
176 }
177
178 // A query matching neither sqid nor name: no rows.
179 p4 := pageEconAccounts(rows, econAccountQuery{Q: "nonesuch"}, econAccountPageSize)
180 if p4.Total != 0 {
181 t.Fatalf("Q=nonesuch: Total=%d, want 0", p4.Total)
182 }
183}
184
185func TestPageEconAccountsGarbageCursorStartsOver(t *testing.T) {
186 rows := econRowsFor(5)
187 p := pageEconAccounts(rows, econAccountQuery{Cursor: "!!not-base64!!"}, econAccountPageSize)
188 if len(p.Rows) != 5 {
189 t.Fatalf("bad cursor must degrade to page one: got %d rows", len(p.Rows))
190 }
191}
192
193// TestPageEconAccountsNextURLCarriesPeriod is the fix-round regression for
194// the silent-wrong-data bug: NextURL (and, by the same econLinkURL builder,
195// AllURL/Links[].Href) must carry period="last"/"7d" so a "Next 50" click
196// doesn't resume a cursor computed against one period's row order under
197// another period's numbers — but must OMIT period=mtd, mtd being the
198// implicit default every other link on this page already omits it for.
199func TestPageEconAccountsNextURLCarriesPeriod(t *testing.T) {
200 rows := econRowsFor(120)
201
202 pLast := pageEconAccounts(rows, econAccountQuery{Period: "last"}, econAccountPageSize)
203 if pLast.NextCursor == "" {
204 t.Fatalf("expected a NextCursor over 120 rows at page size %d", econAccountPageSize)
205 }
206 if !strings.Contains(pLast.NextURL, "period=last") {
207 t.Fatalf("NextURL = %q, want it to carry period=last", pLast.NextURL)
208 }
209 if !strings.Contains(pLast.AllURL, "period=last") {
210 t.Fatalf("AllURL = %q, want it to carry period=last", pLast.AllURL)
211 }
212
213 pMTD := pageEconAccounts(rows, econAccountQuery{Period: "mtd"}, econAccountPageSize)
214 if strings.Contains(pMTD.NextURL, "period=") {
215 t.Fatalf("NextURL = %q, want no period param for the default mtd", pMTD.NextURL)
216 }
217 if strings.Contains(pMTD.AllURL, "period=") {
218 t.Fatalf("AllURL = %q, want no period param for the default mtd", pMTD.AllURL)
219 }
220
221 // The zero value (Period: "") behaves identically to "mtd" — omitted.
222 pZero := pageEconAccounts(rows, econAccountQuery{}, econAccountPageSize)
223 if strings.Contains(pZero.NextURL, "period=") {
224 t.Fatalf("NextURL = %q, want no period param when Period is unset", pZero.NextURL)
225 }
226
227 // A pos-filter link (Links[].Href) carries period too — resuming the
228 // SAME bug from a different link on the page would be an incomplete fix.
229 rows[0].Econ.FreeTierPos = "inside"
230 pLastFiltered := pageEconAccounts(rows, econAccountQuery{Period: "last"}, econAccountPageSize)
231 var insideHref string
232 for _, l := range pLastFiltered.Links {
233 if l.Word == "inside" {
234 insideHref = l.Href
235 }
236 }
237 if !strings.Contains(insideHref, "period=last") {
238 t.Fatalf("inside link Href = %q, want it to carry period=last", insideHref)
239 }
240}
241
242// TestEconPeriodURLPreservesQAndPosDropsCursor is the mirror fix: the
243// period-switch nav preserves Q and Pos (an operator's search survives a
244// period switch) but never carries a cursor — econAccountQuery has no
245// Cursor parameter for econPeriodURL to accidentally thread through in the
246// first place, so this pins that shape at the signature level too.
247func TestEconPeriodURLPreservesQAndPosDropsCursor(t *testing.T) {
248 q := econAccountQuery{Q: "acct-01", Pos: "partial", Cursor: "some-cursor-that-must-never-appear"}
249 href := econPeriodURL(q, "last")
250 if !strings.Contains(href, "q=acct-01") {
251 t.Fatalf("href = %q, want it to preserve q=acct-01", href)
252 }
253 if !strings.Contains(href, "pos=partial") {
254 t.Fatalf("href = %q, want it to preserve pos=partial", href)
255 }
256 if !strings.Contains(href, "period=last") {
257 t.Fatalf("href = %q, want it to carry period=last", href)
258 }
259 if strings.Contains(href, "cursor") {
260 t.Fatalf("href = %q, must never carry a cursor across a period switch", href)
261 }
262
263 // Switching TO mtd omits period= (the default), same "omit when mtd" rule.
264 hrefMTD := econPeriodURL(q, "mtd")
265 if strings.Contains(hrefMTD, "period=") {
266 t.Fatalf("href = %q, want no period param when switching to the default mtd", hrefMTD)
267 }
268}
269
270func TestParseEconAccountQuery(t *testing.T) {
271 q := parseEconAccountQuery(url.Values{"q": {" acct-01 "}, "pos": {"inside"}, "cursor": {"abc"}})
272 if q.Q != "acct-01" || q.Pos != "inside" || q.Cursor != "abc" {
273 t.Fatalf("%+v", q)
274 }
275 // An unrecognized pos value is no filter, not an error.
276 q2 := parseEconAccountQuery(url.Values{"pos": {"nonsense"}})
277 if q2.Pos != "" {
278 t.Fatalf("unknown pos = %q, want \"\" (no filter)", q2.Pos)
279 }
280}
281