mirror of
https://github.com/hanzoai/orm.git
synced 2026-10-30 15:01:22 +00:00
* feat(query): re-export dbx SQL AST under orm/query namespace
Every type is an identity alias (type X = dbx.X) and every constructor
is a thin package-level var bound to dbx. Values produced via orm/query
are bit-identical to values produced via dbx and pass through any type
boundary without conversion. Consumers can migrate from hanzoai/dbx to
hanzoai/orm/query file-by-file with zero coordination cost.
* feat: Typed[T] generic wrapper for type-safe SelectQuery binding
Typed[T] is a thin shell around *query.SelectQuery that binds result
rows to a known Go type T. All/One/First/Count terminators replace the
untyped interface{} ceremony in dbx's raw API. orm.Select[T](db, table)
is the common-case constructor; orm.NewTyped[T](sq) wraps any existing
*query.SelectQuery. The escape hatch Typed.Query() exposes the raw
SelectQuery for dbx methods not mirrored on Typed (joins, aggregates,
window functions, per-query hooks).
* test: verify re-export identity and Typed[T] round-trip
query/query_test.go proves:
- type aliases are identity (assignability both directions, reflect
type equality) so the migration is a no-op at the type system level
- Expression builders produce byte-identical SQL and bound params when
invoked via dbx or via query (NewExp, Not, And, Or, In, NotIn, Like,
NotLike, Between, NotBetween, Enclose)
- values cross the dbx / query boundary without conversion
typed_test.go proves:
- All returns []T with correct filtering / ordering
- One returns *T or ErrNotFound on empty
- First returns (T, ok, error) without treating empty as error
- Typed.Query() escape hatch works for unmirrored dbx methods
- NewTyped wraps any externally-built SelectQuery
* docs: SQL query guide + v0.5.0 changelog
docs/sql-query.md explains when to use orm.Typed[T] vs orm/query
directly, gives the two-sed migration recipe from hanzoai/dbx, and
lists what is intentionally not re-exported (the //go:build ignore
compat layer).
CHANGELOG.md flags v0.4.0 existed as a KV/entity-only release; v0.5.0
is the additive SQL slice that unblocks hanzoai/base migrating off
direct dbx imports.
* chore: bump hanzoai/dbx to v1.16.0
Pulls in SelectQuery.Copy() — the deep-copy primitive used by
Typed[T] to keep Count() non-mutating and chain methods race-safe
(Red P6-C1, P6-H1).
* fix(typed): race-safe immutable chain, non-mutating Count, ctx plumbed
Typed[T] is now effectively immutable. Every chain method (Where,
AndWhere, OrWhere, OrderBy, Limit, Offset) returns a Typed[T] wrapping
a CLONE of the underlying *SelectQuery. Sharing a *Typed[T] across
goroutines is safe — no terminator or chain method mutates the
receiver.
Concrete fixes vs Red Part 6:
- P6-C1 (CRITICAL): Count() rewrites SELECT to COUNT(*) on a clone, so
the caller's original projection is intact for subsequent
.All() / .One() calls. The pagination pattern
(tq := ...; tq.Count(); tq.All()) returns real rows, not zero-valued
garbage. Guarded by TestCountDoesNotMutateQuery.
- P6-H1 (HIGH): concurrent .All() + .Count() on a shared *Typed[T] is
race-free. Each terminator operates on a fresh clone; the receiver's
*SelectQuery is never touched by the chain. Guarded by
TestTypedConcurrentSafe under -race.
- P6-M2 (MEDIUM): every terminator threads the passed ctx via
*SelectQuery.WithContext so cancellation and deadlines reach the
driver. No API change.
- P6-M3 (MEDIUM): orm.Select[T](nil, ...) and orm.NewTyped[T](nil) no
longer panic — they return a Typed[T] carrying ErrNilDB that every
terminator surfaces as an error. Guarded by TestSelectNilDBReturnsError
and TestNewTypedNilQuery.
Typed[T].Clone() is added as a public escape hatch for callers that
reach into Typed[T].Query() and want a mutation-isolated copy.
Depends on hanzoai/dbx v1.16.0 for SelectQuery.Copy().
Test: go test -race -count=5 ./... passes clean (zero races, zero
fails).
* feat(orm): SafeHashExp validates keys against injection (P6-H2)
dbx.SqliteBuilder.QuoteSimpleColumnName passes any string containing a
backtick through unchanged, so a query.HashExp key like
id\`, (SELECT 1), \`x
builds to
id\`, (SELECT 1), \`x = {:p0}
— a SQL injection. SafeHashExp is the defense-in-depth boundary for
untrusted column identifiers: keys must match
^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$
anything else is rejected with a clear error identifying the offending
key. Values remain unvalidated because dbx parameterizes them — the
injection surface is exclusively in the keys.
MustSafeHashExp is the panicking flavor for static call sites where
invalid keys are a programming bug.
query.HashExp continues to exist unchanged for developer-controlled
keys. Application code that reads column names from HTTP query strings
or config must go through SafeHashExp.
Test: TestSafeHashExpRejectsInjection covers the 4 Red-proof payloads
plus space/NUL/newline/empty variants. TestSafeHashExpRoundTrip proves
the valid path still builds correct SQL.
* docs: v0.5.1 changelog, SafeHashExp guidance, softened dbx language (P6-H3)
- CHANGELOG.md gains a v0.5.1 section summarizing the P6-C1 / P6-H1 /
P6-H2 / P6-M2 / P6-M3 / P6-H3 fixes and the dbx v1.16.0 bump.
- CHANGELOG.md v0.5.0 "Deprecated" section is reframed as "Migration
posture": direct hanzoai/dbx imports are not deprecated and continue
to work; new code should prefer orm/query. This matches the reality
that base/feat/orm-query-seam still has 68 files importing dbx
directly and a hard deprecation would make those files instantly
non-conforming without a tracked migration PR (Red P6-H3).
- docs/sql-query.md gains a "HashExp keys — developer-controlled only"
section with the backtick-breakout attack payload and the
SafeHashExp remediation pattern. The previous blanket "do not
import hanzoai/dbx directly from application code" is softened to
"new code should import orm/query".
* test: re-register init()-bound types after resetRegistry
TestResetRegistry wipes the model registry to exercise the teardown
path, then re-registers every type it knows about. Three types were
missing: intgUser / intgHooked (adapter_test.go init()) and
UpdateHookedModel (gaps_test.go init()). Without them, running the
full suite under -count>=2 hit a "type not registered" panic in
TestSQLiteAdapterCreate and TestUpdateHookReceivesOldState.
Pre-existing bug exposed while satisfying -race -count=5 on the
P6 regression gate.
532 lines
12 KiB
Go
532 lines
12 KiB
Go
package orm
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ---- Additional tests for higher coverage ----
|
|
|
|
// Test all noop cache methods
|
|
func TestNoopCacheFull(t *testing.T) {
|
|
c := &NoopCache{}
|
|
ctx := context.Background()
|
|
|
|
if err := c.InvalidateEntity(ctx, "k", "1"); err != nil {
|
|
t.Error(err)
|
|
}
|
|
if err := c.InvalidateKind(ctx, "k"); err != nil {
|
|
t.Error(err)
|
|
}
|
|
_, ok, err := c.GetQuery(ctx, "k", "q")
|
|
if err != nil || ok {
|
|
t.Error("expected miss")
|
|
}
|
|
if err := c.SetQuery(ctx, "k", "q", []byte("x"), time.Minute); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
// Test MemoryCache Close and InvalidateKind
|
|
func TestMemoryCacheClose(t *testing.T) {
|
|
c := NewMemoryCache(100)
|
|
ctx := context.Background()
|
|
c.SetEntity(ctx, "a", "1", []byte("x"), time.Minute)
|
|
c.SetQuery(ctx, "a", "q1", []byte("y"), time.Minute)
|
|
|
|
if err := c.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Len() != 0 {
|
|
t.Errorf("expected 0 entries after close, got %d", c.Len())
|
|
}
|
|
}
|
|
|
|
func TestMemoryCacheInvalidateKind(t *testing.T) {
|
|
c := NewMemoryCache(100)
|
|
ctx := context.Background()
|
|
c.SetQuery(ctx, "product", "q1", []byte("a"), time.Minute)
|
|
c.SetQuery(ctx, "product", "q2", []byte("b"), time.Minute)
|
|
c.SetQuery(ctx, "order", "q1", []byte("c"), time.Minute)
|
|
|
|
if err := c.InvalidateKind(ctx, "product"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, ok, _ := c.GetQuery(ctx, "product", "q1")
|
|
if ok {
|
|
t.Error("expected product query to be invalidated")
|
|
}
|
|
_, ok, _ = c.GetQuery(ctx, "order", "q1")
|
|
if !ok {
|
|
t.Error("expected order query to survive")
|
|
}
|
|
}
|
|
|
|
func TestMemoryCacheDefaultSize(t *testing.T) {
|
|
c := NewMemoryCache(0) // should default to 10000
|
|
if c.maxSize != 10000 {
|
|
t.Errorf("expected maxSize 10000, got %d", c.maxSize)
|
|
}
|
|
}
|
|
|
|
// Test QueryCacheKey with namespace
|
|
func TestQueryCacheKeyWithNamespace(t *testing.T) {
|
|
key := QueryCacheKey("tenant1", "product", "abc")
|
|
if key != "orm:tenant1:product:q:abc" {
|
|
t.Errorf("unexpected key: %s", key)
|
|
}
|
|
}
|
|
|
|
// Test WithParent, WithInit, WithDefaults, WithCache options
|
|
type ParentedModel struct {
|
|
Model[ParentedModel]
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type InitModel struct {
|
|
Model[InitModel]
|
|
Custom string `json:"custom"`
|
|
}
|
|
|
|
type DefaultsModel struct {
|
|
Model[DefaultsModel]
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type CachedModel struct {
|
|
Model[CachedModel]
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
func init() {
|
|
Register[ParentedModel]("parented-model", WithParent[ParentedModel](func(db DB) Key {
|
|
return db.NewKey("synckey", "", 1, nil)
|
|
}))
|
|
|
|
Register[InitModel]("init-model", WithInit[InitModel](func(db DB, m *InitModel) {
|
|
m.Custom = "initialized"
|
|
}))
|
|
|
|
Register[DefaultsModel]("defaults-model", WithDefaults[DefaultsModel](func(m *DefaultsModel) {
|
|
if m.Value == "" {
|
|
m.Value = "custom-default"
|
|
}
|
|
}))
|
|
|
|
Register[CachedModel]("cached-model", WithCache[CachedModel](CacheConfig{
|
|
Enabled: true,
|
|
EntityTTL: 300,
|
|
QueryTTL: 60,
|
|
}))
|
|
}
|
|
|
|
func TestWithParent(t *testing.T) {
|
|
db := newMockDB()
|
|
m := New[ParentedModel](db)
|
|
// Parent is set lazily during Key() allocation
|
|
_ = m.Key()
|
|
if m.Parent() == nil {
|
|
t.Error("expected parent key to be set after Key()")
|
|
}
|
|
}
|
|
|
|
func TestWithInit(t *testing.T) {
|
|
db := newMockDB()
|
|
m := New[InitModel](db)
|
|
if m.Custom != "initialized" {
|
|
t.Errorf("expected custom='initialized', got %q", m.Custom)
|
|
}
|
|
}
|
|
|
|
func TestWithDefaults(t *testing.T) {
|
|
db := newMockDB()
|
|
m := New[DefaultsModel](db)
|
|
if m.Value != "custom-default" {
|
|
t.Errorf("expected value='custom-default', got %q", m.Value)
|
|
}
|
|
}
|
|
|
|
func TestWithCache(t *testing.T) {
|
|
meta, ok := Lookup("cached-model")
|
|
if !ok {
|
|
t.Fatal("cached-model not registered")
|
|
}
|
|
if meta.CacheConfig == nil {
|
|
t.Fatal("expected cache config")
|
|
}
|
|
if !meta.CacheConfig.Enabled {
|
|
t.Error("expected cache enabled")
|
|
}
|
|
if meta.CacheConfig.EntityTTL != 300 {
|
|
t.Errorf("expected entity TTL 300, got %d", meta.CacheConfig.EntityTTL)
|
|
}
|
|
}
|
|
|
|
// Test Model methods that were uncovered
|
|
func TestModelDB(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
if pi.DB() != db {
|
|
t.Error("expected DB() to return the mock db")
|
|
}
|
|
}
|
|
|
|
func TestModelSetDB(t *testing.T) {
|
|
db1 := newMockDB()
|
|
db2 := newMockDB()
|
|
pi := New[PaymentIntent](db1)
|
|
pi.SetDB(db2)
|
|
if pi.DB() != db2 {
|
|
t.Error("expected SetDB to change the db")
|
|
}
|
|
}
|
|
|
|
func TestModelSetId(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
pi.SetId("custom-id")
|
|
if pi.Id() != "custom-id" {
|
|
t.Errorf("expected id 'custom-id', got %q", pi.Id())
|
|
}
|
|
}
|
|
|
|
func TestModelSetParent(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
parent := db.NewKey("parent", "p1", 0, nil)
|
|
pi.SetParent(parent)
|
|
if pi.Parent() == nil {
|
|
t.Error("expected parent to be set")
|
|
}
|
|
if pi.Parent().StringID() != "p1" {
|
|
t.Errorf("expected parent stringID 'p1', got %q", pi.Parent().StringID())
|
|
}
|
|
}
|
|
|
|
func TestModelIsMock(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
if pi.IsMock() {
|
|
t.Error("expected not mock by default")
|
|
}
|
|
pi.SetMock(true)
|
|
if !pi.IsMock() {
|
|
t.Error("expected mock after SetMock(true)")
|
|
}
|
|
}
|
|
|
|
func TestModelSetKey(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
|
|
key := db.NewKey("payment-intent", "test-key", 0, nil)
|
|
pi.SetKey(key)
|
|
|
|
if pi.Key() != key {
|
|
t.Error("expected SetKey to set the key")
|
|
}
|
|
if pi.Id() != "test-key" {
|
|
t.Errorf("expected id 'test-key', got %q", pi.Id())
|
|
}
|
|
}
|
|
|
|
func TestModelSetKeyWithParent(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
|
|
parent := db.NewKey("synckey", "", 1, nil)
|
|
key := db.NewKey("payment-intent", "pk", 0, parent)
|
|
pi.SetKey(key)
|
|
|
|
if pi.Parent() == nil {
|
|
t.Error("expected parent to be set from key")
|
|
}
|
|
}
|
|
|
|
func TestModelSetKeyNil(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
pi.SetKey(nil) // should not panic
|
|
}
|
|
|
|
func TestModelExists(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
|
|
ok, err := pi.Exists()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Error("expected entity to not exist before Create")
|
|
}
|
|
|
|
pi.Create()
|
|
|
|
ok, err = pi.Exists()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok {
|
|
t.Error("expected entity to exist after Create")
|
|
}
|
|
}
|
|
|
|
func TestModelGet(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
pi.Amount = 5000
|
|
pi.Create()
|
|
|
|
key := pi.Key()
|
|
|
|
pi2 := New[PaymentIntent](db)
|
|
err := pi2.Get(key)
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestModelGetNilKey(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
pi.Create()
|
|
|
|
// Get with nil key uses existing key
|
|
err := pi.Get(nil)
|
|
if err != nil {
|
|
t.Fatalf("Get(nil) failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test setFieldFromString for various types
|
|
type AllTypesModel struct {
|
|
Model[AllTypesModel]
|
|
S string `json:"s" orm:"default:hello"`
|
|
I int `json:"i" orm:"default:42"`
|
|
I8 int8 `json:"i8" orm:"default:8"`
|
|
I16 int16 `json:"i16" orm:"default:16"`
|
|
I32 int32 `json:"i32" orm:"default:32"`
|
|
I64 int64 `json:"i64" orm:"default:64"`
|
|
U uint `json:"u" orm:"default:10"`
|
|
U8 uint8 `json:"u8" orm:"default:8"`
|
|
U16 uint16 `json:"u16" orm:"default:16"`
|
|
U32 uint32 `json:"u32" orm:"default:32"`
|
|
U64 uint64 `json:"u64" orm:"default:64"`
|
|
F32 float32 `json:"f32" orm:"default:3.14"`
|
|
F64 float64 `json:"f64" orm:"default:2.718"`
|
|
B bool `json:"b" orm:"default:true"`
|
|
D time.Duration `json:"d" orm:"default:5s"`
|
|
}
|
|
|
|
func init() {
|
|
Register[AllTypesModel]("all-types")
|
|
}
|
|
|
|
func TestAllTypesDefaults(t *testing.T) {
|
|
db := newMockDB()
|
|
m := New[AllTypesModel](db)
|
|
|
|
if m.S != "hello" {
|
|
t.Errorf("s: expected 'hello', got %q", m.S)
|
|
}
|
|
if m.I != 42 {
|
|
t.Errorf("i: expected 42, got %d", m.I)
|
|
}
|
|
if m.I8 != 8 {
|
|
t.Errorf("i8: expected 8, got %d", m.I8)
|
|
}
|
|
if m.I16 != 16 {
|
|
t.Errorf("i16: expected 16, got %d", m.I16)
|
|
}
|
|
if m.I32 != 32 {
|
|
t.Errorf("i32: expected 32, got %d", m.I32)
|
|
}
|
|
if m.I64 != 64 {
|
|
t.Errorf("i64: expected 64, got %d", m.I64)
|
|
}
|
|
if m.U != 10 {
|
|
t.Errorf("u: expected 10, got %d", m.U)
|
|
}
|
|
if m.U8 != 8 {
|
|
t.Errorf("u8: expected 8, got %d", m.U8)
|
|
}
|
|
if m.U16 != 16 {
|
|
t.Errorf("u16: expected 16, got %d", m.U16)
|
|
}
|
|
if m.U32 != 32 {
|
|
t.Errorf("u32: expected 32, got %d", m.U32)
|
|
}
|
|
if m.U64 != 64 {
|
|
t.Errorf("u64: expected 64, got %d", m.U64)
|
|
}
|
|
if m.F32 == 0 {
|
|
t.Error("f32: expected non-zero")
|
|
}
|
|
if m.F64 == 0 {
|
|
t.Error("f64: expected non-zero")
|
|
}
|
|
if !m.B {
|
|
t.Error("b: expected true")
|
|
}
|
|
if m.D != 5*time.Second {
|
|
t.Errorf("d: expected 5s, got %v", m.D)
|
|
}
|
|
}
|
|
|
|
// Test resetRegistry
|
|
func TestResetRegistry(t *testing.T) {
|
|
// Save current state
|
|
kindsBefore := Kinds()
|
|
if len(kindsBefore) == 0 {
|
|
t.Fatal("expected some registered kinds")
|
|
}
|
|
|
|
resetRegistry()
|
|
kindsAfter := Kinds()
|
|
if len(kindsAfter) != 0 {
|
|
t.Errorf("expected 0 kinds after reset, got %d", len(kindsAfter))
|
|
}
|
|
|
|
// Re-register for other tests
|
|
Register[PaymentIntent]("payment-intent")
|
|
Register[Product]("product")
|
|
Register[Order]("order")
|
|
Register[HookedModel]("hooked-model")
|
|
Register[StringKeyModel]("string-key-model", WithStringKey[StringKeyModel]())
|
|
Register[ParentedModel]("parented-model", WithParent[ParentedModel](func(db DB) Key {
|
|
return db.NewKey("synckey", "", 1, nil)
|
|
}))
|
|
Register[InitModel]("init-model", WithInit[InitModel](func(db DB, m *InitModel) {
|
|
m.Custom = "initialized"
|
|
}))
|
|
Register[DefaultsModel]("defaults-model", WithDefaults[DefaultsModel](func(m *DefaultsModel) {
|
|
if m.Value == "" {
|
|
m.Value = "custom-default"
|
|
}
|
|
}))
|
|
Register[CachedModel]("cached-model", WithCache[CachedModel](CacheConfig{
|
|
Enabled: true,
|
|
EntityTTL: 300,
|
|
QueryTTL: 60,
|
|
}))
|
|
Register[AllTypesModel]("all-types")
|
|
|
|
// Re-register types whose init() runs once — resetting the registry
|
|
// here would otherwise leave every consumer test broken on `-count>1`.
|
|
Register[intgUser]("intg-user")
|
|
Register[intgHooked]("intg-hooked")
|
|
Register[UpdateHookedModel]("update-hooked")
|
|
}
|
|
|
|
// Test duplicate registration panics
|
|
func TestDuplicateRegistration(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Error("expected panic for duplicate registration")
|
|
}
|
|
}()
|
|
Register[PaymentIntent]("payment-intent")
|
|
}
|
|
|
|
// Test LookupType
|
|
func TestLookupType(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
_ = pi
|
|
|
|
// Direct type lookup
|
|
meta, ok := Lookup("payment-intent")
|
|
if !ok {
|
|
t.Fatal("expected to find payment-intent")
|
|
}
|
|
if meta.Type.Name() != "PaymentIntent" {
|
|
t.Errorf("expected type name PaymentIntent, got %s", meta.Type.Name())
|
|
}
|
|
}
|
|
|
|
// Test Query.Ancestor, Get, ById, KeyExists, IdExists, Inner
|
|
func TestModelQueryMethods(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
q := pi.Query()
|
|
|
|
// Ancestor
|
|
parentKey := db.NewKey("parent", "p", 0, nil)
|
|
q2 := q.Ancestor(parentKey)
|
|
if q2 == nil {
|
|
t.Error("expected non-nil query after Ancestor")
|
|
}
|
|
|
|
// Inner
|
|
inner := q.Inner()
|
|
if inner == nil {
|
|
t.Error("expected non-nil inner query")
|
|
}
|
|
}
|
|
|
|
func TestModelQueryGet(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
q := pi.Query()
|
|
|
|
// Get on empty DB returns false
|
|
ok, err := q.Get()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Error("expected false for empty DB")
|
|
}
|
|
}
|
|
|
|
func TestModelQueryById(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
pi.Create()
|
|
|
|
pi2 := New[PaymentIntent](db)
|
|
q := pi2.Query()
|
|
|
|
// Query for non-existent ID
|
|
ok, err := q.ById("nonexistent")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Error("expected false for nonexistent ID")
|
|
}
|
|
}
|
|
|
|
func TestModelQueryKeyExists(t *testing.T) {
|
|
db := newMockDB()
|
|
pi := New[PaymentIntent](db)
|
|
pi.Create()
|
|
|
|
q := pi.Query()
|
|
ok, err := q.KeyExists(pi.Key())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok {
|
|
t.Error("expected key to exist")
|
|
}
|
|
}
|
|
|
|
func TestModelQueryIdExists(t *testing.T) {
|
|
db := newMockDB()
|
|
|
|
pi := New[PaymentIntent](db)
|
|
q := pi.Query()
|
|
|
|
_, ok, err := q.IdExists("nonexistent")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Error("expected id to not exist")
|
|
}
|
|
}
|