2026-05-14 22:36:04 +08:00
|
|
|
package tableDB
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-14 23:39:37 +08:00
|
|
|
"fmt"
|
2026-05-14 22:36:04 +08:00
|
|
|
"os"
|
|
|
|
|
"testing"
|
2026-05-14 23:39:37 +08:00
|
|
|
"time"
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
"apigo.cc/go/cast"
|
2026-05-14 22:36:04 +08:00
|
|
|
"apigo.cc/go/log"
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
func setupDB(t *testing.T, dbFile string) *TableDB {
|
|
|
|
|
_ = os.Remove(dbFile)
|
2026-05-14 22:36:04 +08:00
|
|
|
logger := log.DefaultLogger
|
2026-05-14 23:39:37 +08:00
|
|
|
logger.SetLevel(log.ERROR)
|
2026-05-14 22:36:04 +08:00
|
|
|
dbInst := GetDB("sqlite://"+dbFile, logger)
|
2026-05-14 23:39:37 +08:00
|
|
|
return dbInst
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
func TestBootstrapAndSync(t *testing.T) {
|
|
|
|
|
dbFile := "test_bootstrap.db"
|
|
|
|
|
dbInst := setupDB(t, dbFile)
|
|
|
|
|
defer os.Remove(dbFile)
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 1. Bootstrap: Sync empty DSL, should still create system tables
|
|
|
|
|
err := dbInst.SyncSchema("")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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")
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 2. Sync business schema
|
|
|
|
|
businessDSL := `
|
|
|
|
|
== Business ==
|
|
|
|
|
users SD
|
2026-05-14 22:36:04 +08:00
|
|
|
id c10 PK
|
2026-05-14 23:39:37 +08:00
|
|
|
name v64 U
|
|
|
|
|
age i
|
2026-05-14 22:36:04 +08:00
|
|
|
`
|
2026-05-14 23:39:37 +08:00
|
|
|
err = dbInst.SyncSchema(businessDSL)
|
2026-05-14 22:36:04 +08:00
|
|
|
if err != nil {
|
2026-05-14 23:39:37 +08:00
|
|
|
t.Fatalf("Business sync failed: %v", err)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Verify business table
|
|
|
|
|
if GlobalCache.GetTable("users") == nil {
|
|
|
|
|
t.Errorf("users table not found in cache after sync")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
func TestAPIDrivenSchema(t *testing.T) {
|
|
|
|
|
dbFile := "test_api_schema.db"
|
|
|
|
|
dbInst := setupDB(t, dbFile)
|
|
|
|
|
defer os.Remove(dbFile)
|
|
|
|
|
|
|
|
|
|
_ = dbInst.SyncSchema("")
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
// Get tid
|
|
|
|
|
tbl, _ := app.Table("_Table").List(map[string]any{"name": "tasks"})
|
|
|
|
|
if len(tbl) == 0 {
|
|
|
|
|
t.Fatalf("Table tasks not found in _Table")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
tid := cast.String(tbl[0]["id"])
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
res, _ := taskTable.List(nil)
|
|
|
|
|
if len(res) != 1 || res[0]["title"] != "Fix tests" {
|
|
|
|
|
t.Errorf("Query from tasks failed: %v", res)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
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
|
|
|
|
|
`)
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
app := dbInst.Auth("admin")
|
|
|
|
|
table := app.Table("products")
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Create
|
|
|
|
|
_ = table.Set(map[string]any{"name": "Laptop", "price": 1000})
|
|
|
|
|
_ = table.Set(map[string]any{"id": "p2", "name": "Mouse", "price": 50})
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Read
|
|
|
|
|
p1, _ := table.Get("p2")
|
|
|
|
|
if p1 == nil || p1["name"] != "Mouse" {
|
|
|
|
|
t.Errorf("Get p2 failed")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
// 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"])
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
// 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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Count
|
|
|
|
|
cnt, _ := table.Count(nil)
|
|
|
|
|
if cnt != 2 {
|
|
|
|
|
t.Errorf("Count failed: %d", cnt)
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Delete (Shadow Delete)
|
|
|
|
|
_ = table.Remove("p2")
|
|
|
|
|
p1_removed, _ := table.Get("p2")
|
|
|
|
|
if p1_removed != nil {
|
|
|
|
|
t.Errorf("Expected p2 to be removed")
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
func TestPermissionsAndAuth(t *testing.T) {
|
|
|
|
|
dbFile := "test_auth.db"
|
|
|
|
|
dbInst := setupDB(t, dbFile)
|
|
|
|
|
defer os.Remove(dbFile)
|
|
|
|
|
|
|
|
|
|
_ = dbInst.SyncSchema(`
|
|
|
|
|
== Secret ==
|
|
|
|
|
secrets SD
|
2026-05-14 22:36:04 +08:00
|
|
|
id c10 PK
|
2026-05-14 23:39:37 +08:00
|
|
|
content t
|
|
|
|
|
creator v64
|
|
|
|
|
`)
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
user1 := dbInst.Auth("user1")
|
|
|
|
|
user2 := dbInst.Auth("user2")
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// user1 creates a secret
|
|
|
|
|
_ = user1.Table("secrets").Set(map[string]any{"id": "s1", "content": "user1-secret"})
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Test _Policy
|
|
|
|
|
// allow user2 to see s1
|
|
|
|
|
err = sys.Table("_Policy").Set(map[string]any{
|
|
|
|
|
"subject": "user2",
|
|
|
|
|
"action": "read",
|
|
|
|
|
"resource": "s1",
|
|
|
|
|
"effect": "allow",
|
2026-05-14 22:36:04 +08:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
2026-05-14 23:39:37 +08:00
|
|
|
t.Fatalf("Failed to set policy: %v", err)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
func TestHooks(t *testing.T) {
|
|
|
|
|
dbFile := "test_hooks.db"
|
|
|
|
|
dbInst := setupDB(t, dbFile)
|
|
|
|
|
defer os.Remove(dbFile)
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
var tableCreated string
|
|
|
|
|
var rowsUpdated int
|
|
|
|
|
|
|
|
|
|
dbInst.Hooks.OnCreatedTable = func(tableName string, record map[string]any) {
|
|
|
|
|
tableCreated = tableName
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
dbInst.Hooks.OnUpdatedRows = func(tableName string, count int) {
|
|
|
|
|
rowsUpdated += count
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
_ = 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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
// 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)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
}
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
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",
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
_, _, err := dbInst.BuildQuery(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("Valid query failed: %v", err)
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// Invalid table
|
|
|
|
|
req.Table = "invalid_table"
|
|
|
|
|
_, _, err = dbInst.BuildQuery(req)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Errorf("Should fail for invalid table")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
2026-05-14 23:39:37 +08:00
|
|
|
|
|
|
|
|
// 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")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
// 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")
|
2026-05-14 22:36:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BenchmarkTableSet(b *testing.B) {
|
|
|
|
|
logger := log.DefaultLogger
|
|
|
|
|
logger.SetLevel(log.ERROR)
|
2026-05-14 23:39:37 +08:00
|
|
|
dbFile := fmt.Sprintf("bench_ops_%d.db", time.Now().UnixNano())
|
|
|
|
|
defer os.Remove(dbFile)
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
dbInst := GetDB("sqlite://"+dbFile, logger)
|
|
|
|
|
_ = dbInst.SyncSchema("bench_ops SD\n id c10 PK\n name v50 U\n val i")
|
2026-05-14 22:36:04 +08:00
|
|
|
|
2026-05-14 23:39:37 +08:00
|
|
|
app := dbInst.Auth("admin")
|
|
|
|
|
table := app.Table("bench_ops")
|
2026-05-14 22:36:04 +08:00
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
2026-05-14 23:39:37 +08:00
|
|
|
_ = table.Set(map[string]any{
|
|
|
|
|
"name": "user_" + cast.String(i),
|
2026-05-14 22:36:04 +08:00
|
|
|
"val": i,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|