194 lines
5.1 KiB
Go
194 lines
5.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
// setupTestDB handles connection pooling, migrations, and cleanup per test run.
|
|
func setupTestDB(t *testing.T) (*sql.DB, *Queries) {
|
|
t.Helper()
|
|
|
|
dsn := os.Getenv("TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("TEST_DATABASE_URL not set; skipping store integration test")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
conn, err := sql.Open("postgres", dsn)
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
|
|
if err := conn.PingContext(ctx); err != nil {
|
|
_ = conn.Close()
|
|
t.Fatalf("cannot reach test database: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
_ = conn.Close()
|
|
})
|
|
|
|
if err := Migrate(ctx, conn, "sql/schema"); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
return conn, New(conn)
|
|
}
|
|
|
|
func makeEmbedding(dim int, val string) string {
|
|
return "[" + strings.TrimSuffix(strings.Repeat(val+",", dim), ",") + "]"
|
|
}
|
|
|
|
func TestStoreRoundTrip(t *testing.T) {
|
|
conn, q := setupTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
uid := fmt.Sprintf("test_%d", time.Now().UnixNano())
|
|
platform := "Bluesky"
|
|
emb1 := makeEmbedding(1024, "0.1")
|
|
|
|
// Clean up after the test so leftover records do not pollute subsequent runs
|
|
t.Cleanup(func() {
|
|
_, _ = conn.ExecContext(context.Background(), "DELETE FROM style_profiles WHERE user_id = $1", uid)
|
|
})
|
|
|
|
// 1. Test INSERT path of Upsert
|
|
err := q.UpsertStyleProfile(ctx, UpsertStyleProfileParams{
|
|
UserID: uid,
|
|
Platform: platform,
|
|
Embedding: emb1,
|
|
TopKeywords: []string{"alpha", "beta"},
|
|
StyleSummary: "original summary",
|
|
PostCount: 5,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("initial upsert failed: %v", err)
|
|
}
|
|
|
|
// 2. Test GET and verify fields
|
|
p, err := q.GetStyleProfile(ctx, GetStyleProfileParams{UserID: uid, Platform: platform})
|
|
if err != nil {
|
|
t.Fatalf("get failed: %v", err)
|
|
}
|
|
|
|
if p.UserID != uid || p.Platform != platform || p.PostCount != 5 {
|
|
t.Errorf("mismatched basic fields: %+v", p)
|
|
}
|
|
if p.StyleSummary != "original summary" {
|
|
t.Errorf("got summary %q, want %q", p.StyleSummary, "original summary")
|
|
}
|
|
if !reflect.DeepEqual(p.TopKeywords, []string{"alpha", "beta"}) {
|
|
t.Errorf("got keywords %v, want %v", p.TopKeywords, []string{"alpha", "beta"})
|
|
}
|
|
|
|
// 3. Test UPDATE path of Upsert (Verify conflict resolution works)
|
|
emb2 := makeEmbedding(1024, "0.2")
|
|
err = q.UpsertStyleProfile(ctx, UpsertStyleProfileParams{
|
|
UserID: uid,
|
|
Platform: platform,
|
|
Embedding: emb2,
|
|
TopKeywords: []string{"gamma"},
|
|
StyleSummary: "updated summary",
|
|
PostCount: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("secondary upsert (update) failed: %v", err)
|
|
}
|
|
|
|
updated, err := q.GetStyleProfile(ctx, GetStyleProfileParams{UserID: uid, Platform: platform})
|
|
if err != nil {
|
|
t.Fatalf("get updated profile failed: %v", err)
|
|
}
|
|
if updated.PostCount != 10 || updated.StyleSummary != "updated summary" {
|
|
t.Errorf("upsert failed to update existing record: %+v", updated)
|
|
}
|
|
|
|
// 4. Test pgvector similarity search
|
|
rows, err := q.FindSimilarUsers(ctx, FindSimilarUsersParams{
|
|
QueryEmbedding: emb2,
|
|
LimitVal: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("find similar failed: %v", err)
|
|
}
|
|
|
|
found := false
|
|
for _, r := range rows {
|
|
if r.UserID == uid {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected user %s in similarity results, but was not found", uid)
|
|
}
|
|
}
|
|
|
|
func TestStoreSaveTransaction(t *testing.T) {
|
|
conn, q := setupTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
txHash := fmt.Sprintf("0x_%d", time.Now().UnixNano())
|
|
amount := "10.0"
|
|
asset := "ETH"
|
|
network := "eip155:1"
|
|
recipient := "0x1234567890abcdef"
|
|
sender := "0xabcdef1234567890"
|
|
|
|
t.Cleanup(func() {
|
|
_, _ = conn.ExecContext(context.Background(), "DELETE FROM transactions WHERE x402_transaction_hash = $1", txHash)
|
|
})
|
|
|
|
ret, err := q.SaveTransaction(ctx, SaveTransactionParams{
|
|
X402TransactionHash: txHash,
|
|
Amount: amount,
|
|
Asset: asset,
|
|
Network: network,
|
|
Recipient: recipient,
|
|
Sender: sender,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SaveTransaction error: %v", err)
|
|
}
|
|
|
|
if len(ret) != 1 {
|
|
t.Fatalf("expected 1 result row returned, got %d", len(ret))
|
|
}
|
|
|
|
saved := ret[0]
|
|
if saved.ID <= 0 {
|
|
t.Errorf("expected positive ID, got %d", saved.ID)
|
|
}
|
|
if !saved.CreatedAt.Valid {
|
|
t.Error("expected CreatedAt to be set")
|
|
}
|
|
|
|
// Since the sqlc query only returns `id` and `created_at`,
|
|
// verify directly that PostgreSQL stored the remaining columns accurately:
|
|
var (
|
|
gotHash, gotAmount, gotAsset, gotNetwork, gotRecipient, gotSender string
|
|
)
|
|
err = conn.QueryRowContext(ctx, `
|
|
SELECT x402_transaction_hash, amount, asset, network, recipient, sender
|
|
FROM transactions
|
|
WHERE id = $1
|
|
`, saved.ID).Scan(&gotHash, &gotAmount, &gotAsset, &gotNetwork, &gotRecipient, &gotSender)
|
|
if err != nil {
|
|
t.Fatalf("failed to query inserted transaction from db: %v", err)
|
|
}
|
|
|
|
if gotHash != txHash || gotAmount != amount || gotAsset != asset ||
|
|
gotNetwork != network || gotRecipient != recipient || gotSender != sender {
|
|
t.Errorf("stored values do not match input params: got (%s, %s, %s, %s, %s, %s)",
|
|
gotHash, gotAmount, gotAsset, gotNetwork, gotRecipient, gotSender)
|
|
}
|
|
}
|