test: push coverage from 90.9% to 97.0%
Cover previously unreachable error paths in New(), GetAll(), and Render(): - New: schema conflict when index named "kv" pre-exists - GetAll: scan error via NULL-key injection after table restructure - GetAll: rows iteration error via database page corruption - Render: scan error via same NULL-key technique - Render: rows iteration error via same corruption technique GetAll and Render now at 100%. Only 3 stmts remain uncovered in New() (sql.Open lazy-error and PRAGMA busy_timeout — both genuinely unreachable without driver mocking). Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
b866dacdd2
commit
a6c99a2e0f
1 changed files with 225 additions and 0 deletions
225
coverage_test.go
Normal file
225
coverage_test.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// New — schema error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNew_Bad_SchemaConflict(t *testing.T) {
|
||||
// Pre-create a database with an INDEX named "kv". When New() runs
|
||||
// CREATE TABLE IF NOT EXISTS kv, SQLite returns an error because the
|
||||
// name "kv" is already taken by the index.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "conflict.db")
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
require.NoError(t, err)
|
||||
db.SetMaxOpenConns(1)
|
||||
_, err = db.Exec("PRAGMA journal_mode=WAL")
|
||||
require.NoError(t, err)
|
||||
_, err = db.Exec("CREATE TABLE dummy (id INTEGER)")
|
||||
require.NoError(t, err)
|
||||
_, err = db.Exec("CREATE INDEX kv ON dummy(id)")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
_, err = New(dbPath)
|
||||
require.Error(t, err, "New should fail when an index named kv already exists")
|
||||
assert.Contains(t, err.Error(), "store.New: schema")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetAll — scan error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetAll_Bad_ScanError(t *testing.T) {
|
||||
// Trigger a scan error by inserting a row with a NULL key. The production
|
||||
// code scans into plain strings, which cannot represent NULL.
|
||||
s, err := New(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
// Insert a normal row first so the query returns results.
|
||||
require.NoError(t, s.Set("g", "good", "value"))
|
||||
|
||||
// Restructure the table to allow NULLs, then insert a NULL-key row.
|
||||
_, err = s.db.Exec("ALTER TABLE kv RENAME TO kv_backup")
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec(`CREATE TABLE kv (
|
||||
grp TEXT,
|
||||
key TEXT,
|
||||
value TEXT,
|
||||
expires_at INTEGER
|
||||
)`)
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec("INSERT INTO kv SELECT * FROM kv_backup")
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec("INSERT INTO kv (grp, key, value) VALUES ('g', NULL, 'null-key-val')")
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec("DROP TABLE kv_backup")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetAll("g")
|
||||
require.Error(t, err, "GetAll should fail when a row contains a NULL key")
|
||||
assert.Contains(t, err.Error(), "store.GetAll: scan")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetAll — rows iteration error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetAll_Bad_RowsError(t *testing.T) {
|
||||
// Trigger rows.Err() by corrupting the database file so that iteration
|
||||
// starts successfully but encounters a malformed page mid-scan.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "corrupt-getall.db")
|
||||
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert enough rows to span multiple database pages.
|
||||
const rows = 5000
|
||||
for i := 0; i < rows; i++ {
|
||||
require.NoError(t, s.Set("g",
|
||||
fmt.Sprintf("key-%06d", i),
|
||||
fmt.Sprintf("value-with-padding-%06d-xxxxxxxxxxxxxxxxxxxxxxxx", i)))
|
||||
}
|
||||
s.Close()
|
||||
|
||||
// Force a WAL checkpoint so all data is in the main database file.
|
||||
raw, err := sql.Open("sqlite", dbPath)
|
||||
require.NoError(t, err)
|
||||
raw.SetMaxOpenConns(1)
|
||||
_, err = raw.Exec("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, raw.Close())
|
||||
|
||||
// Corrupt data pages in the latter portion of the file (skip the first
|
||||
// pages which hold the schema).
|
||||
info, err := os.Stat(dbPath)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, info.Size(), int64(16384), "DB should be large enough to corrupt")
|
||||
|
||||
f, err := os.OpenFile(dbPath, os.O_RDWR, 0644)
|
||||
require.NoError(t, err)
|
||||
garbage := make([]byte, 4096)
|
||||
for i := range garbage {
|
||||
garbage[i] = 0xFF
|
||||
}
|
||||
offset := info.Size() * 3 / 4
|
||||
_, err = f.WriteAt(garbage, offset)
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteAt(garbage, offset+4096)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
// Remove WAL/SHM so the reopened connection reads from the main file.
|
||||
os.Remove(dbPath + "-wal")
|
||||
os.Remove(dbPath + "-shm")
|
||||
|
||||
s2, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
defer s2.Close()
|
||||
|
||||
_, err = s2.GetAll("g")
|
||||
require.Error(t, err, "GetAll should fail on corrupted database pages")
|
||||
assert.Contains(t, err.Error(), "store.GetAll: rows")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render — scan error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRender_Bad_ScanError(t *testing.T) {
|
||||
// Same NULL-key technique as TestGetAll_Bad_ScanError.
|
||||
s, err := New(":memory:")
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
require.NoError(t, s.Set("g", "good", "value"))
|
||||
|
||||
_, err = s.db.Exec("ALTER TABLE kv RENAME TO kv_backup")
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec(`CREATE TABLE kv (
|
||||
grp TEXT,
|
||||
key TEXT,
|
||||
value TEXT,
|
||||
expires_at INTEGER
|
||||
)`)
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec("INSERT INTO kv SELECT * FROM kv_backup")
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec("INSERT INTO kv (grp, key, value) VALUES ('g', NULL, 'null-key-val')")
|
||||
require.NoError(t, err)
|
||||
_, err = s.db.Exec("DROP TABLE kv_backup")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.Render("{{ .good }}", "g")
|
||||
require.Error(t, err, "Render should fail when a row contains a NULL key")
|
||||
assert.Contains(t, err.Error(), "store.Render: scan")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render — rows iteration error path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRender_Bad_RowsError(t *testing.T) {
|
||||
// Same corruption technique as TestGetAll_Bad_RowsError.
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "corrupt-render.db")
|
||||
|
||||
s, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
const rows = 5000
|
||||
for i := 0; i < rows; i++ {
|
||||
require.NoError(t, s.Set("g",
|
||||
fmt.Sprintf("key-%06d", i),
|
||||
fmt.Sprintf("value-with-padding-%06d-xxxxxxxxxxxxxxxxxxxxxxxx", i)))
|
||||
}
|
||||
s.Close()
|
||||
|
||||
raw, err := sql.Open("sqlite", dbPath)
|
||||
require.NoError(t, err)
|
||||
raw.SetMaxOpenConns(1)
|
||||
_, err = raw.Exec("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, raw.Close())
|
||||
|
||||
info, err := os.Stat(dbPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := os.OpenFile(dbPath, os.O_RDWR, 0644)
|
||||
require.NoError(t, err)
|
||||
garbage := make([]byte, 4096)
|
||||
for i := range garbage {
|
||||
garbage[i] = 0xFF
|
||||
}
|
||||
offset := info.Size() * 3 / 4
|
||||
_, err = f.WriteAt(garbage, offset)
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteAt(garbage, offset+4096)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
os.Remove(dbPath + "-wal")
|
||||
os.Remove(dbPath + "-shm")
|
||||
|
||||
s2, err := New(dbPath)
|
||||
require.NoError(t, err)
|
||||
defer s2.Close()
|
||||
|
||||
_, err = s2.Render("{{ . }}", "g")
|
||||
require.Error(t, err, "Render should fail on corrupted database pages")
|
||||
assert.Contains(t, err.Error(), "store.Render: rows")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue