389 lines
9.8 KiB
Go
389 lines
9.8 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).Auth(SystemUserID)
|
|
// Bootstrap system tables
|
|
_ = dbInst.syncSchema("")
|
|
return dbInst
|
|
}
|
|
|
|
func TestBootstrapAndSync(t *testing.T) {
|
|
dbFile := "test_bootstrap.db"
|
|
_ = os.Remove(dbFile)
|
|
dbInst := GetDB("sqlite://"+dbFile, log.DefaultLogger).Auth(SystemUserID)
|
|
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. Create business table via API
|
|
err = dbInst.Table("users").SetField(
|
|
FieldSchema{Name: "name", Type: "v64", IsIndex: true},
|
|
FieldSchema{Name: "age", Type: "i"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Create table failed: %v", err)
|
|
}
|
|
|
|
// Verify business table
|
|
if GlobalCache.GetTable("users") == nil {
|
|
t.Errorf("users table not found in cache after sync")
|
|
}
|
|
fields, _ := dbInst.Table("users").Fields()
|
|
if len(fields) < 3 { // id, name, age
|
|
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)
|
|
|
|
app := dbInst.Auth("admin")
|
|
|
|
// 1. Create table and add fields via SetField API
|
|
err := app.Table("tasks").SetField(
|
|
FieldSchema{Name: "title", Type: "v100", Memo: "Task title"},
|
|
FieldSchema{Name: "done", Type: "b", IsIndex: true},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create table/fields via API: %v", err)
|
|
}
|
|
|
|
// 2. 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.Table("products").SetField(
|
|
FieldSchema{Name: "name", Type: "v64", IsIndex: true},
|
|
FieldSchema{Name: "price", Type: "i"},
|
|
)
|
|
|
|
app := dbInst.Auth("admin")
|
|
table := app.Table("products")
|
|
|
|
// Create (Batch)
|
|
err := table.Set(
|
|
map[string]any{"name": "Laptop", "price": 1000},
|
|
map[string]any{"id": "p2", "name": "Mouse", "price": 50},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Batch set failed: %v", err)
|
|
}
|
|
|
|
laptop, _ := table.List(map[string]any{"name": "Laptop"})
|
|
if len(laptop) == 0 {
|
|
t.Fatalf("Laptop not created")
|
|
}
|
|
laptopID := cast.String(laptop[0]["id"])
|
|
|
|
// 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 (Batch)
|
|
err = table.Remove("p2", laptopID)
|
|
if err != nil {
|
|
t.Fatalf("Batch remove failed: %v", err)
|
|
}
|
|
|
|
p1_removed, _ := table.Get("p2")
|
|
if p1_removed != nil {
|
|
t.Errorf("Expected p2 to be removed")
|
|
}
|
|
|
|
// Verify shadow delete (actual table has _deleted suffix)
|
|
raw, _ := dbInst.GetRawDB()
|
|
res := raw.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.Table("secrets").SetField(
|
|
FieldSchema{Name: "content", Type: "t"},
|
|
FieldSchema{Name: "creator", Type: "v64"},
|
|
)
|
|
_ = dbInst.SetTable(TableSchema{Name: "secrets", EnableRLS: true})
|
|
|
|
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
|
|
_, err := user2.Table("secrets").Get("s1")
|
|
if err == nil {
|
|
t.Errorf("user2 should not have permission to get user1 secret")
|
|
}
|
|
|
|
// system gets it
|
|
sys := dbInst.Auth(SystemUserID)
|
|
s, err := sys.Table("secrets").Get("s1")
|
|
if err != nil || s == nil {
|
|
t.Errorf("system should have permission: %v", err)
|
|
}
|
|
|
|
// Test _Policy: Grant access to specific ID via condition
|
|
err = sys.Table("_Policy").Set(map[string]any{
|
|
"userID": "user2",
|
|
"type": "table",
|
|
"targets": []string{"secrets"},
|
|
"action": "read",
|
|
"condition": "id = ?",
|
|
"conditionArgs": []any{"s1"},
|
|
})
|
|
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 TestInheritance(t *testing.T) {
|
|
dbFile := "test_inherit.db"
|
|
dbInst := setupDB(t, dbFile)
|
|
defer os.Remove(dbFile)
|
|
|
|
_ = dbInst.Table("docs").SetField(
|
|
FieldSchema{Name: "title", Type: "v64"},
|
|
FieldSchema{Name: "creator", Type: "v64"},
|
|
)
|
|
_ = dbInst.SetTable(TableSchema{Name: "docs", EnableRLS: true})
|
|
|
|
sys := dbInst.Auth(SystemUserID)
|
|
_ = sys.Table("docs").Set(map[string]any{"id": "d1", "title": "secret doc", "creator": "boss"})
|
|
|
|
// manager inherits boss
|
|
_ = sys.Table("_Policy").Set(map[string]any{
|
|
"userID": "manager",
|
|
"type": "inherit",
|
|
"targets": []string{"boss"},
|
|
})
|
|
|
|
// boss can read docs
|
|
_ = sys.Table("_Policy").Set(map[string]any{
|
|
"userID": "boss",
|
|
"type": "table",
|
|
"targets": []string{"docs"},
|
|
"action": "read",
|
|
})
|
|
|
|
manager := dbInst.Auth("manager")
|
|
d, err := manager.Table("docs").Get("d1")
|
|
if err != nil || d == nil {
|
|
t.Errorf("manager should inherit boss's permission to read docs: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPolicyInterfaces(t *testing.T) {
|
|
dbFile := "test_policy_api.db"
|
|
dbInst := setupDB(t, dbFile)
|
|
defer os.Remove(dbFile)
|
|
|
|
sys := dbInst.Auth(SystemUserID)
|
|
_ = sys.Table("data").SetField(FieldSchema{Name: "val", Type: "v10"})
|
|
_ = sys.SetTable(TableSchema{Name: "data", EnableRLS: true})
|
|
|
|
// 1. Normal user tries to set inherit policy (should fail)
|
|
user1 := dbInst.Auth("user1")
|
|
err := user1.SetPolicy(PolicySchema{
|
|
UserID: "user2",
|
|
Type: "inherit",
|
|
Targets: []string{"user1"},
|
|
})
|
|
if err == nil {
|
|
t.Errorf("Normal user should not be able to set inherit policy")
|
|
}
|
|
|
|
// 2. Normal user tries to set table policy for table they don't have full access to (should fail)
|
|
err = user1.SetPolicy(PolicySchema{
|
|
UserID: "user2",
|
|
Type: "table",
|
|
Targets: []string{"data"},
|
|
Action: "read",
|
|
})
|
|
if err == nil {
|
|
t.Errorf("User without full access to table should not be able to set policy for it")
|
|
}
|
|
|
|
// 3. System grants user1 full access to 'data'
|
|
_ = sys.SetPolicy(PolicySchema{
|
|
UserID: "user1",
|
|
Type: "table",
|
|
Targets: []string{"data"},
|
|
Action: "full",
|
|
})
|
|
|
|
// 4. Now user1 sets policy for user2 on 'data' (should succeed)
|
|
err = user1.SetPolicy(PolicySchema{
|
|
UserID: "user2",
|
|
Type: "table",
|
|
Targets: []string{"data"},
|
|
Action: "read",
|
|
})
|
|
if err != nil {
|
|
t.Errorf("User with full access should be able to set policy: %v", err)
|
|
}
|
|
|
|
// 5. List policies
|
|
pols, err := user1.ListPolicy(map[string]any{"userID": "user2"})
|
|
if err != nil || len(pols) == 0 || pols[0].UserID != "user2" {
|
|
t.Errorf("ListPolicy failed: %v, %v", err, pols)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
app := dbInst.Auth("admin")
|
|
|
|
// Trigger OnCreatedTable
|
|
_ = app.Table("hook_test").SetField(FieldSchema{Name: "val", Type: "v10"})
|
|
if tableCreated != "hook_test" {
|
|
t.Errorf("OnCreatedTable hook failed, got %s", tableCreated)
|
|
}
|
|
|
|
// 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.Table("safe_table").SetField(FieldSchema{Name: "name", Type: "v64"})
|
|
|
|
app := dbInst.Auth("user1")
|
|
|
|
// Valid query
|
|
req := QueryRequest{
|
|
Select: []string{"id", "name"},
|
|
OrderBy: "name DESC",
|
|
}
|
|
_, err := app.Table("safe_table").Query(req)
|
|
if err != nil {
|
|
t.Errorf("Valid query failed: %v", err)
|
|
}
|
|
|
|
// SQL Injection in Select
|
|
req.Select = []string{"name` FROM safe_table; --"}
|
|
_, err = app.Table("safe_table").Query(req)
|
|
if err == nil {
|
|
t.Errorf("Should fail for injection in Select")
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
app := GetDB("sqlite://"+dbFile, logger).Auth(SystemUserID)
|
|
_ = app.syncSchema("")
|
|
_ = app.Table("bench_ops").SetField(
|
|
FieldSchema{Name: "name", Type: "v50", IsIndex: true},
|
|
FieldSchema{Name: "val", Type: "i"},
|
|
)
|
|
|
|
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,
|
|
})
|
|
}
|
|
}
|