tableDB/tableDB_test.go

333 lines
7.7 KiB
Go

package tableDB
import (
"fmt"
"os"
"testing"
"time"
"apigo.cc/go/cast"
"apigo.cc/go/log"
)
func setupDB(t *testing.T, dbFile string) *TableDB {
_ = os.Remove(dbFile)
logger := log.DefaultLogger
logger.SetLevel(log.ERROR)
dbInst := GetDB("sqlite://"+dbFile, logger)
return dbInst
}
func TestBootstrapAndSync(t *testing.T) {
dbFile := "test_bootstrap.db"
dbInst := setupDB(t, dbFile)
defer os.Remove(dbFile)
// 1. Bootstrap: Sync empty DSL, should still create system tables
err := dbInst.SyncSchema("")
if err != nil {
t.Fatalf("Bootstrap failed: %v", err)
}
// Verify system tables in cache
if GlobalCache.GetTable("_Table") == nil {
t.Errorf("_Table not found in cache after bootstrap")
}
if GlobalCache.GetTable("_Field") == nil {
t.Errorf("_Field not found in cache after bootstrap")
}
// 2. Sync business schema
businessDSL := `
== Business ==
users SD
id c10 PK
name v64 U
age i
`
err = dbInst.SyncSchema(businessDSL)
if err != nil {
t.Fatalf("Business sync failed: %v", err)
}
// Verify business table
if GlobalCache.GetTable("users") == nil {
t.Errorf("users table not found in cache after sync")
}
fields := GlobalCache.GetValidFields("users")
if len(fields) < 3 { // id, name, age (and maybe others from SD)
t.Errorf("Expected at least 3 fields for users, got %v", fields)
}
}
func TestAPIDrivenSchema(t *testing.T) {
dbFile := "test_api_schema.db"
dbInst := setupDB(t, dbFile)
defer os.Remove(dbFile)
_ = dbInst.SyncSchema("")
app := dbInst.Auth("admin")
// 1. Create table via _Table API
err := app.Table("_Table").Set(map[string]any{
"name": "tasks",
"memo": "Task list",
})
if err != nil {
t.Fatalf("Failed to create table via API: %v", err)
}
// Get tid
tbl, _ := app.Table("_Table").List(map[string]any{"name": "tasks"})
if len(tbl) == 0 {
t.Fatalf("Table tasks not found in _Table")
}
tid := cast.String(tbl[0]["id"])
// 2. Add fields via _Field API
_ = app.Table("_Field").Set(map[string]any{
"tableId": tid,
"name": "title",
"type": "v100",
"memo": "Task title",
})
_ = app.Table("_Field").Set(map[string]any{
"tableId": tid,
"name": "done",
"type": "b",
"isIndex": 1,
})
// 3. Verify table works
taskTable := app.Table("tasks")
err = taskTable.Set(map[string]any{
"title": "Fix tests",
"done": false,
})
if err != nil {
t.Fatalf("Failed to insert into tasks table: %v", err)
}
res, _ := taskTable.List(nil)
if len(res) != 1 || res[0]["title"] != "Fix tests" {
t.Errorf("Query from tasks failed: %v", res)
}
}
func TestDataOperations(t *testing.T) {
dbFile := "test_data_ops.db"
dbInst := setupDB(t, dbFile)
defer os.Remove(dbFile)
_ = dbInst.SyncSchema(`
== Test ==
products SD
id c10 PK
name v64 U
price i
`)
app := dbInst.Auth("admin")
table := app.Table("products")
// Create
_ = table.Set(map[string]any{"name": "Laptop", "price": 1000})
_ = table.Set(map[string]any{"id": "p2", "name": "Mouse", "price": 50})
// Read
p1, _ := table.Get("p2")
if p1 == nil || p1["name"] != "Mouse" {
t.Errorf("Get p2 failed")
}
// Update
_ = table.Set(map[string]any{"id": "p2", "price": 45})
p1_updated, _ := table.Get("p2")
if cast.Int(p1_updated["price"]) != 45 {
t.Errorf("Update p2 failed, got %v", p1_updated["price"])
}
// List & Filter
list, _ := table.List(map[string]any{"price >": 100})
if len(list) != 1 || list[0]["name"] != "Laptop" {
t.Errorf("List/Filter failed: %v", list)
}
// Count
cnt, _ := table.Count(nil)
if cnt != 2 {
t.Errorf("Count failed: %d", cnt)
}
// Delete (Shadow Delete)
_ = table.Remove("p2")
p1_removed, _ := table.Get("p2")
if p1_removed != nil {
t.Errorf("Expected p2 to be removed")
}
// Verify shadow delete (actual table has _deleted suffix)
res := dbInst.Base().Query("SELECT name FROM sqlite_master WHERE name='products_deleted'")
if res.MapOnR1()["name"] == nil {
t.Errorf("Shadow delete table products_deleted not found")
}
}
func TestPermissionsAndAuth(t *testing.T) {
dbFile := "test_auth.db"
dbInst := setupDB(t, dbFile)
defer os.Remove(dbFile)
_ = dbInst.SyncSchema(`
== Secret ==
secrets SD
id c10 PK
content t
creator v64
`)
user1 := dbInst.Auth("user1")
user2 := dbInst.Auth("user2")
// user1 creates a secret
_ = user1.Table("secrets").Set(map[string]any{"id": "s1", "content": "user1-secret"})
// user2 tries to get it -> should fail or return nil depending on implementation
// Current implementation: checkAuth returns error if no permission
_, err := user2.Table("secrets").Get("s1")
if err == nil {
t.Errorf("user2 should not have permission to get user1 secret")
}
// admin gets it -> _system or admin should have permission?
// Implementation says t.userID == "_system" bypasses.
sys := dbInst.Auth("_system")
s, err := sys.Table("secrets").Get("s1")
if err != nil || s == nil {
t.Errorf("system should have permission: %v", err)
}
// Test _Policy
// allow user2 to see s1
err = sys.Table("_Policy").Set(map[string]any{
"subject": "user2",
"action": "read",
"resource": "s1",
"effect": "allow",
})
if err != nil {
t.Fatalf("Failed to set policy: %v", err)
}
// Now user2 should be able to get it
s, err = user2.Table("secrets").Get("s1")
if err != nil || s == nil {
t.Errorf("user2 should now have permission via policy: %v", err)
}
}
func TestHooks(t *testing.T) {
dbFile := "test_hooks.db"
dbInst := setupDB(t, dbFile)
defer os.Remove(dbFile)
var tableCreated string
var rowsUpdated int
dbInst.Hooks.OnCreatedTable = func(tableName string, record map[string]any) {
tableCreated = tableName
}
dbInst.Hooks.OnUpdatedRows = func(tableName string, count int) {
rowsUpdated += count
}
_ = dbInst.SyncSchema("")
app := dbInst.Auth("admin")
// Trigger OnCreatedTable
_ = app.Table("_Table").Set(map[string]any{"name": "hook_test"})
if tableCreated != "hook_test" {
t.Errorf("OnCreatedTable hook failed, got %s", tableCreated)
}
// Add a field to ensure table exists
tbl, _ := app.Table("_Table").List(map[string]any{"name": "hook_test"})
tid := cast.String(tbl[0]["id"])
_ = app.Table("_Field").Set(map[string]any{"tableId": tid, "name": "val", "type": "v10"})
// Trigger OnUpdatedRows
_ = app.Table("hook_test").Set(map[string]any{"id": "1", "val": "a"})
if rowsUpdated != 1 {
t.Errorf("OnUpdatedRows hook failed, got %d", rowsUpdated)
}
}
func TestQueryValidationAndInjection(t *testing.T) {
dbFile := "test_query.db"
dbInst := setupDB(t, dbFile)
defer os.Remove(dbFile)
_ = dbInst.SyncSchema(`
== Test ==
safe_table SD
id c10 PK
name v64
`)
// Valid query
req := QueryRequest{
Table: "safe_table",
Select: []string{"id", "name"},
OrderBy: "name DESC",
}
_, _, err := dbInst.BuildQuery(req)
if err != nil {
t.Errorf("Valid query failed: %v", err)
}
// Invalid table
req.Table = "invalid_table"
_, _, err = dbInst.BuildQuery(req)
if err == nil {
t.Errorf("Should fail for invalid table")
}
// SQL Injection in Select
req.Table = "safe_table"
req.Select = []string{"name` FROM safe_table; --"}
_, _, err = dbInst.BuildQuery(req)
if err == nil {
t.Errorf("Should fail for injection in Select")
}
// SQL Injection in OrderBy
req.Select = []string{"name"}
req.OrderBy = "name; DROP TABLE safe_table"
_, _, err = dbInst.BuildQuery(req)
if err == nil {
t.Errorf("Should fail for injection in OrderBy")
}
}
func BenchmarkTableSet(b *testing.B) {
logger := log.DefaultLogger
logger.SetLevel(log.ERROR)
dbFile := fmt.Sprintf("bench_ops_%d.db", time.Now().UnixNano())
defer os.Remove(dbFile)
dbInst := GetDB("sqlite://"+dbFile, logger)
_ = dbInst.SyncSchema("bench_ops SD\n id c10 PK\n name v50 U\n val i")
app := dbInst.Auth("admin")
table := app.Table("bench_ops")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = table.Set(map[string]any{
"name": "user_" + cast.String(i),
"val": i,
})
}
}