Skip to content

Commit dbb73de

Browse files
authored
test: increase shared/platform coverage targeting partial hits (#1848)
Cover zero-coverage and partial-coverage functions in shared/platform: - audit/worker_unit_test.go: WithBatchSize, WithPollInterval, WithMaxRetries, WithAdaptivePolling options (all paths incl. inverted ranges), and calculateAdaptiveInterval exponential backoff logic - audit/hooks_test.go: RecordUpdateManual happy path and nil tx error path - audit/metrics_test.go: RecordEmptyPolls - scheduler/metrics_config_test.go: DefaultConfig and all six RecordWorker*/RecordShutdown*/RecordInFlightWork/RecordPoll functions - ratelimit/interceptor_test.go: NewMetrics AlreadyRegisteredError branch, nil registry fallback, and empty namespace fallback - gateway/tenant_resolver_test.go: extractSlugFromRequestOptional local-dev mode paths (valid header slug and invalid header slug) - quantity/value_test.go: ParseQuantity with non-empty invalid dimension (exercises the ValidDimensions map check branch) Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 7e14fbf commit dbb73de

7 files changed

Lines changed: 376 additions & 0 deletions

File tree

shared/platform/audit/hooks_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,51 @@ func TestAuditOutboxTableName(t *testing.T) {
403403
outbox := AuditOutbox{}
404404
assert.Equal(t, "audit_outbox", outbox.TableName())
405405
}
406+
407+
func TestRecordUpdateManual(t *testing.T) {
408+
db := setupHooksTestDB(t)
409+
410+
t.Run("records UPDATE with explicit old and new values", func(t *testing.T) {
411+
oldEntity := testEntity{
412+
ID: uuid.New(),
413+
Name: "Old Name",
414+
Status: "pending",
415+
}
416+
newEntity := testEntity{
417+
ID: oldEntity.ID,
418+
Name: "New Name",
419+
Status: "active",
420+
}
421+
422+
err := db.Transaction(func(tx *gorm.DB) error {
423+
return RecordUpdateManual(tx, oldEntity, newEntity)
424+
})
425+
require.NoError(t, err)
426+
427+
var outbox testAuditOutbox
428+
err = db.Order("created_at DESC").First(&outbox).Error
429+
require.NoError(t, err)
430+
431+
assert.Equal(t, "test_entity", outbox.Table)
432+
assert.Equal(t, OperationUpdate, outbox.Operation)
433+
assert.Equal(t, oldEntity.ID.String(), outbox.RecordID)
434+
assert.NotEmpty(t, outbox.OldValues)
435+
assert.NotEmpty(t, outbox.NewValues)
436+
437+
var oldVals map[string]interface{}
438+
require.NoError(t, json.Unmarshal([]byte(outbox.OldValues), &oldVals))
439+
assert.Equal(t, "Old Name", oldVals["Name"])
440+
441+
var newVals map[string]interface{}
442+
require.NoError(t, json.Unmarshal([]byte(outbox.NewValues), &newVals))
443+
assert.Equal(t, "New Name", newVals["Name"])
444+
})
445+
446+
t.Run("returns error on nil transaction", func(t *testing.T) {
447+
oldEntity := testEntity{ID: uuid.New(), Name: "Old"}
448+
newEntity := testEntity{ID: oldEntity.ID, Name: "New"}
449+
450+
err := RecordUpdateManual[testEntity](nil, oldEntity, newEntity)
451+
assert.ErrorIs(t, err, ErrNilTransaction)
452+
})
453+
}

shared/platform/audit/metrics_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ func TestRecordKafkaMetrics(t *testing.T) {
1111
RecordPollInterval("test_schema", 5.0)
1212
})
1313

