tests: migrate assertions to testify's assert/require
Replaces raw t.Error/t.Errorf/t.Fatal/t.Fatalf across every test file that has any (domains/accounts, domains/authentication, domains/raw_events, domains/amazon, domains/reports x2) with testify's assert (non-halting) / require (halting) equivalents. The three Example-based tests (server/ui/svg, server/ui/charts) have no *testing.T at all - nothing to convert there. require.Eventually replaces several hand-rolled polling loops in domains/amazon/mock_test.go. Its condition function runs on a separate goroutine (confirmed in testify's source), so calling require.* from inside one - which two of the new Eventually calls initially did, via the isProcessed helper - is unsafe per Go's testing rules (t.FailNow must only be called from the test's own goroutine). Fixed by splitting a *testing.T-free queryIsProcessed(ctx, pool, shopID, eventID) out of isProcessed for use inside those closures specifically. github.com/stretchr/testify promoted from an indirect to a direct dependency (go.mod only - it was already present transitively, so go.sum is unchanged). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
This commit is contained in:
@@ -2,9 +2,11 @@ package accounts_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"ruben/inventory2/consts"
|
||||
"ruben/inventory2/domains/accounts"
|
||||
"ruben/inventory2/internal/testdb"
|
||||
@@ -21,30 +23,18 @@ func TestCreateAccount(t *testing.T) {
|
||||
email := userID + "@example.com"
|
||||
|
||||
acct, err := store.CreateAccount(ctx, userID, email)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAccount() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "CreateAccount()")
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), "DELETE FROM accounts WHERE account_id = $1", acct.AccountID)
|
||||
})
|
||||
|
||||
if acct.AccountID == 0 {
|
||||
t.Error("CreateAccount() returned a zero AccountID")
|
||||
}
|
||||
if acct.UserID != userID {
|
||||
t.Errorf("CreateAccount() UserID = %q, want %q", acct.UserID, userID)
|
||||
}
|
||||
if acct.Email != email {
|
||||
t.Errorf("CreateAccount() Email = %q, want %q", acct.Email, email)
|
||||
}
|
||||
assert.NotZero(t, acct.AccountID, "CreateAccount() returned a zero AccountID")
|
||||
assert.Equal(t, userID, acct.UserID, "CreateAccount() UserID")
|
||||
assert.Equal(t, email, acct.Email, "CreateAccount() Email")
|
||||
|
||||
got, err := store.GetAccount(ctx, acct.AccountID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccount() error = %v", err)
|
||||
}
|
||||
if got != acct {
|
||||
t.Errorf("GetAccount() = %+v, want %+v", got, acct)
|
||||
}
|
||||
require.NoError(t, err, "GetAccount()")
|
||||
assert.Equal(t, acct, got, "GetAccount()")
|
||||
}
|
||||
|
||||
func TestCreateAccount_DuplicateUserIsConflict(t *testing.T) {
|
||||
@@ -56,17 +46,13 @@ func TestCreateAccount_DuplicateUserIsConflict(t *testing.T) {
|
||||
testdb.SeedOAuthUser(t, pool, userID)
|
||||
|
||||
acct, err := store.CreateAccount(ctx, userID, userID+"@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("first CreateAccount() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "first CreateAccount()")
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), "DELETE FROM accounts WHERE account_id = $1", acct.AccountID)
|
||||
})
|
||||
|
||||
_, err = store.CreateAccount(ctx, userID, userID+"-other@example.com")
|
||||
if !errors.Is(err, consts.ErrConflict) {
|
||||
t.Fatalf("second CreateAccount() error = %v, want %v", err, consts.ErrConflict)
|
||||
}
|
||||
require.ErrorIs(t, err, consts.ErrConflict, "second CreateAccount()")
|
||||
}
|
||||
|
||||
func TestGetAccount_NotFound(t *testing.T) {
|
||||
@@ -75,9 +61,7 @@ func TestGetAccount_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := store.GetAccount(ctx, -1)
|
||||
if !errors.Is(err, consts.ErrNotFound) {
|
||||
t.Fatalf("GetAccount() error = %v, want %v", err, consts.ErrNotFound)
|
||||
}
|
||||
require.ErrorIs(t, err, consts.ErrNotFound, "GetAccount()")
|
||||
}
|
||||
|
||||
func TestGetUserAndAccountByAccessToken(t *testing.T) {
|
||||
@@ -90,34 +74,22 @@ func TestGetUserAndAccountByAccessToken(t *testing.T) {
|
||||
|
||||
// before an account exists: user resolves, account does not.
|
||||
user, acct, err := store.GetUserAndAccountByAccessToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserAndAccountByAccessToken() before account creation: error = %v", err)
|
||||
}
|
||||
if user.UserID != userID {
|
||||
t.Errorf("GetUserAndAccountByAccessToken() UserID = %q, want %q", user.UserID, userID)
|
||||
}
|
||||
if acct != nil {
|
||||
t.Errorf("GetUserAndAccountByAccessToken() Account = %+v, want nil before an account is created", acct)
|
||||
}
|
||||
require.NoError(t, err, "GetUserAndAccountByAccessToken() before account creation")
|
||||
assert.Equal(t, userID, user.UserID, "GetUserAndAccountByAccessToken() UserID")
|
||||
assert.Nil(t, acct, "GetUserAndAccountByAccessToken() Account should be nil before an account is created")
|
||||
|
||||
created, err := store.CreateAccount(ctx, userID, userID+"@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAccount() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "CreateAccount()")
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), "DELETE FROM accounts WHERE account_id = $1", created.AccountID)
|
||||
})
|
||||
|
||||
// after an account exists: both resolve.
|
||||
user, acct, err = store.GetUserAndAccountByAccessToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserAndAccountByAccessToken() after account creation: error = %v", err)
|
||||
}
|
||||
if user.UserID != userID {
|
||||
t.Errorf("GetUserAndAccountByAccessToken() UserID = %q, want %q", user.UserID, userID)
|
||||
}
|
||||
if acct == nil || acct.AccountID != created.AccountID {
|
||||
t.Errorf("GetUserAndAccountByAccessToken() Account = %+v, want AccountID %d", acct, created.AccountID)
|
||||
require.NoError(t, err, "GetUserAndAccountByAccessToken() after account creation")
|
||||
assert.Equal(t, userID, user.UserID, "GetUserAndAccountByAccessToken() UserID")
|
||||
if assert.NotNil(t, acct, "GetUserAndAccountByAccessToken() Account should be set after an account is created") {
|
||||
assert.Equal(t, created.AccountID, acct.AccountID, "GetUserAndAccountByAccessToken() Account.AccountID")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +99,5 @@ func TestGetUserAndAccountByAccessToken_UnknownToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, _, err := store.GetUserAndAccountByAccessToken(ctx, "no-such-token-"+testdb.NewUserID(t))
|
||||
if !errors.Is(err, consts.ErrNotFound) {
|
||||
t.Fatalf("GetUserAndAccountByAccessToken() error = %v, want %v", err, consts.ErrNotFound)
|
||||
}
|
||||
require.ErrorIs(t, err, consts.ErrNotFound, "GetUserAndAccountByAccessToken()")
|
||||
}
|
||||
|
||||
+99
-152
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"ruben/inventory2/domains/raw_events"
|
||||
"ruben/inventory2/internal/testdb"
|
||||
@@ -70,9 +72,15 @@ func (s *notifySpy) ackAt(t *testing.T, i int) {
|
||||
s.mu.Lock()
|
||||
ack := s.acks[i]
|
||||
s.mu.Unlock()
|
||||
if err := ack(context.Background()); err != nil {
|
||||
t.Fatalf("ack() error = %v", err)
|
||||
}
|
||||
require.NoError(t, ack(context.Background()), "ack()")
|
||||
}
|
||||
|
||||
// completedCountSnapshot is safe to call from another goroutine (e.g. from
|
||||
// inside require.Eventually's condition), unlike helpers that call t.Fatal.
|
||||
func (s *notifySpy) completedCountSnapshot() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.completedCount
|
||||
}
|
||||
|
||||
// waitForCount blocks until at least n Notify calls have fully completed
|
||||
@@ -80,20 +88,9 @@ func (s *notifySpy) ackAt(t *testing.T, i int) {
|
||||
// more than once with increasing n.
|
||||
func (s *notifySpy) waitForCount(t *testing.T, n int, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
s.mu.Lock()
|
||||
got := s.completedCount
|
||||
s.mu.Unlock()
|
||||
|
||||
if got >= n {
|
||||
return
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("timed out after %v waiting for %d total completed notifications (got %d so far)", timeout, n, got)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
return s.completedCountSnapshot() >= n
|
||||
}, timeout, 5*time.Millisecond, "timed out waiting for %d total completed notifications (got %d)", n, s.completedCountSnapshot())
|
||||
}
|
||||
|
||||
// insertRawAmazonEvent inserts directly into mock.raw_shop_events, the same
|
||||
@@ -109,9 +106,7 @@ func insertRawAmazonEvent(t *testing.T, pool *pgxpool.Pool, shopID, eventID stri
|
||||
INSERT INTO mock.raw_shop_events (platform, shop_id, event_timestamp, event_id, raw_payload)
|
||||
VALUES ('amazon', $1, NOW(), $2, '{}'::jsonb)
|
||||
`, shopID, eventID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert raw amazon event: %v", err)
|
||||
}
|
||||
require.NoError(t, err, "insert raw amazon event")
|
||||
}
|
||||
|
||||
// insertRawAmazonEvents bulk-inserts n events for shopID in one statement,
|
||||
@@ -123,22 +118,27 @@ func insertRawAmazonEvents(t *testing.T, pool *pgxpool.Pool, shopID string, n in
|
||||
SELECT 'amazon', $1, NOW() + (s || ' milliseconds')::interval, 'evt-' || s, '{}'::jsonb
|
||||
FROM generate_series(1, $2) AS s
|
||||
`, shopID, n)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert %d raw amazon events: %v", n, err)
|
||||
}
|
||||
require.NoError(t, err, "insert %d raw amazon events", n)
|
||||
}
|
||||
|
||||
func isProcessed(t *testing.T, pool *pgxpool.Pool, shopID, eventID string) bool {
|
||||
t.Helper()
|
||||
// queryIsProcessed has no *testing.T dependency, so it's safe to call from
|
||||
// require.Eventually's condition function (which testify runs on a
|
||||
// separate goroutine - t.Fatal/require must only be called from the main
|
||||
// test goroutine). isProcessed wraps it for direct, main-goroutine use.
|
||||
func queryIsProcessed(ctx context.Context, pool *pgxpool.Pool, shopID, eventID string) (bool, error) {
|
||||
var processed bool
|
||||
err := pool.QueryRow(context.Background(), `
|
||||
err := pool.QueryRow(ctx, `
|
||||
SELECT processed_at IS NOT NULL
|
||||
FROM mock.shop_amazon_events
|
||||
WHERE shop_id = $1 AND event_id = $2
|
||||
`, shopID, eventID).Scan(&processed)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check processed state: %v", err)
|
||||
}
|
||||
return processed, err
|
||||
}
|
||||
|
||||
func isProcessed(t *testing.T, pool *pgxpool.Pool, shopID, eventID string) bool {
|
||||
t.Helper()
|
||||
processed, err := queryIsProcessed(context.Background(), pool, shopID, eventID)
|
||||
require.NoError(t, err, "check processed state")
|
||||
return processed
|
||||
}
|
||||
|
||||
@@ -152,9 +152,7 @@ func isNotified(t *testing.T, pool *pgxpool.Pool, shopID, eventID string) bool {
|
||||
FROM mock.shop_amazon_events
|
||||
WHERE shop_id = $1 AND event_id = $2
|
||||
`, shopID, eventID).Scan(¬ified)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check notified state: %v", err)
|
||||
}
|
||||
require.NoError(t, err, "check notified state")
|
||||
return notified
|
||||
}
|
||||
|
||||
@@ -164,9 +162,7 @@ func countUnprocessed(t *testing.T, pool *pgxpool.Pool, shopID string) int {
|
||||
err := pool.QueryRow(context.Background(), `
|
||||
SELECT count(*) FROM mock.shop_amazon_events WHERE shop_id = $1 AND processed_at IS NULL
|
||||
`, shopID).Scan(&n)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to count unprocessed events: %v", err)
|
||||
}
|
||||
require.NoError(t, err, "count unprocessed events")
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -197,12 +193,33 @@ func terminateListenConnection(t *testing.T, pool *pgxpool.Pool) {
|
||||
ORDER BY backend_start DESC
|
||||
LIMIT 1
|
||||
`, eventChannelName).Scan(&pid)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to find the LISTEN connection's backend pid: %v", err)
|
||||
}
|
||||
require.NoError(t, err, "find the LISTEN connection's backend pid")
|
||||
|
||||
if _, err := pool.Exec(ctx, `SELECT pg_terminate_backend($1)`, pid); err != nil {
|
||||
t.Fatalf("failed to terminate backend %d: %v", pid, err)
|
||||
_, err = pool.Exec(ctx, `SELECT pg_terminate_backend($1)`, pid)
|
||||
require.NoError(t, err, "terminate backend %d", pid)
|
||||
}
|
||||
|
||||
// waitReady blocks until m signals it's actively listening, failing the
|
||||
// test if that doesn't happen within timeout.
|
||||
func waitReady(t *testing.T, m *Mocks, timeout time.Duration) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-m.Ready():
|
||||
case <-time.After(timeout):
|
||||
require.Fail(t, "ProcessEvents() did not become ready (LISTEN registered)", "within %v", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// waitStopped blocks until ProcessEvents returns on errCh, asserting it
|
||||
// returns a nil error, failing the test if that doesn't happen within
|
||||
// timeout.
|
||||
func waitStopped(t *testing.T, errCh <-chan error, timeout time.Duration) {
|
||||
t.Helper()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.NoError(t, err, "ProcessEvents() after context cancellation")
|
||||
case <-time.After(timeout):
|
||||
require.Fail(t, "ProcessEvents() did not return", "within %v of context cancellation", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,15 +235,12 @@ func TestProcessUnprocessedEvents_ProcessesAllEventsAcrossBatches(t *testing.T)
|
||||
const n = 150 // exceeds the 100-row LIMIT per batch inside processUnprocessedEvents
|
||||
insertRawAmazonEvents(t, pool, shopID, n)
|
||||
|
||||
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||
t.Fatalf("processUnprocessedEvents() error = %v", err)
|
||||
}
|
||||
require.NoError(t, m.processUnprocessedEvents(ctx), "processUnprocessedEvents()")
|
||||
|
||||
spy.waitForCount(t, n, 5*time.Second)
|
||||
|
||||
if got := countUnprocessed(t, pool, shopID); got != 0 {
|
||||
t.Errorf("countUnprocessed() = %d, want 0 (all %d events should be processed across multiple 100-row batches)", got, n)
|
||||
}
|
||||
assert.Zero(t, countUnprocessed(t, pool, shopID),
|
||||
"countUnprocessed() should be 0 (all %d events should be processed across multiple 100-row batches)", n)
|
||||
}
|
||||
|
||||
func TestProcessUnprocessedEvents_NoListenerConfigured(t *testing.T) {
|
||||
@@ -239,18 +253,14 @@ func TestProcessUnprocessedEvents_NoListenerConfigured(t *testing.T) {
|
||||
|
||||
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
||||
|
||||
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||
t.Fatalf("processUnprocessedEvents() error = %v", err)
|
||||
}
|
||||
require.NoError(t, m.processUnprocessedEvents(ctx), "processUnprocessedEvents()")
|
||||
|
||||
// with no listener, nothing ever acks - the event is expected to stay
|
||||
// "notified" forever, not silently marked processed.
|
||||
if isProcessed(t, pool, shopID, "evt-1") {
|
||||
t.Error("event was marked processed despite no listener being configured to ack it")
|
||||
}
|
||||
if !isNotified(t, pool, shopID, "evt-1") {
|
||||
t.Error("event should be in the notified state after being handed off with no listener to ack it")
|
||||
}
|
||||
assert.False(t, isProcessed(t, pool, shopID, "evt-1"),
|
||||
"event was marked processed despite no listener being configured to ack it")
|
||||
assert.True(t, isNotified(t, pool, shopID, "evt-1"),
|
||||
"event should be in the notified state after being handed off with no listener to ack it")
|
||||
}
|
||||
|
||||
// TestProcessUnprocessedEvents_RetriesUnackedNotification is the core of
|
||||
@@ -271,47 +281,29 @@ func TestProcessUnprocessedEvents_RetriesUnackedNotification(t *testing.T) {
|
||||
|
||||
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
||||
|
||||
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||
t.Fatalf("processUnprocessedEvents() (1st pass) error = %v", err)
|
||||
}
|
||||
require.NoError(t, m.processUnprocessedEvents(ctx), "processUnprocessedEvents() (1st pass)")
|
||||
spy.waitForCount(t, 1, 2*time.Second)
|
||||
|
||||
if isProcessed(t, pool, shopID, "evt-1") {
|
||||
t.Fatal("event was marked processed despite the listener never acking")
|
||||
}
|
||||
if !isNotified(t, pool, shopID, "evt-1") {
|
||||
t.Fatal("event should be in the notified state after the first dispatch")
|
||||
}
|
||||
require.False(t, isProcessed(t, pool, shopID, "evt-1"), "event was marked processed despite the listener never acking")
|
||||
require.True(t, isNotified(t, pool, shopID, "evt-1"), "event should be in the notified state after the first dispatch")
|
||||
|
||||
// still within notifyRetryAfter: shouldn't be re-notified yet.
|
||||
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||
t.Fatalf("processUnprocessedEvents() (immediate 2nd pass) error = %v", err)
|
||||
}
|
||||
if got := len(spy.eventsSnapshot()); got != 1 {
|
||||
t.Fatalf("listener was notified %d times before notifyRetryAfter elapsed, want 1", got)
|
||||
}
|
||||
require.NoError(t, m.processUnprocessedEvents(ctx), "processUnprocessedEvents() (immediate 2nd pass)")
|
||||
require.Len(t, spy.eventsSnapshot(), 1, "listener should not have been notified again before notifyRetryAfter elapsed")
|
||||
|
||||
time.Sleep(60 * time.Millisecond) // past notifyRetryAfter
|
||||
|
||||
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||
t.Fatalf("processUnprocessedEvents() (3rd pass, after retry window) error = %v", err)
|
||||
}
|
||||
require.NoError(t, m.processUnprocessedEvents(ctx), "processUnprocessedEvents() (3rd pass, after retry window)")
|
||||
spy.waitForCount(t, 2, 2*time.Second)
|
||||
|
||||
// the listener "finishes" the first notification late, via the ack it
|
||||
// was originally handed - not a fresh one from the retry.
|
||||
spy.ackAt(t, 0)
|
||||
|
||||
if !isProcessed(t, pool, shopID, "evt-1") {
|
||||
t.Fatal("event should be processed once any recorded ack for it is called")
|
||||
}
|
||||
require.True(t, isProcessed(t, pool, shopID, "evt-1"), "event should be processed once any recorded ack for it is called")
|
||||
|
||||
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||
t.Fatalf("processUnprocessedEvents() (4th pass, after ack) error = %v", err)
|
||||
}
|
||||
if got := len(spy.eventsSnapshot()); got != 2 {
|
||||
t.Fatalf("listener was notified again after being acked: got %d calls, want 2", got)
|
||||
}
|
||||
require.NoError(t, m.processUnprocessedEvents(ctx), "processUnprocessedEvents() (4th pass, after ack)")
|
||||
assert.Len(t, spy.eventsSnapshot(), 2, "listener should not be notified again after being acked")
|
||||
}
|
||||
|
||||
// TestProcessEvents_PollFallbackPicksUpRetryDueEvents proves the poll
|
||||
@@ -341,11 +333,7 @@ func TestProcessEvents_PollFallbackPicksUpRetryDueEvents(t *testing.T) {
|
||||
errCh <- m.ProcessEvents(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-m.Ready():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not become ready (LISTEN registered) within 5s")
|
||||
}
|
||||
waitReady(t, m, 5*time.Second)
|
||||
|
||||
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
||||
|
||||
@@ -358,14 +346,7 @@ func TestProcessEvents_PollFallbackPicksUpRetryDueEvents(t *testing.T) {
|
||||
spy.waitForCount(t, 2, 3*time.Second)
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessEvents() returned error = %v after context cancellation, want nil", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not return within 5s of context cancellation")
|
||||
}
|
||||
waitStopped(t, errCh, 5*time.Second)
|
||||
}
|
||||
|
||||
// TestProcessEvents_ReactsToNotification drives the actual long-running
|
||||
@@ -389,36 +370,24 @@ func TestProcessEvents_ReactsToNotification(t *testing.T) {
|
||||
errCh <- m.ProcessEvents(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-m.Ready():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not become ready (LISTEN registered) within 5s")
|
||||
}
|
||||
waitReady(t, m, 5*time.Second)
|
||||
|
||||
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for !isProcessed(t, pool, shopID, "evt-1") {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("event was not processed within 5s of insertion - the reactive LISTEN/NOTIFY wake-up did not fire (the 1-minute poll fallback would eventually catch it, but this test intentionally doesn't wait that long)")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
processed, _ := queryIsProcessed(context.Background(), pool, shopID, "evt-1")
|
||||
return processed
|
||||
}, 5*time.Second, 20*time.Millisecond,
|
||||
"event was not processed within 5s of insertion - the reactive LISTEN/NOTIFY wake-up did not fire "+
|
||||
"(the 1-minute poll fallback would eventually catch it, but this test intentionally doesn't wait that long)")
|
||||
|
||||
spy.waitForCount(t, 1, 2*time.Second)
|
||||
if got := spy.eventsSnapshot()[0]; got.StoreID != shopID || got.EventID != "evt-1" {
|
||||
t.Errorf("listener notified with %+v, want StoreID=%q EventID=%q", got, shopID, "evt-1")
|
||||
}
|
||||
got := spy.eventsSnapshot()[0]
|
||||
assert.Equal(t, shopID, got.StoreID, "listener notified with unexpected StoreID")
|
||||
assert.Equal(t, "evt-1", got.EventID, "listener notified with unexpected EventID")
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessEvents() returned error = %v after context cancellation, want nil", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not return within 5s of context cancellation")
|
||||
}
|
||||
waitStopped(t, errCh, 5*time.Second)
|
||||
}
|
||||
|
||||
// TestProcessEvents_ShutsDownOnContextCancel checks the lifecycle in
|
||||
@@ -436,21 +405,10 @@ func TestProcessEvents_ShutsDownOnContextCancel(t *testing.T) {
|
||||
errCh <- m.ProcessEvents(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-m.Ready():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not become ready (LISTEN registered) within 5s")
|
||||
}
|
||||
waitReady(t, m, 5*time.Second)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessEvents() returned error = %v after context cancellation, want nil", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not return within 5s of context cancellation")
|
||||
}
|
||||
waitStopped(t, errCh, 5*time.Second)
|
||||
}
|
||||
|
||||
// TestProcessEvents_ReconnectsAfterListenConnectionDrops is insight #4's
|
||||
@@ -474,11 +432,7 @@ func TestProcessEvents_ReconnectsAfterListenConnectionDrops(t *testing.T) {
|
||||
errCh <- m.ProcessEvents(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-m.Ready():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not become ready (LISTEN registered) within 5s")
|
||||
}
|
||||
waitReady(t, m, 5*time.Second)
|
||||
|
||||
terminateListenConnection(t, pool)
|
||||
|
||||
@@ -486,27 +440,20 @@ func TestProcessEvents_ReconnectsAfterListenConnectionDrops(t *testing.T) {
|
||||
// backoff is 1s) - it must not have returned because of this.
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Fatalf("ProcessEvents() returned (err = %v) after its LISTEN connection was killed, want it to reconnect and keep running", err)
|
||||
require.Fail(t, "ProcessEvents() returned after its LISTEN connection was killed, want it to reconnect and keep running",
|
||||
"err = %v", err)
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for !isProcessed(t, pool, shopID, "evt-1") {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("event was not processed within 5s of insertion after the LISTEN connection was forcibly dropped - reconnection did not restore the reactive path")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
processed, _ := queryIsProcessed(context.Background(), pool, shopID, "evt-1")
|
||||
return processed
|
||||
}, 5*time.Second, 20*time.Millisecond,
|
||||
"event was not processed within 5s of insertion after the LISTEN connection was forcibly dropped - "+
|
||||
"reconnection did not restore the reactive path")
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessEvents() returned error = %v after context cancellation, want nil", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ProcessEvents() did not return within 5s of context cancellation")
|
||||
}
|
||||
waitStopped(t, errCh, 5*time.Second)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package authentication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"ruben/inventory2/consts"
|
||||
"ruben/inventory2/internal/testdb"
|
||||
)
|
||||
@@ -22,36 +24,21 @@ func TestDevLogin(t *testing.T) {
|
||||
})
|
||||
|
||||
accessToken, expiration, err := auth.DevLogin(ctx, userID, "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("DevLogin() error = %v", err)
|
||||
}
|
||||
if accessToken == "" {
|
||||
t.Fatal("DevLogin() returned an empty access token")
|
||||
}
|
||||
if !expiration.After(time.Now().Add(30 * 24 * time.Hour)) {
|
||||
t.Errorf("DevLogin() expiration = %v, want something far enough out to avoid the near-expiry refresh path", expiration)
|
||||
}
|
||||
require.NoError(t, err, "DevLogin()")
|
||||
require.NotEmpty(t, accessToken, "DevLogin() returned an empty access token")
|
||||
assert.True(t, expiration.After(time.Now().Add(30*24*time.Hour)),
|
||||
"DevLogin() expiration = %v, want something far enough out to avoid the near-expiry refresh path", expiration)
|
||||
|
||||
claims, err := auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccessTokenClaimsAndExpiration() error = %v", err)
|
||||
}
|
||||
if claims.Name != "Test User" {
|
||||
t.Errorf("claims.Name = %q, want %q", claims.Name, "Test User")
|
||||
}
|
||||
require.NoError(t, err, "GetAccessTokenClaimsAndExpiration()")
|
||||
assert.Equal(t, "Test User", claims.Name, "claims.Name")
|
||||
// Postgres timestamptz has microsecond precision, so the round-tripped
|
||||
// value loses the sub-microsecond portion of Go's nanosecond clock.
|
||||
if diff := claims.Expiration.Sub(expiration); diff > time.Millisecond || diff < -time.Millisecond {
|
||||
t.Errorf("claims.Expiration = %v, want ~%v (diff %v)", claims.Expiration, expiration, diff)
|
||||
}
|
||||
assert.WithinDuration(t, expiration, claims.Expiration, time.Millisecond, "claims.Expiration")
|
||||
|
||||
_, tokenType, err := auth.getRefreshTokenForAccessToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("getRefreshTokenForAccessToken() error = %v", err)
|
||||
}
|
||||
if tokenType != "dev" {
|
||||
t.Errorf("tokenType = %q, want %q", tokenType, "dev")
|
||||
}
|
||||
require.NoError(t, err, "getRefreshTokenForAccessToken()")
|
||||
assert.Equal(t, "dev", tokenType, "tokenType")
|
||||
}
|
||||
|
||||
// DevLogin should be safe to call more than once for the same user_id -
|
||||
@@ -69,18 +56,12 @@ func TestDevLogin_SameUserIDTwice(t *testing.T) {
|
||||
})
|
||||
|
||||
token1, _, err := auth.DevLogin(ctx, userID, "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("first DevLogin() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "first DevLogin()")
|
||||
|
||||
token2, _, err := auth.DevLogin(ctx, userID, "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("second DevLogin() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "second DevLogin()")
|
||||
|
||||
if token1 == token2 {
|
||||
t.Fatalf("DevLogin() returned the same access token twice: %q", token1)
|
||||
}
|
||||
assert.NotEqual(t, token1, token2, "DevLogin() returned the same access token twice")
|
||||
}
|
||||
|
||||
func TestGetAccessTokenClaimsAndExpiration_UnknownToken(t *testing.T) {
|
||||
@@ -89,7 +70,5 @@ func TestGetAccessTokenClaimsAndExpiration_UnknownToken(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := auth.GetAccessTokenClaimsAndExpiration(ctx, "no-such-token-"+testdb.NewUserID(t))
|
||||
if !errors.Is(err, consts.ErrNotFound) {
|
||||
t.Fatalf("GetAccessTokenClaimsAndExpiration() error = %v, want %v", err, consts.ErrNotFound)
|
||||
}
|
||||
require.ErrorIs(t, err, consts.ErrNotFound, "GetAccessTokenClaimsAndExpiration()")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"ruben/inventory2/domains/raw_events"
|
||||
"ruben/inventory2/internal/testdb"
|
||||
@@ -39,33 +41,20 @@ func TestSaveAndLoadEventsForStore(t *testing.T) {
|
||||
Payload: json.RawMessage(`{"n":2}`),
|
||||
}
|
||||
|
||||
if err := store.Save(ctx, &older); err != nil {
|
||||
t.Fatalf("Save() older event error = %v", err)
|
||||
}
|
||||
if err := store.Save(ctx, &newer); err != nil {
|
||||
t.Fatalf("Save() newer event error = %v", err)
|
||||
}
|
||||
require.NoError(t, store.Save(ctx, &older), "Save() older event")
|
||||
require.NoError(t, store.Save(ctx, &newer), "Save() newer event")
|
||||
|
||||
got, err := store.LoadEventsForStore(ctx, platform, storeID)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadEventsForStore() error = %v", err)
|
||||
}
|
||||
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("LoadEventsForStore() returned %d events, want 2: %+v", len(got), got)
|
||||
}
|
||||
require.NoError(t, err, "LoadEventsForStore()")
|
||||
require.Len(t, got, 2, "LoadEventsForStore()")
|
||||
|
||||
// ordered event_timestamp DESC - newest first.
|
||||
if got[0].EventID != "evt-2" || got[1].EventID != "evt-1" {
|
||||
t.Errorf("LoadEventsForStore() order = [%s, %s], want [evt-2, evt-1]", got[0].EventID, got[1].EventID)
|
||||
}
|
||||
assert.Equal(t, "evt-2", got[0].EventID, "LoadEventsForStore()[0]")
|
||||
assert.Equal(t, "evt-1", got[1].EventID, "LoadEventsForStore()[1]")
|
||||
|
||||
var payload struct{ N int }
|
||||
if err := json.Unmarshal(got[0].Payload, &payload); err != nil {
|
||||
t.Fatalf("failed to unmarshal LoadEventsForStore()[0].Payload = %s: %v", got[0].Payload, err)
|
||||
}
|
||||
if payload.N != 2 {
|
||||
t.Errorf("LoadEventsForStore()[0].Payload n = %d, want 2", payload.N)
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(got[0].Payload, &payload), "unmarshal LoadEventsForStore()[0].Payload")
|
||||
assert.Equal(t, 2, payload.N, "LoadEventsForStore()[0].Payload n")
|
||||
}
|
||||
|
||||
func TestLoadEventsForStore_NoEvents(t *testing.T) {
|
||||
@@ -74,10 +63,6 @@ func TestLoadEventsForStore_NoEvents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
got, err := store.LoadEventsForStore(ctx, "test-platform", "no-such-store-"+uuid.NewString())
|
||||
if err != nil {
|
||||
t.Fatalf("LoadEventsForStore() error = %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("LoadEventsForStore() = %+v, want empty", got)
|
||||
}
|
||||
require.NoError(t, err, "LoadEventsForStore()")
|
||||
assert.Empty(t, got, "LoadEventsForStore()")
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ package reports_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"ruben/inventory2/consts"
|
||||
"ruben/inventory2/domains/accounts"
|
||||
@@ -28,17 +29,13 @@ func setupAmazonMockShop(t *testing.T, pool *pgxpool.Pool, acctStore *accounts.S
|
||||
testdb.SeedOAuthUser(t, pool, userID)
|
||||
|
||||
acct, err := acctStore.CreateAccount(ctx, userID, userID+"@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAccount() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "CreateAccount()")
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), "DELETE FROM accounts WHERE account_id = $1", acct.AccountID)
|
||||
})
|
||||
|
||||
id, err := acctStore.CreateMockShop(ctx, acct.AccountID, accounts.Amazon, "Test Shop")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateMockShop() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "CreateMockShop()")
|
||||
shopID = id.String()
|
||||
|
||||
t.Cleanup(func() {
|
||||
@@ -61,9 +58,7 @@ func insertRawShopEvent(t *testing.T, pool *pgxpool.Pool, shopID, eventID string
|
||||
INSERT INTO mock.raw_shop_events (platform, shop_id, event_timestamp, event_id, raw_payload)
|
||||
VALUES ('amazon', $1, $2, $3, $4::jsonb)
|
||||
`, shopID, ts, eventID, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert raw shop event: %v", err)
|
||||
}
|
||||
require.NoError(t, err, "insert raw shop event")
|
||||
}
|
||||
|
||||
func TestGetRawShopEvents(t *testing.T) {
|
||||
@@ -80,30 +75,19 @@ func TestGetRawShopEvents(t *testing.T) {
|
||||
insertRawShopEvent(t, pool, shopID, "evt-2", newer, `{"n":2}`)
|
||||
|
||||
got, err := reportsStore.GetRawShopEvents(ctx, acctID, accounts.Amazon, shopID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRawShopEvents() error = %v", err)
|
||||
}
|
||||
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("GetRawShopEvents() returned %d events, want 2: %+v", len(got), got)
|
||||
}
|
||||
require.NoError(t, err, "GetRawShopEvents()")
|
||||
require.Len(t, got, 2, "GetRawShopEvents()")
|
||||
|
||||
// ordered event_timestamp DESC, event_id ASC - newest first.
|
||||
if got[0].EventID != "evt-2" || got[1].EventID != "evt-1" {
|
||||
t.Errorf("GetRawShopEvents() order = [%s, %s], want [evt-2, evt-1]", got[0].EventID, got[1].EventID)
|
||||
}
|
||||
assert.Equal(t, "evt-2", got[0].EventID, "GetRawShopEvents()[0]")
|
||||
assert.Equal(t, "evt-1", got[1].EventID, "GetRawShopEvents()[1]")
|
||||
|
||||
if got[0].Platform != accounts.Amazon || got[0].ShopID != shopID {
|
||||
t.Errorf("got[0] Platform/ShopID = %s/%s, want %s/%s", got[0].Platform, got[0].ShopID, accounts.Amazon, shopID)
|
||||
}
|
||||
assert.Equal(t, accounts.Amazon, got[0].Platform, "got[0].Platform")
|
||||
assert.Equal(t, shopID, got[0].ShopID, "got[0].ShopID")
|
||||
|
||||
var payload struct{ N int }
|
||||
if err := json.Unmarshal(got[0].RawPayload, &payload); err != nil {
|
||||
t.Fatalf("failed to unmarshal got[0].RawPayload = %s: %v", got[0].RawPayload, err)
|
||||
}
|
||||
if payload.N != 2 {
|
||||
t.Errorf("got[0].RawPayload n = %d, want 2", payload.N)
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(got[0].RawPayload, &payload), "unmarshal got[0].RawPayload")
|
||||
assert.Equal(t, 2, payload.N, "got[0].RawPayload n")
|
||||
}
|
||||
|
||||
func TestGetRawShopEvents_NoEvents(t *testing.T) {
|
||||
@@ -115,12 +99,8 @@ func TestGetRawShopEvents_NoEvents(t *testing.T) {
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
|
||||
got, err := reportsStore.GetRawShopEvents(ctx, acctID, accounts.Amazon, shopID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRawShopEvents() error = %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("GetRawShopEvents() = %+v, want empty", got)
|
||||
}
|
||||
require.NoError(t, err, "GetRawShopEvents()")
|
||||
assert.Empty(t, got, "GetRawShopEvents()")
|
||||
}
|
||||
|
||||
func TestGetRawShopEvents_UnknownShop(t *testing.T) {
|
||||
@@ -133,15 +113,11 @@ func TestGetRawShopEvents_UnknownShop(t *testing.T) {
|
||||
testdb.SeedOAuthUser(t, pool, userID)
|
||||
|
||||
acct, err := acctStore.CreateAccount(ctx, userID, userID+"@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAccount() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "CreateAccount()")
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), "DELETE FROM accounts WHERE account_id = $1", acct.AccountID)
|
||||
})
|
||||
|
||||
_, err = reportsStore.GetRawShopEvents(ctx, acct.AccountID, accounts.Amazon, "no-such-shop-"+uuid.NewString())
|
||||
if !errors.Is(err, consts.ErrNotFound) {
|
||||
t.Fatalf("GetRawShopEvents() error = %v, want %v", err, consts.ErrNotFound)
|
||||
}
|
||||
require.ErrorIs(t, err, consts.ErrNotFound, "GetRawShopEvents()")
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package reports_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"ruben/inventory2/consts"
|
||||
"ruben/inventory2/domains/accounts"
|
||||
"ruben/inventory2/domains/reports"
|
||||
@@ -34,9 +36,7 @@ func createAmazonListing(t *testing.T, acctStore *accounts.Store, acctID int64,
|
||||
Description: "a listing created for a test",
|
||||
Count: baseCount,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateMockListing() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "CreateMockListing()")
|
||||
|
||||
t.Cleanup(func() {
|
||||
pool.Exec(context.Background(), `
|
||||
@@ -64,38 +64,28 @@ func TestGetListingCountsOverTime(t *testing.T) {
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
listingID := createAmazonListing(t, acctStore, acctID, shopID, 100)
|
||||
|
||||
if _, err := acctStore.SaveNewMockSale(ctx, acctID, accounts.Amazon, shopID, listingID, 10); err != nil {
|
||||
t.Fatalf("SaveNewMockSale() error = %v", err)
|
||||
}
|
||||
if _, err := acctStore.SaveNewMockRefund(ctx, acctID, accounts.Amazon, shopID, listingID, 5); err != nil {
|
||||
t.Fatalf("SaveNewMockRefund() error = %v", err)
|
||||
}
|
||||
if _, err := acctStore.SaveNewMockInventoryReset(ctx, acctID, accounts.Amazon, shopID, listingID, 50); err != nil {
|
||||
t.Fatalf("SaveNewMockInventoryReset() error = %v", err)
|
||||
}
|
||||
_, err := acctStore.SaveNewMockSale(ctx, acctID, accounts.Amazon, shopID, listingID, 10)
|
||||
require.NoError(t, err, "SaveNewMockSale()")
|
||||
_, err = acctStore.SaveNewMockRefund(ctx, acctID, accounts.Amazon, shopID, listingID, 5)
|
||||
require.NoError(t, err, "SaveNewMockRefund()")
|
||||
_, err = acctStore.SaveNewMockInventoryReset(ctx, acctID, accounts.Amazon, shopID, listingID, 50)
|
||||
require.NoError(t, err, "SaveNewMockInventoryReset()")
|
||||
|
||||
got, err := reportsStore.GetListingCountsOverTime(ctx, acctID, accounts.Amazon, shopID, listingID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetListingCountsOverTime() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "GetListingCountsOverTime()")
|
||||
|
||||
wantCounts := []int64{100, 110, 105, 50}
|
||||
if len(got) != len(wantCounts) {
|
||||
t.Fatalf("GetListingCountsOverTime() returned %d rows, want %d: %+v", len(got), len(wantCounts), got)
|
||||
}
|
||||
require.Len(t, got, len(wantCounts), "GetListingCountsOverTime()")
|
||||
|
||||
if got[0].EventTimestamp != nil {
|
||||
t.Errorf("got[0].EventTimestamp = %v, want nil (the base count row)", got[0].EventTimestamp)
|
||||
}
|
||||
gotCounts := make([]int64, len(got))
|
||||
for i, row := range got {
|
||||
if row.Count != wantCounts[i] {
|
||||
t.Errorf("got[%d].Count = %d, want %d (full sequence: %+v)", i, row.Count, wantCounts[i], got)
|
||||
}
|
||||
gotCounts[i] = row.Count
|
||||
}
|
||||
assert.Equal(t, wantCounts, gotCounts, "GetListingCountsOverTime() counts, full sequence: %+v", got)
|
||||
|
||||
assert.Nil(t, got[0].EventTimestamp, "got[0].EventTimestamp should be nil (the base count row)")
|
||||
for i := 1; i < len(got); i++ {
|
||||
if got[i].EventTimestamp == nil {
|
||||
t.Errorf("got[%d].EventTimestamp = nil, want set (only the base row should be nil)", i)
|
||||
}
|
||||
assert.NotNil(t, got[i].EventTimestamp, "got[%d].EventTimestamp should be set (only the base row should be nil)", i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +98,7 @@ func TestGetListingCountsOverTime_UnknownListing(t *testing.T) {
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
|
||||
_, err := reportsStore.GetListingCountsOverTime(ctx, acctID, accounts.Amazon, shopID, "no-such-listing")
|
||||
if !errors.Is(err, consts.ErrNotFound) {
|
||||
t.Fatalf("GetListingCountsOverTime() error = %v, want %v", err, consts.ErrNotFound)
|
||||
}
|
||||
require.ErrorIs(t, err, consts.ErrNotFound, "GetListingCountsOverTime()")
|
||||
}
|
||||
|
||||
func TestGetListingCountsReport(t *testing.T) {
|
||||
@@ -122,28 +110,20 @@ func TestGetListingCountsReport(t *testing.T) {
|
||||
acctID, shopID := setupAmazonMockShop(t, pool, acctStore)
|
||||
listingID := createAmazonListing(t, acctStore, acctID, shopID, 100)
|
||||
|
||||
if _, err := acctStore.SaveNewMockSale(ctx, acctID, accounts.Amazon, shopID, listingID, 10); err != nil {
|
||||
t.Fatalf("SaveNewMockSale() error = %v", err)
|
||||
}
|
||||
if _, err := acctStore.SaveNewMockInventoryReset(ctx, acctID, accounts.Amazon, shopID, listingID, 20); err != nil {
|
||||
t.Fatalf("SaveNewMockInventoryReset() error = %v", err)
|
||||
}
|
||||
_, err := acctStore.SaveNewMockSale(ctx, acctID, accounts.Amazon, shopID, listingID, 10)
|
||||
require.NoError(t, err, "SaveNewMockSale()")
|
||||
_, err = acctStore.SaveNewMockInventoryReset(ctx, acctID, accounts.Amazon, shopID, listingID, 20)
|
||||
require.NoError(t, err, "SaveNewMockInventoryReset()")
|
||||
|
||||
report, err := reportsStore.GetListingCountsReport(ctx, acctID, accounts.Amazon, shopID, listingID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetListingCountsReport() error = %v", err)
|
||||
}
|
||||
require.NoError(t, err, "GetListingCountsReport()")
|
||||
|
||||
if report.AccountID != acctID || report.Platform != accounts.Amazon || report.ShopID != shopID || report.ListingID != listingID {
|
||||
t.Errorf("report identity = %+v, want AccountID=%d Platform=%s ShopID=%s ListingID=%s",
|
||||
report, acctID, accounts.Amazon, shopID, listingID)
|
||||
}
|
||||
assert.Equal(t, acctID, report.AccountID, "report.AccountID")
|
||||
assert.Equal(t, accounts.Amazon, report.Platform, "report.Platform")
|
||||
assert.Equal(t, shopID, report.ShopID, "report.ShopID")
|
||||
assert.Equal(t, listingID, report.ListingID, "report.ListingID")
|
||||
|
||||
// counts over the sequence: 100 (base) -> 110 (sale +10) -> 20 (reset)
|
||||
if got := report.MaxCount().Count; got != 110 {
|
||||
t.Errorf("MaxCount().Count = %d, want 110", got)
|
||||
}
|
||||
if got := report.MinCount().Count; got != 20 {
|
||||
t.Errorf("MinCount().Count = %d, want 20", got)
|
||||
}
|
||||
assert.Equal(t, int64(110), report.MaxCount().Count, "MaxCount().Count")
|
||||
assert.Equal(t, int64(20), report.MinCount().Count, "MinCount().Count")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user