Claude-assisted improvements (untested)

- dev auth flow (side-step OAuth)
- db event processing integration tests
- dev scripts (eg Makefile)
- db / test db migration setup scripts.
This commit is contained in:
2026-08-19 23:20:15 -06:00
parent 82efe19ae0
commit 4e77052a37
35 changed files with 1242 additions and 144 deletions
+3
View File
@@ -2,3 +2,6 @@
*.swp *.swp
node_modules node_modules
.env
.env.example
+55
View File
@@ -0,0 +1,55 @@
.DEFAULT_GOAL := dev
-include .env
export
MIGRATE := migrate -path database_migrations -database "$(DATABASE_URL)"
MIGRATE_TEST := migrate -path database_migrations -database "$(TEST_DATABASE_URL)"
.PHONY: dev migrate-up migrate-down migrate-version tailwind run \
test test-against-dev-db migrate-test-up migrate-test-down migrate-test-version
# applies pending migrations, starts the tailwind watcher in the background,
# then runs the server in the foreground. Ctrl-C stops both.
dev: migrate-up
@./tailwind.sh & \
TW_PID=$$!; \
trap "kill $$TW_PID 2>/dev/null" EXIT INT TERM; \
go run .
migrate-up:
$(MIGRATE) up
migrate-down:
$(MIGRATE) down 1
migrate-version:
$(MIGRATE) version
tailwind:
./tailwind.sh
run:
go run .
# runs the integration test suite against TEST_DATABASE_URL (the dedicated
# inventory_2_test database by default). Applies pending migrations there
# first.
test: migrate-test-up
go test ./...
# same as `test`, but points TEST_DATABASE_URL at the real dev database for
# this run instead of the dedicated test database - useful for checking
# behavior against real data. Leaves the dev DB in whatever state the tests'
# own cleanup produces; it's still your dev data, treat it accordingly.
test-against-dev-db: TEST_DATABASE_URL := $(DATABASE_URL)
test-against-dev-db: test
migrate-test-up:
$(MIGRATE_TEST) up
migrate-test-down:
$(MIGRATE_TEST) down 1
migrate-test-version:
$(MIGRATE_TEST) version
+76
View File
@@ -0,0 +1,76 @@
// Package config loads application configuration from environment variables,
// optionally populated from a .env file in the working directory.
package config
import (
"fmt"
"os"
"strconv"
"github.com/joho/godotenv"
)
type Config struct {
DatabaseURL string
Auth0Domain string
Auth0ClientID string
Auth0ClientSecret string
Auth0CallbackURL string
EtsyAPIKeystring string
EtsyAPISharedSecret string
// DevAuthEnabled, when true, exposes a route that mints a local
// login session for any user_id without going through Auth0. Must
// never be true outside local development.
DevAuthEnabled bool
}
// Load reads a .env file, if present, into the process environment, then
// reads the required configuration values from the environment.
func Load() (Config, error) {
if err := godotenv.Load(); err != nil && !os.IsNotExist(err) {
return Config{}, fmt.Errorf("failed to load .env file: %w", err)
}
var (
cfg Config
missing []string
)
required := func(key string) string {
v := os.Getenv(key)
if v == "" {
missing = append(missing, key)
}
return v
}
cfg.DatabaseURL = required("DATABASE_URL")
cfg.Auth0Domain = required("AUTH0_DOMAIN")
cfg.Auth0ClientID = required("AUTH0_CLIENT_ID")
cfg.Auth0ClientSecret = required("AUTH0_CLIENT_SECRET")
cfg.Auth0CallbackURL = required("AUTH0_CALLBACK_URL")
cfg.EtsyAPIKeystring = required("ETSY_API_KEYSTRING")
cfg.EtsyAPISharedSecret = required("ETSY_API_SHARED_SECRET")
if len(missing) > 0 {
return Config{}, fmt.Errorf(
"missing required environment variables: %v (see .env.example)",
missing,
)
}
if raw := os.Getenv("DEV_AUTH_ENABLED"); raw != "" {
devAuthEnabled, err := strconv.ParseBool(raw)
if err != nil {
return Config{}, fmt.Errorf("invalid DEV_AUTH_ENABLED value %q: %w", raw, err)
}
cfg.DevAuthEnabled = devAuthEnabled
}
return cfg, nil
}
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
func newPool(ctx context.Context) (*pgxpool.Pool, error) { func newPool(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
pool, err := pgxpool.New(ctx, "postgres://app_client:app_password@localhost:5432/inventory_2?sslmode=disable") pool, err := pgxpool.New(ctx, databaseURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create database client: %w", err) return nil, fmt.Errorf("failed to create database client: %w", err)
} }
@@ -1,6 +1,6 @@
CREATE TABLE oauth_login_states ( CREATE TABLE oauth_login_states (
state BYTEA NOT NULL, state BYTEA NOT NULL,
expiration TIMESTAMPTZ NOT NULL DEFAULT (NOW() + 10 'minute'), expiration TIMESTAMPTZ NOT NULL DEFAULT (NOW() + '10 minutes'::interval),
PRIMARY KEY (state) PRIMARY KEY (state)
); );
@@ -0,0 +1,14 @@
BEGIN;
ALTER TABLE oauth_tokens
ALTER COLUMN id_token_custom_claims_updated_at
TYPE TEXT
USING id_token_custom_claims_updated_at::TEXT;
ALTER TABLE oauth_tokens
ADD COLUMN claims JSONB NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE oauth_tokens
ALTER COLUMN claims DROP DEFAULT;
COMMIT;
@@ -0,0 +1,16 @@
BEGIN;
-- Reconciles oauth_tokens with the schema the application code actually
-- expects. Neither of these two changes was ever committed as a migration
-- despite being applied by hand at some point - see the "claims" column
-- below, which no application code reads or writes.
ALTER TABLE oauth_tokens
DROP COLUMN claims;
ALTER TABLE oauth_tokens
ALTER COLUMN id_token_custom_claims_updated_at
TYPE TIMESTAMPTZ
USING id_token_custom_claims_updated_at::TIMESTAMPTZ;
COMMIT;
-2
View File
@@ -1468,8 +1468,6 @@ func (db *Store) SetListingInListingInMockSyncGroupBeingEdited(ctx context.Conte
default: default:
return fmt.Errorf("unexpected number of rows affected: %d", n) return fmt.Errorf("unexpected number of rows affected: %d", n)
} }
return nil
} }
return consts.ErrNotFound return consts.ErrNotFound
+133
View File
@@ -0,0 +1,133 @@
package accounts_test
import (
"context"
"errors"
"testing"
"ruben/inventory2/consts"
"ruben/inventory2/domains/accounts"
"ruben/inventory2/internal/testdb"
)
func TestCreateAccount(t *testing.T) {
pool := testdb.Pool(t)
store := accounts.NewStore(testdb.Logger(), pool)
ctx := context.Background()
userID := testdb.NewUserID(t)
testdb.SeedOAuthUser(t, pool, userID)
email := userID + "@example.com"
acct, err := store.CreateAccount(ctx, userID, email)
if err != nil {
t.Fatalf("CreateAccount() error = %v", err)
}
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)
}
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)
}
}
func TestCreateAccount_DuplicateUserIsConflict(t *testing.T) {
pool := testdb.Pool(t)
store := accounts.NewStore(testdb.Logger(), pool)
ctx := context.Background()
userID := testdb.NewUserID(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)
}
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)
}
}
func TestGetAccount_NotFound(t *testing.T) {
pool := testdb.Pool(t)
store := accounts.NewStore(testdb.Logger(), pool)
ctx := context.Background()
_, err := store.GetAccount(ctx, -1)
if !errors.Is(err, consts.ErrNotFound) {
t.Fatalf("GetAccount() error = %v, want %v", err, consts.ErrNotFound)
}
}
func TestGetUserAndAccountByAccessToken(t *testing.T) {
pool := testdb.Pool(t)
store := accounts.NewStore(testdb.Logger(), pool)
ctx := context.Background()
userID := testdb.NewUserID(t)
accessToken := testdb.SeedOAuthSession(t, pool, userID)
// 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)
}
created, err := store.CreateAccount(ctx, userID, userID+"@example.com")
if err != nil {
t.Fatalf("CreateAccount() error = %v", err)
}
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)
}
}
func TestGetUserAndAccountByAccessToken_UnknownToken(t *testing.T) {
pool := testdb.Pool(t)
store := accounts.NewStore(testdb.Logger(), pool)
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)
}
}
+248
View File
@@ -0,0 +1,248 @@
package amazon
import (
"context"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/internal/testdb"
)
// notifySpy is a MockEventListener that records every event it's notified
// about. (*Mocks).processEvent fires notifications from a goroutine without
// waiting for them, so tests need to synchronize on notified rather than
// asserting on the event list immediately.
type notifySpy struct {
mu sync.Mutex
events []raw_events.Event
notified chan struct{}
}
func newNotifySpy() *notifySpy {
return &notifySpy{notified: make(chan struct{}, 1000)}
}
func (s *notifySpy) Notify(_ context.Context, e raw_events.Event) error {
s.mu.Lock()
s.events = append(s.events, e)
s.mu.Unlock()
s.notified <- struct{}{}
return nil
}
func (s *notifySpy) eventsSnapshot() []raw_events.Event {
s.mu.Lock()
defer s.mu.Unlock()
return append([]raw_events.Event(nil), s.events...)
}
func (s *notifySpy) waitForCount(t *testing.T, n int, timeout time.Duration) {
t.Helper()
deadline := time.After(timeout)
for i := 0; i < n; i++ {
select {
case <-s.notified:
case <-deadline:
t.Fatalf("timed out after %v waiting for notification %d/%d (got %d so far)", timeout, i+1, n, len(s.eventsSnapshot()))
}
}
}
// insertRawAmazonEvent inserts directly into mock.raw_shop_events, the same
// entry point real mock sale/refund/inventory simulations use. A DB trigger
// (mock.process_raw_amazon_event, see migrations 000026/000028) copies the
// row into mock.shop_amazon_events and fires pg_notify on
// mock_shop_amazon_event_inserted - so this one insert exercises the exact
// same path production traffic does, instead of faking the downstream
// table directly.
func insertRawAmazonEvent(t *testing.T, pool *pgxpool.Pool, shopID, eventID string) {
t.Helper()
_, err := pool.Exec(context.Background(), `
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)
}
}
// insertRawAmazonEvents bulk-inserts n events for shopID in one statement,
// each with a distinct event_id/event_timestamp.
func insertRawAmazonEvents(t *testing.T, pool *pgxpool.Pool, shopID string, n int) {
t.Helper()
_, err := pool.Exec(context.Background(), `
INSERT INTO mock.raw_shop_events (platform, shop_id, event_timestamp, event_id, raw_payload)
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)
}
}
func isProcessed(t *testing.T, pool *pgxpool.Pool, shopID, eventID string) bool {
t.Helper()
var processed, successful bool
err := pool.QueryRow(context.Background(), `
SELECT processed, processed_successfully
FROM mock.shop_amazon_events
WHERE shop_id = $1 AND event_id = $2
`, shopID, eventID).Scan(&processed, &successful)
if err != nil {
t.Fatalf("failed to check processed state: %v", err)
}
return processed && successful
}
func countUnprocessed(t *testing.T, pool *pgxpool.Pool, shopID string) int {
t.Helper()
var n int
err := pool.QueryRow(context.Background(), `
SELECT count(*) FROM mock.shop_amazon_events WHERE shop_id = $1 AND NOT processed
`, shopID).Scan(&n)
if err != nil {
t.Fatalf("failed to count unprocessed events: %v", err)
}
return n
}
// cleanupShop registers deletion of every row this test's shopID may have
// produced, in FK-safe order (shop_amazon_events references raw_shop_events).
func cleanupShop(t *testing.T, pool *pgxpool.Pool, shopID string) {
t.Cleanup(func() {
ctx := context.Background()
pool.Exec(ctx, `DELETE FROM mock.shop_amazon_events WHERE shop_id = $1`, shopID)
pool.Exec(ctx, `DELETE FROM mock.raw_shop_events WHERE platform = 'amazon' AND shop_id = $1`, shopID)
})
}
func TestProcessUnprocessedEvents_ProcessesAllEventsAcrossBatches(t *testing.T) {
pool := testdb.Pool(t)
spy := newNotifySpy()
m := NewMocks(testdb.Logger(), pool).SetListener(spy)
ctx := context.Background()
shopID := "test-shop-" + uuid.NewString()
cleanupShop(t, pool, shopID)
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)
}
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)
}
spy.waitForCount(t, n, 5*time.Second)
}
func TestProcessUnprocessedEvents_NoListenerConfigured(t *testing.T) {
pool := testdb.Pool(t)
m := NewMocks(testdb.Logger(), pool) // no SetListener call
ctx := context.Background()
shopID := "test-shop-" + uuid.NewString()
cleanupShop(t, pool, shopID)
insertRawAmazonEvent(t, pool, shopID, "evt-1")
if err := m.processUnprocessedEvents(ctx); err != nil {
t.Fatalf("processUnprocessedEvents() error = %v", err)
}
if !isProcessed(t, pool, shopID, "evt-1") {
t.Error("event was not marked processed despite no listener being configured")
}
}
// TestProcessEvents_ReactsToNotification drives the actual long-running
// loop: LISTEN registration, a real Postgres NOTIFY fired by the DB trigger
// on insert, WaitForNotification waking the loop, and the listener callback
// - the full stateful path, not just the deterministic batch-processing
// core covered above.
func TestProcessEvents_ReactsToNotification(t *testing.T) {
pool := testdb.Pool(t)
spy := newNotifySpy()
m := NewMocks(testdb.Logger(), pool).SetListener(spy)
shopID := "test-shop-" + uuid.NewString()
cleanupShop(t, pool, shopID)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errCh := make(chan error, 1)
go func() {
errCh <- m.ProcessEvents(ctx)
}()
// ProcessEvents registers its Postgres LISTEN synchronously before
// entering its main loop, but that registration still races against
// this goroutine actually getting scheduled - the production code
// exposes no "listening now" signal a caller (or a test) can wait
// on. A short sleep is the only lever available from outside; see
// the accompanying report for why that's a real testability/design
// gap, not just a test-flakiness workaround.
time.Sleep(200 * time.Millisecond)
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)
}
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")
}
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")
}
}
// TestProcessEvents_ShutsDownOnContextCancel checks the lifecycle in
// isolation, without depending on NOTIFY timing at all - a fast, low-flake
// guard against shutdown regressions (hangs, goroutine leaks) independent
// of whether the reactive path above is working.
func TestProcessEvents_ShutsDownOnContextCancel(t *testing.T) {
pool := testdb.Pool(t)
m := NewMocks(testdb.Logger(), pool)
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
errCh <- m.ProcessEvents(ctx)
}()
time.Sleep(50 * time.Millisecond) // let it reach its first select
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")
}
}
+11 -24
View File
@@ -17,26 +17,11 @@ import (
"ruben/inventory2/logging" "ruben/inventory2/logging"
) )
// TODO: move these to a config?
const (
// The URL of our Auth0 Tenant Domain.
// If you're using a Custom Domain, be sure to set this to that value instead.
AUTH0_DOMAIN = "dev-uq3gqy5bdnwxmr6d.us.auth0.com"
// Our Auth0 application"s Client ID.
AUTH0_CLIENT_ID = "JEjrXTQ9fxlTLgp9RgTIACpUk8a2lqNT"
// Our Auth0 application"s Client Secret.
AUTH0_CLIENT_SECRET = "83U-iWdVaNnwk9XDzteo_2VMyOq_l1siKYqg1_2E7jCzgL8MnkaxlysPMcPMGlxA"
// The Callback URL of our application.
AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/api/auth/login/callback"
)
type ( type (
// Authenticator is used to authenticate our users. // Authenticator is used to authenticate our users.
Authenticator struct { Authenticator struct {
log *logging.Logger log *logging.Logger
domain string
*oidc.Provider *oidc.Provider
oauth2.Config oauth2.Config
db *pgxpool.Pool db *pgxpool.Pool
@@ -65,10 +50,11 @@ func New(
ctx context.Context, ctx context.Context,
db *pgxpool.Pool, db *pgxpool.Pool,
logger *logging.Logger, logger *logging.Logger,
domain, clientID, clientSecret, callbackURL string,
) (*Authenticator, error) { ) (*Authenticator, error) {
provider, err := oidc.NewProvider( provider, err := oidc.NewProvider(
ctx, ctx,
"https://"+AUTH0_DOMAIN+"/", "https://"+domain+"/",
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -76,11 +62,12 @@ func New(
return &Authenticator{ return &Authenticator{
log: logger, log: logger,
domain: domain,
Provider: provider, Provider: provider,
Config: oauth2.Config{ Config: oauth2.Config{
ClientID: AUTH0_CLIENT_ID, ClientID: clientID,
ClientSecret: AUTH0_CLIENT_SECRET, ClientSecret: clientSecret,
RedirectURL: AUTH0_CALLBACK_URL, RedirectURL: callbackURL,
Endpoint: provider.Endpoint(), Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile"}, Scopes: []string{oidc.ScopeOpenID, "profile"},
}, },
@@ -244,7 +231,7 @@ func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token)
func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL { func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL {
return &url.URL{ return &url.URL{
Scheme: "https", Scheme: "https",
Host: AUTH0_DOMAIN, Host: a.domain,
Path: "/v2/logout", Path: "/v2/logout",
RawQuery: url.Values{ RawQuery: url.Values{
"returnTo": { "returnTo": {
@@ -253,7 +240,7 @@ func (a *Authenticator) GetLogoutURL(requestHost string) *url.URL {
Host: requestHost, Host: requestHost,
}).String(), }).String(),
}, },
"client_id": {AUTH0_CLIENT_ID}, "client_id": {a.Config.ClientID},
}.Encode(), }.Encode(),
} }
} }
@@ -267,7 +254,7 @@ func (a *Authenticator) RefreshAccessToken(
err error, err error,
) { ) {
refreshToken, tokenType, err := a.getRefreshTokenForAccessToken(ctx, accessToken) refreshToken, tokenType, err := a.getRefreshTokenForAccessToken(ctx, oldAccessToken)
if err != nil { if err != nil {
return "", time.Time{}, fmt.Errorf("failed to load refresh token: %w", err) return "", time.Time{}, fmt.Errorf("failed to load refresh token: %w", err)
} }
+97
View File
@@ -0,0 +1,97 @@
package authentication
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
// DevLogin mints a local session for userID without going through the real
// Auth0 OAuth flow. It writes a real oauth_users/oauth_tokens row, so every
// other code path (identity lookup, account linking/creation, cookie
// handling) treats it exactly like a normal login.
//
// Only ever call this from a route gated on an explicit dev-mode flag -
// never wire it up unconditionally, since it lets the caller authenticate
// as any user_id with no credentials.
func (a *Authenticator) DevLogin(ctx context.Context, userID, name string) (accessToken string, expiration time.Time, err error) {
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
return "", time.Time{}, fmt.Errorf("failed to generate access token: %w", err)
}
accessToken = "dev_" + hex.EncodeToString(tokenBytes)
// Set far in the future so the near-expiry refresh path in
// server/auth never tries to refresh a token that has no real
// Auth0-side refresh token behind it.
expiration = time.Now().Add(365 * 24 * time.Hour)
if _, err = a.db.Exec(
ctx,
`
WITH ensured_user AS (
INSERT INTO oauth_users (user_id)
VALUES (@user_id)
ON CONFLICT DO NOTHING
)
INSERT INTO oauth_tokens (
access_token,
token_type,
refresh_token,
expiry,
id_token_issuer,
id_token_audience,
id_token_subject,
id_token_expiry,
id_token_issued_at,
id_token_nonce,
id_token_access_token_hash,
id_token_custom_claims_family_name,
id_token_custom_claims_given_name,
id_token_custom_claims_name,
id_token_custom_claims_nickname,
id_token_custom_claims_picture,
id_token_custom_claims_updated_at
)
VALUES (
@access_token,
'dev',
'',
@expiry,
'dev-auth-bypass',
@audience,
@user_id,
@expiry,
NOW(),
'',
'',
@name,
@name,
@name,
@name,
'',
NOW()
)
`,
pgx.NamedArgs{
"access_token": accessToken,
"expiry": expiration,
"user_id": userID,
"name": name,
"audience": pgtype.FlatArray[string]{"dev"},
},
); err != nil {
return "", time.Time{}, fmt.Errorf("failed to save dev session: %w", err)
}
return accessToken, expiration, nil
}
+95
View File
@@ -0,0 +1,95 @@
package authentication
import (
"context"
"errors"
"testing"
"time"
"ruben/inventory2/consts"
"ruben/inventory2/internal/testdb"
)
func TestDevLogin(t *testing.T) {
pool := testdb.Pool(t)
auth := &Authenticator{db: pool}
ctx := context.Background()
userID := testdb.NewUserID(t)
t.Cleanup(func() {
pool.Exec(context.Background(), "DELETE FROM oauth_tokens WHERE id_token_subject = $1", userID)
pool.Exec(context.Background(), "DELETE FROM oauth_users WHERE user_id = $1", userID)
})
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)
}
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")
}
// 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)
}
_, 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")
}
}
// DevLogin should be safe to call more than once for the same user_id -
// e.g. testing multiple times as the same dev identity - since oauth_users
// is keyed on user_id but each call mints its own oauth_tokens row.
func TestDevLogin_SameUserIDTwice(t *testing.T) {
pool := testdb.Pool(t)
auth := &Authenticator{db: pool}
ctx := context.Background()
userID := testdb.NewUserID(t)
t.Cleanup(func() {
pool.Exec(context.Background(), "DELETE FROM oauth_tokens WHERE id_token_subject = $1", userID)
pool.Exec(context.Background(), "DELETE FROM oauth_users WHERE user_id = $1", userID)
})
token1, _, err := auth.DevLogin(ctx, userID, "Test User")
if err != nil {
t.Fatalf("first DevLogin() error = %v", err)
}
token2, _, err := auth.DevLogin(ctx, userID, "Test User")
if err != nil {
t.Fatalf("second DevLogin() error = %v", err)
}
if token1 == token2 {
t.Fatalf("DevLogin() returned the same access token twice: %q", token1)
}
}
func TestGetAccessTokenClaimsAndExpiration_UnknownToken(t *testing.T) {
pool := testdb.Pool(t)
auth := &Authenticator{db: pool}
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)
}
}
+83
View File
@@ -0,0 +1,83 @@
package raw_events_test
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/google/uuid"
"ruben/inventory2/domains/raw_events"
"ruben/inventory2/internal/testdb"
)
func TestSaveAndLoadEventsForStore(t *testing.T) {
pool := testdb.Pool(t)
store := raw_events.NewStore(testdb.Logger(), pool)
ctx := context.Background()
platform := "test-platform"
storeID := "test-store-" + uuid.NewString()
t.Cleanup(func() {
pool.Exec(context.Background(), "DELETE FROM raw_store_events WHERE platform = $1 AND store_id = $2", platform, storeID)
})
older := raw_events.Event{
Platform: platform,
StoreID: storeID,
EventID: "evt-1",
EventTimestamp: time.Now().Add(-time.Hour).UTC(),
Payload: json.RawMessage(`{"n":1}`),
}
newer := raw_events.Event{
Platform: platform,
StoreID: storeID,
EventID: "evt-2",
EventTimestamp: time.Now().UTC(),
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)
}
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)
}
// 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)
}
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)
}
}
func TestLoadEventsForStore_NoEvents(t *testing.T) {
pool := testdb.Pool(t)
store := raw_events.NewStore(testdb.Logger(), pool)
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)
}
}
+1 -5
View File
@@ -11,6 +11,7 @@ require (
github.com/coreos/go-oidc/v3 v3.17.0 github.com/coreos/go-oidc/v3 v3.17.0
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/lmittmann/tint v1.1.2 github.com/lmittmann/tint v1.1.2
github.com/oapi-codegen/runtime v1.1.2 github.com/oapi-codegen/runtime v1.1.2
golang.org/x/oauth2 v0.34.0 golang.org/x/oauth2 v0.34.0
@@ -26,14 +27,11 @@ require (
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
@@ -42,7 +40,6 @@ require (
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -82,7 +79,6 @@ require (
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+4 -94
View File
@@ -16,29 +16,15 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY=
github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -51,24 +37,14 @@ github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5ql
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -81,15 +57,11 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -103,22 +75,14 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -129,23 +93,19 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -153,8 +113,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -195,8 +153,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
@@ -230,20 +186,15 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
@@ -253,28 +204,17 @@ github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4Z
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts= github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE= github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -282,23 +222,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -315,44 +247,26 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -361,8 +275,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -382,5 +294,3 @@ gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+115
View File
@@ -0,0 +1,115 @@
// Package testdb provides shared fixtures for integration tests that need a
// real Postgres connection. It targets TEST_DATABASE_URL, which `make test`
// points at a dedicated inventory_2_test database by default; override it
// (e.g. `make test TEST_DATABASE_URL=...`) to run the same tests against
// another database, such as the real dev one.
package testdb
import (
"context"
"io"
"log/slog"
"os"
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"ruben/inventory2/logging"
)
// Pool connects to TEST_DATABASE_URL. If it isn't set, the test is skipped
// rather than failed, so `go test ./...` works without Postgres running.
func Pool(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("TEST_DATABASE_URL not set; skipping integration test (see .env.example, or run via `make test`)")
}
pool, err := pgxpool.New(context.Background(), dsn)
if err != nil {
t.Fatalf("failed to connect to test database: %v", err)
}
t.Cleanup(pool.Close)
if err := pool.Ping(context.Background()); err != nil {
t.Fatalf("failed to reach test database at TEST_DATABASE_URL: %v", err)
}
return pool
}
// Logger returns a logger that discards output, for stores that require one
// but whose logging isn't under test.
func Logger() *logging.Logger {
return logging.New(slog.NewTextHandler(io.Discard, nil))
}
// NewUserID returns a unique user_id for a test to use, so parallel test
// runs (including against a shared database) never collide.
func NewUserID(t *testing.T) string {
t.Helper()
return "test-" + t.Name() + "-" + uuid.NewString()
}
// SeedOAuthUser inserts a bare oauth_users row so accounts/etc. FKs that
// reference it are satisfiable, and registers cleanup. Use SeedOAuthSession
// instead if the test also needs a valid access token.
func SeedOAuthUser(t *testing.T, pool *pgxpool.Pool, userID string) {
t.Helper()
ctx := context.Background()
if _, err := pool.Exec(ctx, `INSERT INTO oauth_users (user_id) VALUES ($1)`, userID); err != nil {
t.Fatalf("failed to seed oauth_users row: %v", err)
}
t.Cleanup(func() {
if _, err := pool.Exec(context.Background(), `DELETE FROM oauth_users WHERE user_id = $1`, userID); err != nil {
t.Errorf("cleanup: failed to delete oauth_users row: %v", err)
}
})
}
// SeedOAuthSession inserts an oauth_users row plus a matching oauth_tokens
// row with a freshly generated access token, mirroring the shape a real (or
// authentication.Authenticator.DevLogin) session leaves behind. Registers
// cleanup for both rows, in dependency order.
func SeedOAuthSession(t *testing.T, pool *pgxpool.Pool, userID string) (accessToken string) {
t.Helper()
ctx := context.Background()
SeedOAuthUser(t, pool, userID)
accessToken = "test-token-" + uuid.NewString()
_, err := pool.Exec(ctx, `
INSERT INTO oauth_tokens (
access_token, token_type, refresh_token, expiry,
id_token_issuer, id_token_audience, id_token_subject,
id_token_expiry, id_token_issued_at, id_token_nonce, id_token_access_token_hash,
id_token_custom_claims_family_name, id_token_custom_claims_given_name,
id_token_custom_claims_name, id_token_custom_claims_nickname,
id_token_custom_claims_picture, id_token_custom_claims_updated_at
) VALUES (
$1, 'test', '', NOW() + INTERVAL '1 hour',
'test', '{test}', $2,
NOW() + INTERVAL '1 hour', NOW(), '', '',
'Test', 'User',
'Test User', 'testuser',
'', NOW()
)
`, accessToken, userID)
if err != nil {
t.Fatalf("failed to seed oauth_tokens row: %v", err)
}
t.Cleanup(func() {
if _, err := pool.Exec(context.Background(), `DELETE FROM oauth_tokens WHERE access_token = $1`, accessToken); err != nil {
t.Errorf("cleanup: failed to delete oauth_tokens row: %v", err)
}
})
return accessToken
}
+23 -10
View File
@@ -21,6 +21,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/lmittmann/tint" "github.com/lmittmann/tint"
"ruben/inventory2/config"
"ruben/inventory2/domains/accounts" "ruben/inventory2/domains/accounts"
"ruben/inventory2/domains/amazon" "ruben/inventory2/domains/amazon"
"ruben/inventory2/domains/authentication" "ruben/inventory2/domains/authentication"
@@ -32,11 +33,6 @@ import (
"ruben/inventory2/server/sse" "ruben/inventory2/server/sse"
) )
const (
etsyAPIKeystring = "38ncokqh0jih5jshfk8iv4n5"
etsyAPISharedSecret = "jaaw0tyizf"
)
func main() { func main() {
logger := logging.New(tint.NewHandler(os.Stderr, &tint.Options{ logger := logging.New(tint.NewHandler(os.Stderr, &tint.Options{
AddSource: true, AddSource: true,
@@ -68,14 +64,29 @@ func runApp(ctx context.Context, logger *logging.Logger) error {
ctx, shutdown := context.WithCancel(ctx) ctx, shutdown := context.WithCancel(ctx)
defer shutdown() defer shutdown()
// load configuration
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}
// connect to the database // connect to the database
connPool, err := newPool(ctx) connPool, err := newPool(ctx, cfg.DatabaseURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize database connection pool: %w", err) return fmt.Errorf("failed to initialize database connection pool: %w", err)
} }
auth, err := authentication.New(ctx, connPool, logger.WithGroup("authenticator")) auth, err := authentication.New(
ctx,
connPool,
logger.WithGroup("authenticator"),
cfg.Auth0Domain,
cfg.Auth0ClientID,
cfg.Auth0ClientSecret,
cfg.Auth0CallbackURL,
)
if err != nil { if err != nil {
return fmt.Errorf("failed to construct authenticator: %w", err) return fmt.Errorf("failed to construct authenticator: %w", err)
} }
@@ -84,7 +95,7 @@ func runApp(ctx context.Context, logger *logging.Logger) error {
// start http server // start http server
sseQueue, srvErrCh := runServer(ctx, logger, connPool, auth, accts) sseQueue, srvErrCh := runServer(ctx, logger, connPool, auth, accts, cfg)
// start background processes // start background processes
@@ -207,6 +218,7 @@ func runServer(
connPool *pgxpool.Pool, connPool *pgxpool.Pool,
auth *authentication.Authenticator, auth *authentication.Authenticator,
accts *accounts.Store, accts *accounts.Store,
cfg config.Config,
) (*sse.Queue, <-chan error) { ) (*sse.Queue, <-chan error) {
r := server.NewRouter( r := server.NewRouter(
logger.WithGroup("server"), logger.WithGroup("server"),
@@ -219,11 +231,12 @@ func runServer(
func(acctID int64) string { func(acctID int64) string {
return fmt.Sprintf("/oauth/account/%d/auth_code", acctID) return fmt.Sprintf("/oauth/account/%d/auth_code", acctID)
}, },
etsyAPIKeystring, cfg.EtsyAPIKeystring,
etsyAPISharedSecret, cfg.EtsyAPISharedSecret,
connPool, connPool,
), ),
auth, auth,
cfg.DevAuthEnabled,
) )
srv := &http.Server{ srv := &http.Server{
+2
View File
@@ -27,11 +27,13 @@ func Routes(
unp *sse.UpdateNotificationPublisher, unp *sse.UpdateNotificationPublisher,
rawEvents *raw_events.Store, rawEvents *raw_events.Store,
etsy *etsy_platform.Platform, etsy *etsy_platform.Platform,
devAuthEnabled bool,
) { ) {
auth_api.Routes( auth_api.Routes(
r.Group("/auth"), r.Group("/auth"),
logger.WithGroup("/auth"), logger.WithGroup("/auth"),
auth.GetAuthenticator(), auth.GetAuthenticator(),
devAuthEnabled,
) )
sse_api.Routes( sse_api.Routes(
r.Group("/events", auth.Authenticate()), r.Group("/events", auth.Authenticate()),
+43
View File
@@ -21,6 +21,7 @@ func Routes(
r *gin.RouterGroup, r *gin.RouterGroup,
logger *logging.Logger, logger *logging.Logger,
auth *authentication.Authenticator, auth *authentication.Authenticator,
devAuthEnabled bool,
) { ) {
ls := &loginSubrouter{ ls := &loginSubrouter{
log: logger, log: logger,
@@ -30,6 +31,11 @@ func Routes(
r.GET("/login", response.Handler(ls.loginPage)) r.GET("/login", response.Handler(ls.loginPage))
r.GET("/login/callback", response.Handler(ls.loginCallback)) r.GET("/login/callback", response.Handler(ls.loginCallback))
r.GET("/logout", response.Handler(ls.logoutPage)) r.GET("/logout", response.Handler(ls.logoutPage))
if devAuthEnabled {
logger.Warn("DEV_AUTH_ENABLED is set: /api/auth/dev-login is live and lets any caller authenticate as any user_id with no credentials. Never enable this outside local development.")
r.GET("/dev-login", response.Handler(ls.devLoginPage))
}
} }
func (s *loginSubrouter) loginPage(c *gin.Context) (response.Response, error) { func (s *loginSubrouter) loginPage(c *gin.Context) (response.Response, error) {
@@ -75,6 +81,43 @@ func (s *loginSubrouter) loginCallback(c *gin.Context) (response.Response, error
Cookie(cookies.AccessToken(accessToken, expiration)), nil Cookie(cookies.AccessToken(accessToken, expiration)), nil
} }
// devLoginPage mints a local session for a user_id, skipping the real Auth0
// OAuth round-trip. Only registered when devAuthEnabled is passed to Routes.
//
// Query params:
// - user_id: identity to log in as (default "dev-user"); use different
// values to test multiple accounts side by side.
// - name: display name for the identity (default derived from user_id).
// - target: where to redirect after login (default "/").
func (s *loginSubrouter) devLoginPage(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
q := r.URL.Query()
userID := q.Get("user_id")
if userID == "" {
userID = "dev-user"
}
name := q.Get("name")
if name == "" {
name = "Dev User (" + userID + ")"
}
targetURI := q.Get("target")
if targetURI == "" {
targetURI = "/"
}
accessToken, expiration, err := s.auth.DevLogin(ctx, userID, name)
if err != nil {
return nil, response.Errorf("failed to create dev session: %w", err)
}
return response.TemporaryRedirect(targetURI).
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
func (s *loginSubrouter) logoutPage(c *gin.Context) (response.Response, error) { func (s *loginSubrouter) logoutPage(c *gin.Context) (response.Response, error) {
r := c.Request r := c.Request
+2
View File
@@ -34,6 +34,7 @@ func NewRouter(
reps *reports.Store, reps *reports.Store,
etsy *etsy_platform.Platform, etsy *etsy_platform.Platform,
authr *authentication.Authenticator, authr *authentication.Authenticator,
devAuthEnabled bool,
) *Router { ) *Router {
authM := auth.NewService( authM := auth.NewService(
logger.WithGroup("auth-middleware"), logger.WithGroup("auth-middleware"),
@@ -99,6 +100,7 @@ func NewRouter(
unp, unp,
rawEvents, rawEvents,
etsy, etsy,
devAuthEnabled,
) )
return &Router{ return &Router{
-2
View File
@@ -137,8 +137,6 @@ func (p *UpdateNotificationPublisher) Push(ctx context.Context, acctID int64, ev
wg.Wait() wg.Wait()
return errors.Join(errs...) return errors.Join(errs...)
return nil
} }
func getPathSegments(p string) []string { func getPathSegments(p string) []string {
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
<html><body><svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100" style="background: pink" viewBox="0 0 100 100" preserveAspectRatio="none"><g transform="translate(5 5) scale(0.9 0.9) scale(1 -1) translate(0 -100)"><polyline points="0,2.5 20,5 40,10 60,20 80,40 100,80" stroke-width="2px" vector-effect="non-scaling-stroke" stroke="purple" fill="none"/><ellipse cx="0" cy="2.5" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="0" y="2.5" transform="translate(0 2.5) scale(1 -1) translate(-0 -2.5)" font-size="5px"></text><ellipse cx="20" cy="5" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="20" y="5" transform="translate(20 5) scale(1 -1) translate(-20 -5)" font-size="5px"></text><ellipse cx="40" cy="10" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="40" y="10" transform="translate(40 10) scale(1 -1) translate(-40 -10)" font-size="5px"></text><ellipse cx="60" cy="20" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="60" y="20" transform="translate(60 20) scale(1 -1) translate(-60 -20)" font-size="5px"></text><ellipse cx="80" cy="40" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="80" y="40" transform="translate(80 40) scale(1 -1) translate(-80 -40)" font-size="5px"></text><ellipse cx="100" cy="80" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="100" y="80" transform="translate(100 80) scale(1 -1) translate(-100 -80)" font-size="5px"></text></g></svg></body></html>
+1 -1
View File
@@ -36,5 +36,5 @@ func ExampleLineChart() {
output := []byte(`<html><body>` + c + `</body></html>`) output := []byte(`<html><body>` + c + `</body></html>`)
os.WriteFile("./line_chart.html", []byte(output), 0666) os.WriteFile("./line_chart.html", []byte(output), 0666)
fmt.Println(c) fmt.Println(c)
// Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"></svg> // Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100" style="background: pink" viewBox="0 0 100 100" preserveAspectRatio="none"><g transform="translate(5 5) scale(0.9 0.9) scale(1 -1) translate(0 -100)"><polyline points="0,2.5 20,5 40,10 60,20 80,40 100,80" stroke-width="2px" vector-effect="non-scaling-stroke" stroke="purple" fill="none"/><ellipse cx="0" cy="2.5" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="0" y="2.5" transform="translate(0 2.5) scale(1 -1) translate(-0 -2.5)" font-size="5px"></text><ellipse cx="20" cy="5" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="20" y="5" transform="translate(20 5) scale(1 -1) translate(-20 -5)" font-size="5px"></text><ellipse cx="40" cy="10" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="40" y="10" transform="translate(40 10) scale(1 -1) translate(-40 -10)" font-size="5px"></text><ellipse cx="60" cy="20" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="60" y="20" transform="translate(60 20) scale(1 -1) translate(-60 -20)" font-size="5px"></text><ellipse cx="80" cy="40" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="80" y="40" transform="translate(80 40) scale(1 -1) translate(-80 -40)" font-size="5px"></text><ellipse cx="100" cy="80" rx="0.16666666666666666%" ry="0.16666666666666666%" stroke="purple" stroke-width="2px" vector-effect="non-scaling-stroke" fill="pink"/><text text-anchor="middle" dominant-baseline="middle" x="100" y="80" transform="translate(100 80) scale(1 -1) translate(-100 -80)" font-size="5px"></text></g></svg>
} }
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -20,7 +20,7 @@
w-fit w-fit
" "
> >
Mock Mode Playground Mode
<input <input
type="checkbox" type="checkbox"
{{- if $mockMode }} {{- if $mockMode }}
@@ -0,0 +1,24 @@
# Work Summary — 2026-08-03 20:09
## Task
Move hardcoded secrets (Auth0 client secret, Etsy API credentials, Postgres DSN) out of source and into environment configuration, as the first step toward a faster local dev/test loop.
## Context
Audited the repo for iteration/testing friction. Found no `.env`/config layer at all — Auth0 client secret, Etsy API keystring/shared secret, and the Postgres connection string (including its password) were literal constants in `domains/authentication/auth.go`, `main.go`, and `database.go`, committed to git. This also blocked adding a dev-only auth bypass cleanly, since `authentication.New` had no way to accept alternate config.
## Changes
- Added `config/config.go`: loads `.env` via `github.com/joho/godotenv`, reads required env vars, fails fast with a clear error naming any that are missing.
- Added `.env` (gitignored, holds real local values so nothing broke) and `.env.example` (committed template).
- `.gitignore`: added `.env`.
- `database.go`: `newPool` now takes `databaseURL` as a parameter instead of a hardcoded DSN.
- `domains/authentication/auth.go`: removed the `AUTH0_*` constants; `authentication.New` now takes `domain, clientID, clientSecret, callbackURL` as parameters; `Authenticator` gained a `domain` field used by `GetLogoutURL`.
- `main.go`: calls `config.Load()` up front and threads values into `newPool`, `authentication.New`, and the Etsy `NewPlatform` call (replacing the hardcoded `etsyAPIKeystring`/`etsyAPISharedSecret` consts).
- `go.mod`/`go.sum`: added `github.com/joho/godotenv`; `go mod tidy` also dropped a few unrelated stale indirect deps.
## Verification
- `go build ./...` — clean.
- `go run .` — boots against `.env`, registers all routes identically to before the change.
## Follow-ups / not done here
- Secrets are still present in old git history (pre-existing commits) — not rotated or scrubbed. Worth rotating the Auth0 client secret and Etsy credentials at some point since repo history still exposes them.
- Next planned step: add an env-gated dev-only auth bypass (mint a local session without going through real Auth0), now that config is externalized enough to support it cleanly.
@@ -0,0 +1,25 @@
# Work Summary — 2026-08-03 23:42
## Task
Add a dev-only auth bypass so local testing doesn't require a real Auth0 login round-trip, per the dev-iteration plan from the previous session (see `work-summary-Claude-2026-08-03-2009.md`).
## Context
The only way to get an authenticated session locally was to log into the real Auth0 tenant in a browser and copy the resulting `access_token` JWT cookie into `curl` commands by hand — evidenced by several one-off curl-with-pasted-cookie entries in `.claude/settings.local.json`. Auth is driven entirely by DB state: `Identify` middleware (`server/auth/auth.go`) looks up `access_token` in `oauth_tokens`, joined to `oauth_users` and `accounts` — there's no in-process session logic to fake, just rows to write.
Also discovered along the way: the `database_migrations/` `.sql` files are stale relative to the live schema — migration `000016` (still on disk) references a `claims JSONB NOT NULL` column and a `TEXT`-typed `id_token_custom_claims_updated_at`, but the live `oauth_tokens` table has no `claims` column at all and that column is actually `timestamptz`. Something changed the schema by hand at some point without updating the migration files. Didn't touch this — just noted it and built against the real live schema (confirmed via `psql \d oauth_tokens`).
## Changes
- `config/config.go`: added optional `DevAuthEnabled bool`, parsed from `DEV_AUTH_ENABLED` (`strconv.ParseBool`; unset = false; invalid value = fail fast).
- `domains/authentication/dev.go` (new): `Authenticator.DevLogin(ctx, userID, name)` generates a random `dev_`-prefixed token and writes real `oauth_users`/`oauth_tokens` rows (expiry set 1 year out, specifically to stay clear of the near-expiry auto-refresh path in `server/auth`, since a dev token has no real Auth0 refresh token behind it). Reuses the exact same tables real login writes to, so every downstream code path (identity lookup, account linking/creation, cookie handling) treats it identically to a real session — no special-cased "is this dev" branches anywhere else in the app.
- `server/api/auth/router.go`: `Routes` takes a new `devAuthEnabled bool`. When true, logs a loud startup warning and registers `GET /api/auth/dev-login?user_id=...&name=...&target=...` (all params optional; different `user_id` values let you test multiple accounts side by side).
- Threaded `devAuthEnabled` through `server/api/apis.go``server/server.go` (`NewRouter`) → `main.go` (`runServer`), sourced from `cfg.DevAuthEnabled`.
- `.env` / `.env.example`: added `DEV_AUTH_ENABLED` (true in local `.env`, documented in `.env.example`).
## Verification
- `go build ./...` clean.
- Full manual end-to-end run against the real local Postgres: hit `/api/auth/dev-login?user_id=dev-smoke-test&target=/ui`, confirmed `Set-Cookie` on the response, followed up with `/ui` using that cookie and got a 200 with account-appropriate content (the "no account yet" state, matching what a real first-time login produces). Verified `oauth_users`/`oauth_tokens` rows landed correctly via `psql`, then deleted the test rows.
- `go vet ./...` shows only two pre-existing unreachable-code warnings unrelated to this change (`domains/accounts/accounts.go:1472`, `server/sse/publisher.go:141`).
## Follow-ups / not done here
- `database_migrations/*.sql` are out of sync with the live DB schema (see Context above) — worth reconciling at some point (either a migration that documents the drift, or regenerating migrations from the live schema) so `go:generate`'d diagrams and any fresh-DB setup aren't misleading.
- Next planned step per the dev-iteration plan: a dev script/Makefile to bring up Postgres, run migrations, build Tailwind, and start the server in one command.
@@ -0,0 +1,25 @@
# Work Summary — 2026-08-03 23:58
## Task
Add a `Makefile` with a `make dev` target: apply pending DB migrations, start the Tailwind watcher, and run the server, all from one command.
## Context
User pushed back on the initial framing ("I can already start the server with `go run .`") — fair, since that part was never the friction. The real friction, confirmed via `.zsh_history`, is the *other* two steps done by hand around it: the `migrate` CLI invoked with its full DSN spelled out literally ~10 times over recent weeks (`migrate -path database_migrations -database "postgres://app_client:app_password@localhost:5432/inventory_2?sslmode=disable" up 1`), and `tailwind.sh` (which already runs `--watch`) needing to be started manually in a separate terminal — easy to forget, leading to template/CSS changes silently not showing up. Framed the Makefile's value around removing those two specific manual steps, not around wrapping `go run .`.
## Changes
- New `Makefile` at repo root:
- `-include .env` + `export` so `DATABASE_URL` (and everything else in `.env`) is available to recipes without hand-typing it — fixes the DSN-drift risk where the hand-typed migrate command could silently diverge from what's actually in `.env`.
- `dev` (default goal): depends on `migrate-up`, then backgrounds `./tailwind.sh`, captures its PID, sets a trap to kill it on `EXIT`/`INT`/`TERM`, then runs `go run .` in the foreground. Ctrl-C (or any termination) stops both cleanly.
- `migrate-up` / `migrate-down` / `migrate-version`: thin wrappers around the `migrate` CLI using `$(DATABASE_URL)`, so the DSN is typed once (in `.env`) instead of per-invocation.
- `tailwind`: one-shot alias for `./tailwind.sh` (still watch mode, matching existing script behavior).
- `run`: plain `go run .`, for when you don't want migrations/CSS touched.
## Verification
- `make migrate-version` / `make migrate-up` — ran cleanly against the real local DB (idempotent: reported "no change" since already at the latest migration).
- `make dev` under a `timeout` — confirmed via log output that migrations ran, Tailwind's watcher started (`tailwindcss v4.1.18` banner), the server bound and served a 200 on `/ui`, and on SIGTERM both the server and the Tailwind watcher shut down (verified no leftover `tailwindcss`/`npx` process survived — first check was a `pgrep` self-match false positive on the search string appearing in the invoking shell's own command line, re-verified cleanly with `ps aux`).
## Incident during verification (self-caused, fixed)
Killing `make dev` mid-run once truncated the *committed* `styles/index.css` to empty (1370 lines → 0) — the Tailwind watcher was killed while mid-write on its first build. Caught it in the post-test `git status`/`git diff` review before finishing; restored via `git checkout -- styles/index.css`. Worth knowing for next time: killing the watcher while it's actively writing that file is a real (if narrow) way to corrupt a tracked file — not something `make dev` itself introduces (same risk exists running `tailwind.sh` directly), but noting it here since it's the kind of thing to double check after using this target.
## Follow-ups / not done here
- Next planned step per the dev-iteration plan: broaden test coverage beyond `server/ui/charts/*_test.go` (nothing currently covers `domains/accounts`, `domains/reports`, `domains/raw_events`, or the Amazon mock pipeline).
@@ -0,0 +1,34 @@
# Work Summary — 2026-08-04 19:14
## Task
Broaden test coverage beyond `server/ui/charts/*_test.go` (which was the only tested package). Final step in the dev-iteration plan.
## Context
Surveyed `domains/accounts`, `domains/reports`, `domains/raw_events`, `domains/amazon` and found the codebase is almost entirely thin `Store` methods wrapping SQL - there's very little pure logic to unit test in isolation. The real risk lives in the queries themselves (already proved this by finding a live bug: `RefreshAccessToken` in `domains/authentication/auth.go:270` passes the empty named return `accessToken` instead of the `oldAccessToken` parameter to `getRefreshTokenForAccessToken` - token refresh is silently broken. Not fixed here, flagging for a decision - see Follow-ups.).
Asked the user how to test DB-heavy code; they chose a dedicated test database with the explicit ability to point the same suite at the real dev DB when wanted.
## Infra changes
- Created `inventory_2_test` Postgres database (owned by `app_client`, matching `inventory_2`'s setup). The `angel` OS-peer-auth Postgres role has `CREATEDB`; `app_client` does not, so this had to be created out-of-band, not from app code.
- **Found and fixed a real migration bug while doing this**: `database_migrations/000010_oauth_login_states.up.sql` had invalid SQL (`NOW() + 10 'minute'`) that fails on any fresh database - a hard blocker for setting up the test DB, and for anyone else spinning up this project from scratch. Fixed to `NOW() + '10 minutes'::interval`, matching what the live dev DB actually runs (confirmed via `psql \d`).
- **Found and fixed schema drift**: migrating fresh revealed `oauth_tokens` in the migration files still has a `claims JSONB NOT NULL` column and a `TEXT`-typed `id_token_custom_claims_updated_at` - neither matches the live dev DB (no `claims` column at all; that column is `timestamptz`), and no application code reads/writes `claims`. Someone patched the dev DB by hand at some point without ever committing the migration. Added `database_migrations/000030_fix_oauth_tokens_schema_drift.{up,down}.sql` to close the gap, then `migrate force 30` on the *dev* DB (schema already matched, just needed the migration bookkeeping to catch up) and a normal `migrate up` on the new test DB. Verified both DBs now have an identical `oauth_tokens` shape (column ordering differs cosmetically, doesn't matter - the app scans by column name).
- New `internal/testdb` package: `Pool(t)` connects via `TEST_DATABASE_URL` (skips the test if unset, so `go test ./...` doesn't hard-require Postgres), `Logger()` for a discard-output logger, `NewUserID(t)` for collision-safe fixture IDs, `SeedOAuthUser`/`SeedOAuthSession` for tests that need a valid `oauth_users`/`oauth_tokens` row. All seed helpers register `t.Cleanup` in FK-safe order.
- `Makefile`: added `test` (migrates the test DB, then `go test ./...`), `test-against-dev-db` (same suite, `TEST_DATABASE_URL` overridden to `DATABASE_URL` for one run - the "option to run against the real database" the user asked for), and `migrate-test-up`/`down`/`version`.
- `.env` / `.env.example`: added `TEST_DATABASE_URL`.
## Test coverage added
- `domains/authentication/dev_test.go`: `DevLogin` round-trips through `GetAccessTokenClaimsAndExpiration` correctly, is safe to call twice for the same `user_id` (mints a new token each time), and `GetAccessTokenClaimsAndExpiration` returns `ErrNotFound` for an unknown token.
- `domains/accounts/accounts_test.go`: `CreateAccount` happy path + round-trip through `GetAccount`; duplicate `user_id` returns `ErrConflict`; `GetAccount` on a missing ID returns `ErrNotFound`; `GetUserAndAccountByAccessToken` correctly resolves the user-but-no-account state and the user-with-account state (the same join logic the auth middleware depends on for every request).
- `domains/raw_events/events_test.go`: `Save` + `LoadEventsForStore` round-trip, ordering (newest first), and the empty-store case.
## Verification
- `make test`: all new tests pass against `inventory_2_test`. `make test-against-dev-db`: same suite, same results, against the real dev DB - confirmed zero leftover rows afterward (`SELECT count(*) FROM oauth_users WHERE user_id LIKE 'test-%'` → 0, same for `raw_store_events`).
- `go build ./...` and `go vet ./...` clean except two pre-existing unreachable-code warnings unrelated to this work.
- The only test failures are `ExampleBar`/`ExampleLineChart` in `server/ui/charts` - confirmed pre-existing and unrelated (verified via `git stash` that they fail identically on the pre-session code; see Incident below). Their hardcoded `// Output:` expectations are stale relative to the current chart-rendering code (missing padding/border/label styling that's since been added).
## Incident during verification (self-caused, recovered cleanly)
Used `git stash` / `git stash pop` to check whether the chart test failures were pre-existing, and the pop conflicted on `server/ui/charts/test.svg` (a file the chart tests overwrite as a side effect of running - it was already dirty before this session even started). Resolved by discarding the working-tree's post-test-run `test.svg` (disposable scratch content) and re-running `git stash pop`, which then applied cleanly. Verified afterward with `go build ./...` and a full `git status` review that every change from this session (and the prior two) was intact. No work was lost, but noting it: don't reach for `git stash` casually in a working tree with substantial uncommitted work when a narrower check (e.g. `git worktree` or just re-reading the file) would do.
## Follow-ups / not done here
- **`RefreshAccessToken` bug** (`domains/authentication/auth.go:270`): passes the empty named-return `accessToken` instead of the `oldAccessToken` parameter to `getRefreshTokenForAccessToken`, so token refresh looks up an empty string instead of the real token and will always fail to find a refresh token. This means the "refresh when access token is old enough" path in `server/auth/auth.go` (`AuthenticateHandler`) is currently broken for real (non-dev) sessions - users would get bounced to `/` with a "failed to refresh access token" body once their 48-hour ID token crosses the refresh floor, instead of transparently refreshing. Did not fix it in this pass since it's a production auth-flow behavior change, not a test-coverage change - flagging for an explicit decision. It's a one-line fix (`accessToken``oldAccessToken` on that call).
- No coverage added for `domains/reports` or `domains/amazon` - `reports.GetRawShopEvents` needs a mock-shop fixture (depends on `accts.GetMockShop`, which lives in the much larger `mocks.go`/`sync_groups.go` surface) and `domains/amazon`'s mock event pipeline is stateful/background-process-shaped, both meaningfully bigger lifts than what fit in this pass.
@@ -0,0 +1,20 @@
# Work Summary — 2026-08-04 19:17
## Task
Fix the `RefreshAccessToken` bug surfaced while adding test coverage (see `work-summaries/work-summary-Claude-2026-08-04-1914.md`), per explicit go-ahead.
## Change
`domains/authentication/auth.go:257`: `RefreshAccessToken` was calling `a.getRefreshTokenForAccessToken(ctx, accessToken)`, where `accessToken` is the function's *empty named return value*, not the `oldAccessToken` parameter it clearly meant to use (both are `string`, so the compiler had nothing to catch). This meant every real (non-dev) token refresh looked up a refresh token for `""` instead of the actual expiring token, always failed, and sent the user back to `/` with a "failed to refresh access token" error instead of transparently refreshing their session.
Fixed by passing `oldAccessToken` instead:
```go
refreshToken, tokenType, err := a.getRefreshTokenForAccessToken(ctx, oldAccessToken)
```
## Verification
- `go build ./...` clean.
- `go test ./domains/authentication/... ./domains/accounts/... ./domains/raw_events/...` — all pass, no regressions.
- No new automated regression test for this specific bug: `RefreshAccessToken` calls `a.TokenSource(...).Token()`, which makes a real network call to Auth0 to redeem the refresh token - not mockable without adding an interface seam around `oauth2.Config`/`oidc.Provider`, which is a larger refactor than this fix warranted. The underlying query helper (`getRefreshTokenForAccessToken`) is already covered indirectly via `domains/authentication/dev_test.go`.
## Follow-ups / not done here
- If this path matters enough to regression-test end-to-end, it'd need `oauth2.Config`'s token source made injectable/mockable - flagging as a possible future task, not doing it now.
@@ -0,0 +1,28 @@
# Work Summary — 2026-08-04 22:53
## Task
Investigate and, if fixable, fix the two pre-existing failing tests (`ExampleBar`, `ExampleLineChart` in `server/ui/charts`) flagged during the test-coverage work.
## Investigation
Read `bar.go`/`line.go` (the renderers) and their tests. Both the source files and their tests were introduced in a single commit, `c51ad80 "prototyped svg reports"` (`git log` shows no other commits touching either) - so this wasn't drift accumulated over time, it was the renderer being finished after (or without) the test's expected output ever being filled in, then committed as-is (the commit message itself says "prototyped").
The renderer code itself looks intentional and coherent, not buggy:
- `Bar.SVG()` draws value labels above each bar and a rotated per-bar `Label` text, plus `padding`/`border-width` styling and `rounded-lg border-border` classes - all deliberate, readable code, not something that looks like an accident.
- `LineChart.SVG()` draws a `polyline`, an `ellipse` marker at each point, and a (here, empty, since the test fixture sets no `Label`) text element per point via `upsideDownCenteredText` - also coherent.
The old `// Output:` expectations were a bare, unstyled rect list (`bar_test.go`) and a completely empty `<svg>...</svg>` shell with no children at all (`line_test.go`) - clearly placeholders from before the labeling/styling/point-rendering features existed, not a description of intended behavior that the code regressed from.
Conclusion: fixable, and the fix is "update the stale expected output to match the current, correct renderer" - not a renderer bug to chase.
## Change
Regenerated both expectations from the renderers' actual current output (captured via `go test -v`, substituted into the test files with a small Python script rather than hand-typing ~3KB single-line SVG strings, to avoid transcription errors) and replaced the `// Output:` line in each file. One line changed per file.
## Verification
- `go build ./...` clean.
- `go test ./server/ui/charts/... -v`: both `ExampleBar` and `ExampleLineChart` pass.
- `gofmt -l` on both changed files: clean.
- `make test`: full suite now exits 0 (previously failed only on these two).
- `go vet ./...`: same two pre-existing, unrelated `unreachable code` warnings as before (`domains/accounts/accounts.go:1472`, `server/sse/publisher.go:141`) - untouched by this change.
## Follow-ups / not done here
- None specific to this fix. The dev-iteration plan (secrets, dev auth, `make dev`, test coverage, refresh-token bug, and now these) is fully closed out as of this session.
@@ -0,0 +1,15 @@
# Work Summary — 2026-08-04 22:56
## Task
Address the two `go vet` "unreachable code" warnings that have been showing up alongside test runs since the test-coverage work started surfacing them.
## Changes
Both were leftover dead `return` statements after the surrounding logic was later changed to already return on every path - simple deletions, no behavior change:
- `domains/accounts/accounts.go:1472` (in `SetListingInListingInMockSyncGroupBeingEdited`'s per-schema update loop): a trailing `return nil` after a `switch` whose three cases (`continue`/`return nil`/`return fmt.Errorf(...)`) already cover every value of `RowsAffected()`. Removed the dead line.
- `server/sse/publisher.go:141` (`Push`): a `return nil` sitting after `return errors.Join(errs...)`, which already unconditionally returns. Removed the dead line - `errors.Join(errs...)` was the intended return value all along (returns `nil` itself when `errs` has no non-nil entries, so behavior is unchanged).
## Verification
- `go build ./...` clean.
- `go vet ./...` now fully clean (previously exactly these two warnings).
- `make test`: full suite still green, no regressions.
@@ -0,0 +1,42 @@
# Work Summary — 2026-08-04 23:18
## Task
Build test coverage for `domains/amazon`, the mock Amazon event processor - explicitly framed by the user as wanting tests around the *stateful background-processing* part specifically, both for long-term regression protection and to surface design insights, not just a coverage checkbox.
## What the code actually does
Traced the full pipeline before writing anything, since `domains/amazon/mock.go` is the consumer end of a chain that starts in SQL:
1. A row is inserted into `mock.raw_shop_events` (platform-agnostic staging table; this is what the "simulate a sale/refund/inventory change" UI writes to, via `domains/accounts/accounts.go`).
2. A Postgres trigger (`amazon_store_events`, `WHEN NEW.platform = 'amazon'`, defined across migrations `000026`/`000028`) copies the row into `mock.shop_amazon_events` and fires `pg_notify('mock_shop_amazon_event_inserted', null)`.
3. `(*Mocks).ProcessEvents` - the code under test - registers `LISTEN mock_shop_amazon_event_inserted` on a dedicated pooled connection, then loops: drain all unprocessed rows from `mock.shop_amazon_events` (paginated 100 at a time via `processUnprocessedEvents`), then block on whichever comes first: a notification, an error, context cancellation, or a 1-minute timeout (poll fallback). Each processed event marks the row `processed=true` and fires an async, fire-and-forget notification to an app-level `MockEventListener` (wired to SSE in `main.go`, so the UI updates live).
## Test approach
Rather than seeding `mock.shop_amazon_events` directly, tests insert into `mock.raw_shop_events` (the real entry point) and let the trigger do its job - so the tests exercise the exact same DB-side path production traffic does, not a hand-rolled approximation of it.
New file: `domains/amazon/mock_test.go`.
- `notifySpy`: a `MockEventListener` that records calls and exposes a channel-based `waitForCount`, since the production code notifies from an un-awaited goroutine - there's no way to assert on it without a synchronization point.
- `TestProcessUnprocessedEvents_ProcessesAllEventsAcrossBatches`: inserts 150 events (the batch loop's `LIMIT` is 100) and confirms every single one gets processed and notified - this is a real correctness test of the pagination loop, not just "does one row work."
- `TestProcessUnprocessedEvents_NoListenerConfigured`: confirms processing doesn't depend on / crash without a listener being set.
- `TestProcessEvents_ReactsToNotification`: the main event - runs the actual `ProcessEvents` loop in a goroutine, inserts a real event, and waits for the full reactive path (LISTEN → trigger → NOTIFY → WaitForNotification → reprocess → listener callback) to complete, then cancels and confirms clean shutdown.
- `TestProcessEvents_ShutsDownOnContextCancel`: isolates lifecycle/shutdown behavior from NOTIFY timing entirely, so a shutdown regression doesn't hide behind notification flakiness or vice versa.
## Verification
- `go build ./...` / `go vet ./...` clean.
- `go test ./domains/amazon/... -v`: all 4 pass; also ran 10x in a row (`TestProcessEvents_ReactsToNotification` specifically, since it's the timing-sensitive one) with consistent ~0.25s runs, no flakes.
- `go test ./domains/amazon/... -race`: clean, no data races detected under this exercise.
- `make test` and `make test-against-dev-db`: full suite green both ways; confirmed zero leftover rows in the dev DB afterward.
## Design insights surfaced while building these tests
This is the part the user specifically asked for - not fixed, just documented, since these are real behavior/architecture calls, not bugs:
1. **No "now listening" readiness signal.** `listenForNotifications` issues `LISTEN` synchronously, but from outside `ProcessEvents` there's no way to know when that's happened - a caller (or a test) that inserts an event immediately after starting `ProcessEvents` in a goroutine is racing against Go's scheduler getting around to running it. The test above works around this with a flat 200ms sleep before inserting, which is reliable in practice (10/10 clean runs) but is inherently a "hope the goroutine got scheduled by then" workaround, not a real guarantee. If this loop is ever driven by something less forgiving than a local dev machine (heavier load, slower CI), a small readiness channel returned from `listenForNotifications` (closed once `LISTEN` succeeds) would remove the guesswork entirely, for tests and for any other caller that cares about "is it actually listening yet."
2. **Fire-and-forget listener notification.** `processEvent` spawns `go func() { listener.Notify(ctx, e) }()` and only logs a failure - the event is marked `processed = true` in the DB regardless of whether the SSE listener actually received it. That's a reasonable tradeoff (a flaky UI push shouldn't block or retry core event processing), but it does mean there's currently no compensating mechanism if a notification is dropped - the UI just silently misses that one live update, permanently, with no re-send. Worth a deliberate decision on whether that's acceptable long-term, since it isn't caught by anything short of a user noticing stale data.
3. **The 1-minute poll fallback is a hardcoded constant** (`time.After(time.Minute)` inline in `ProcessEvents`), not a field/parameter. That's fine for production but means the fallback-poll path itself is essentially untestable without either waiting a full minute per test run or refactoring the interval to be injectable - the tests above only exercise the NOTIFY-driven reactive path, not the poll fallback, for exactly this reason. If the fallback path's correctness ever needs its own regression test, that constant would need to become configurable first.
4. **A LISTEN-connection error is fatal to the whole application, not just this subsystem.** If `WaitForNotification` ever errors for a reason other than context cancellation (dropped connection, Postgres restart, pool churn), `listenForNotifications`'s goroutine sends that error on `errCh`, `ProcessEvents` returns it, and in `main.go`, `runApp`'s top-level `select` treats `eventErrCh` firing as cause to call `shutdown()` - which cancels the shared context and tears down the HTTP server too. There's no reconnect/retry loop around the LISTEN connection specifically. In other words: a transient hiccup on one background Postgres connection currently brings down the entire server, not just the mock Amazon event processor. This is probably the single most consequential finding here if this pattern gets reused for the other nine `mock_shop_*_event_inserted` channels already defined in the migrations (`big_cartel`, `ebay`, `ecwid`, `etsy`, `shopify`, `square_online`, `squarespace`, `tiktok`, `walmart_marketplace`, `wix`, `woo_commerce`, `zoho` all have the same trigger+notify shape already migrated, just no Go-side processor yet) - multiplying this fragility by twelve without addressing it first would mean any one platform's listen-connection blip can take the whole app down.
## Follow-ups / not done here
- None of the four insights above were acted on - flagging for a decision, same pattern as the `RefreshAccessToken` bug from the previous session. #4 in particular seems worth prioritizing before this pattern is replicated across the other mock platforms, given the "reports potentially later" plan implies more processors like this one are coming.
- `domains/reports` still has no coverage (explicitly deferred by the user to later).