14+
t.Run("RecordEmptyPolls", func(_ *testing.T) {
15+
RecordEmptyPolls("test_schema", 0)
16+
RecordEmptyPolls("test_schema", 5)
17+
})
18+
1419
t.Run("RecordKafkaPublished", func(_ *testing.T) {
1520
RecordKafkaPublished("test_schema", "INSERT", "success")
1621
RecordKafkaPublished("test_schema", "UPDATE", "failure")
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package audit
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/meridianhub/meridian/shared/platform/defaults"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// newTestWorker creates a Worker with nil db for unit testing option functions.
12+
// Do not call Start/Stop on this worker.
13+
func newTestWorker() *Worker {
14+
return &Worker{
15+
batchSize: defaultBatchSize,
16+
pollInterval: defaultPollInterval,
17+
maxRetries: defaultMaxRetries,
18+
minPollInterval: defaultMinPollInterval,
19+
maxPollInterval: defaultMaxPollInterval,
20+
shutdown: make(chan struct{}),
21+
}
22+
}
23+
24+
func TestWithBatchSize(t *testing.T) {
25+
t.Run("positive value is applied", func(t *testing.T) {
26+
w := newTestWorker()
27+
WithBatchSize(50)(w)
28+
assert.Equal(t, 50, w.batchSize)
29+
})
30+
31+
t.Run("zero value is ignored", func(t *testing.T) {
32+
w := newTestWorker()
33+
WithBatchSize(0)(w)
34+
assert.Equal(t, defaultBatchSize, w.batchSize)
35+
})
36+
37+
t.Run("negative value is ignored", func(t *testing.T) {
38+
w := newTestWorker()
39+
WithBatchSize(-10)(w)
40+
assert.Equal(t, defaultBatchSize, w.batchSize)
41+
})
42+
}
43+
44+
func TestWithPollInterval(t *testing.T) {
45+
t.Run("positive duration is applied", func(t *testing.T) {
46+
w := newTestWorker()
47+
WithPollInterval(10 * time.Second)(w)
48+
assert.Equal(t, 10*time.Second, w.pollInterval)
49+
})
50+
51+
t.Run("zero duration is ignored", func(t *testing.T) {
52+
w := newTestWorker()
53+
WithPollInterval(0)(w)
54+
assert.Equal(t, defaultPollInterval, w.pollInterval)
55+
})
56+
57+
t.Run("negative duration is ignored", func(t *testing.T) {
58+
w := newTestWorker()
59+
WithPollInterval(-1 * time.Second)(w)
60+
assert.Equal(t, defaultPollInterval, w.pollInterval)
61+
})
62+
}
63+
64+
func TestWithMaxRetries(t *testing.T) {
65+
t.Run("positive value is applied", func(t *testing.T) {
66+
w := newTestWorker()
67+
WithMaxRetries(5)(w)
68+
assert.Equal(t, 5, w.maxRetries)
69+
})
70+
71+
t.Run("zero is accepted", func(t *testing.T) {
72+
w := newTestWorker()
73+
WithMaxRetries(0)(w)
74+
assert.Equal(t, 0, w.maxRetries)
75+
})
76+
77+
t.Run("negative value is ignored", func(t *testing.T) {
78+
w := newTestWorker()
79+
WithMaxRetries(-1)(w)
80+
assert.Equal(t, defaultMaxRetries, w.maxRetries)
81+
})
82+
}
83+
84+
func TestWithAdaptivePolling(t *testing.T) {
85+
t.Run("valid min and max are applied", func(t *testing.T) {
86+
w := newTestWorker()
87+
WithAdaptivePolling(100*time.Millisecond, 30*time.Second)(w)
88+
assert.True(t, w.adaptivePolling)
89+
assert.Equal(t, 100*time.Millisecond, w.minPollInterval)
90+
assert.Equal(t, 30*time.Second, w.maxPollInterval)
91+
})
92+
93+
t.Run("inverted min/max are swapped", func(t *testing.T) {
94+
w := newTestWorker()
95+
WithAdaptivePolling(30*time.Second, 100*time.Millisecond)(w)
96+
assert.True(t, w.adaptivePolling)
97+
assert.Equal(t, 100*time.Millisecond, w.minPollInterval)
98+
assert.Equal(t, 30*time.Second, w.maxPollInterval)
99+
})
100+
101+
t.Run("zero min interval is ignored", func(t *testing.T) {
102+
w := newTestWorker()
103+
WithAdaptivePolling(0, 30*time.Second)(w)
104+
assert.True(t, w.adaptivePolling)
105+
assert.Equal(t, defaultMinPollInterval, w.minPollInterval)
106+
assert.Equal(t, 30*time.Second, w.maxPollInterval)
107+
})
108+
109+
t.Run("zero max interval is ignored", func(t *testing.T) {
110+
w := newTestWorker()
111+
WithAdaptivePolling(100*time.Millisecond, 0)(w)
112+
assert.True(t, w.adaptivePolling)
113+
assert.Equal(t, 100*time.Millisecond, w.minPollInterval)
114+
assert.Equal(t, defaultMaxPollInterval, w.maxPollInterval)
115+
})
116+
}
117+
118+
func TestCalculateAdaptiveInterval(t *testing.T) {
119+
min := defaults.DefaultRetryDelay // 100ms
120+
max := defaults.DefaultRPCTimeout // 30s
121+
122+
t.Run("work found resets to min interval", func(t *testing.T) {
123+
w := newTestWorker()
124+
w.minPollInterval = min
125+
w.maxPollInterval = max
126+
w.emptyPollCount = 5 // simulate previous idle state
127+
128+
interval := w.calculateAdaptiveInterval(10)
129+
assert.Equal(t, min, interval)
130+
assert.Equal(t, 0, w.emptyPollCount)
131+
})
132+
133+
t.Run("no work increases interval exponentially", func(t *testing.T) {
134+
w := newTestWorker()
135+
w.minPollInterval = min
136+
w.maxPollInterval = max
137+
138+
// First empty poll: emptyPollCount becomes 1, multiplier = 2^1 = 2
139+
interval := w.calculateAdaptiveInterval(0)
140+
assert.Equal(t, 1, w.emptyPollCount)
141+
assert.Equal(t, 2*min, interval)
142+
143+
// Second empty poll: emptyPollCount becomes 2, multiplier = 2^2 = 4
144+
interval = w.calculateAdaptiveInterval(0)
145+
assert.Equal(t, 2, w.emptyPollCount)
146+
assert.Equal(t, 4*min, interval)
147+
})
148+
149+
t.Run("interval is capped at max", func(t *testing.T) {
150+
w := newTestWorker()
151+
w.minPollInterval = min
152+
w.maxPollInterval = 200 * time.Millisecond // very small max
153+
w.emptyPollCount = 20 // large count to exceed max
154+
155+
interval := w.calculateAdaptiveInterval(0)
156+
assert.Equal(t, 200*time.Millisecond, interval)
157+
})
158+
}

shared/platform/gateway/tenant_resolver_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,80 @@ func TestHandlerOptionalTenant_TenantNotFoundInDB(t *testing.T) {
14171417
mockRepo.AssertExpectations(t)
14181418
}
14191419

1420+
func TestHandlerOptionalTenant_LocalDevMode_ValidSlugFromHeader(t *testing.T) {
1421+
ctx := context.Background()
1422+
baseDomain := "api.meridian.io"
1423+
testSlug := "acme"
1424+
testTenantID := tenant.MustNewTenantID("tenant_abc")
1425+
logger := slog.Default()
1426+
1427+
mockCache := new(MockSlugCache)
1428+
mockRepo := new(MockTenantRepository)
1429+
1430+
mockCache.On("Get", ctx, testSlug).Return(testTenantID, nil)
1431+
1432+
middleware := &TenantResolverMiddleware{
1433+
slugCache: mockCache,
1434+
tenantRepo: mockRepo,
1435+
baseDomain: baseDomain,
1436+
logger: logger,
1437+
localDevMode: true,
1438+
}
1439+
1440+
req := httptest.NewRequest(http.MethodGet, "http://"+baseDomain+"/api/test", nil)
1441+
req.Header.Set(TenantSlugHeader, testSlug)
1442+
req = req.WithContext(ctx)
1443+
rec := httptest.NewRecorder()
1444+
1445+
var capturedTenantOk bool
1446+
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1447+
_, capturedTenantOk = tenant.FromContext(r.Context())
1448+
w.WriteHeader(http.StatusOK)
1449+
})
1450+
1451+
handler := middleware.HandlerOptionalTenant(next)
1452+
handler.ServeHTTP(rec, req)
1453+
1454+
assert.Equal(t, http.StatusOK, rec.Code)
1455+
assert.True(t, capturedTenantOk, "tenant should be resolved from header slug in local dev mode")
1456+
mockCache.AssertExpectations(t)
1457+
}
1458+
1459+
func TestHandlerOptionalTenant_LocalDevMode_InvalidSlugFromHeader(t *testing.T) {
1460+
ctx := context.Background()
1461+
baseDomain := "api.meridian.io"
1462+
logger := slog.Default()
1463+
1464+
mockCache := new(MockSlugCache)
1465+
mockRepo := new(MockTenantRepository)
1466+
1467+
middleware := &TenantResolverMiddleware{
1468+
slugCache: mockCache,
1469+
tenantRepo: mockRepo,
1470+
baseDomain: baseDomain,
1471+
logger: logger,
1472+
localDevMode: true,
1473+
}
1474+
1475+
req := httptest.NewRequest(http.MethodGet, "http://"+baseDomain+"/api/test", nil)
1476+
req.Header.Set(TenantSlugHeader, "INVALID SLUG!!!")
1477+
req = req.WithContext(ctx)
1478+
rec := httptest.NewRecorder()
1479+
1480+
var capturedTenantOk bool
1481+
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1482+
_, capturedTenantOk = tenant.FromContext(r.Context())
1483+
w.WriteHeader(http.StatusOK)
1484+
})
1485+
1486+
handler := middleware.HandlerOptionalTenant(next)
1487+
handler.ServeHTTP(rec, req)
1488+
1489+
// Invalid slug: falls through to subdomain extraction, which also returns empty, so no tenant
1490+
assert.Equal(t, http.StatusOK, rec.Code)
1491+
assert.False(t, capturedTenantOk, "no tenant should be resolved with invalid slug")
1492+
}
1493+
14201494
func TestPlatformPathsDoesNotIncludeDex(t *testing.T) {
14211495
assert.False(t, IsPlatformPath("/dex/auth"), "/dex/auth should not be a platform path")
14221496
assert.False(t, IsPlatformPath("/dex/callback"), "/dex/callback should not be a platform path")

shared/platform/quantity/value_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,18 @@ func TestValue_HighPrecision(t *testing.T) {
607607
require.True(t, ok)
608608
assert.Equal(t, "1.123456789012345678", result.Amount.String())
609609
}
610+
611+
func TestParseQuantity_InvalidNonEmptyDimension(t *testing.T) {
612+
// Create an instrument with a non-empty dimension that is not in ValidDimensions.
613+
// This bypasses the empty-string case and exercises the ValidDimensions check.
614+
invalidInst := quantity.Instrument{
615+
Code: "BOGUS",
616+
Version: 1,
617+
Dimension: "NOT_A_VALID_DIMENSION",
618+
Precision: 2,
619+
}
620+
621+
_, err := quantity.ParseQuantity(decimal.NewFromInt(42), invalidInst)
622+
require.Error(t, err)
623+
assert.ErrorIs(t, err, quantity.ErrUnknownDimension)
624+
}

shared/platform/ratelimit/interceptor_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,32 @@ func TestInterceptor_NilMetrics(t *testing.T) {
322322
require.Error(t, err)
323323
assert.Equal(t, codes.ResourceExhausted, status.Code(err))
324324
}
325+
326+
func TestNewMetrics_AlreadyRegistered(t *testing.T) {
327+
// Register metrics once
328+
registry := testRegistry()
329+
m1 := NewMetrics("dedupe_test", registry)
330+
assert.NotNil(t, m1)
331+
332+
// Registering again with the same registry should not panic
333+
// (AlreadyRegisteredError is handled gracefully)
334+
assert.NotPanics(t, func() {
335+
m2 := NewMetrics("dedupe_test", registry)
336+
assert.NotNil(t, m2)
337+
})
338+
}
339+
340+
func TestNewMetrics_NilRegistry(t *testing.T) {
341+
// nil registry falls back to prometheus.DefaultRegisterer
342+
// This should not panic
343+
assert.NotPanics(t, func() {
344+
_ = NewMetrics("nil_reg_test", nil)
345+
})
346+
}
347+
348+
func TestNewMetrics_EmptyNamespace(t *testing.T) {
349+
registry := testRegistry()
350+
// Empty namespace falls back to "grpc"
351+
m := NewMetrics("", registry)
352+
assert.NotNil(t, m)
353+
}

0 commit comments

Comments
 (0)