Files
zeekay 7fe0beaa96 test: 16 OrgDB tests — provisioning, DEK derivation, isolation
- ProvisionOrg/User creates directories and paths correctly
- Invalid slugs rejected (path traversal, uppercase, specials)
- HKDF DEK is deterministic, unique per org, unique per user,
  org≠user even with same input (different HKDF info param)
- Empty master key returns empty DEK (dev mode, no encryption)
- Lookup works after provision, returns false before
- DeleteUser removes directory and lookup entry
- Integration: OrgDB + PoolManager read/write through dual pools
- Multi-org isolation: same table name, different data per org
2026-03-02 14:39:34 -07:00

340 lines
8.3 KiB
Go

package platform
import (
"os"
"path/filepath"
"testing"
"github.com/hanzoai/base/core"
)
// mockApp implements the minimal core.App interface for OrgDB tests.
type mockApp struct {
core.BaseApp
dataDir string
}
func (a *mockApp) DataDir() string { return a.dataDir }
func testOrgDB(t *testing.T) (*OrgDB, string) {
t.Helper()
dir := t.TempDir()
app := &mockApp{dataDir: dir}
return NewOrgDB(app, "test-master-key-32-bytes-long!!!"), dir
}
// --- OrgDB provisioning ---
func TestOrgDB_ProvisionOrg(t *testing.T) {
db, dir := testOrgDB(t)
path, err := db.ProvisionOrg("acme")
if err != nil {
t.Fatal(err)
}
expected := filepath.Join(dir, "orgs", "acme", "org.db")
if path != expected {
t.Fatalf("expected %s, got %s", expected, path)
}
// Directory should exist
if _, err := os.Stat(filepath.Join(dir, "orgs", "acme")); os.IsNotExist(err) {
t.Fatal("org directory not created")
}
// Users subdirectory should exist
if _, err := os.Stat(filepath.Join(dir, "orgs", "acme", "users")); os.IsNotExist(err) {
t.Fatal("users subdirectory not created")
}
}
func TestOrgDB_ProvisionUser(t *testing.T) {
db, dir := testOrgDB(t)
_, _ = db.ProvisionOrg("acme")
path, err := db.ProvisionUser("acme", "user-001")
if err != nil {
t.Fatal(err)
}
expected := filepath.Join(dir, "orgs", "acme", "users", "user-001", "data.db")
if path != expected {
t.Fatalf("expected %s, got %s", expected, path)
}
if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) {
t.Fatal("user directory not created")
}
}
func TestOrgDB_ProvisionMultipleOrgs(t *testing.T) {
db, _ := testOrgDB(t)
orgs := []string{"acme", "globex", "initech", "umbrella"}
for _, org := range orgs {
if _, err := db.ProvisionOrg(org); err != nil {
t.Fatalf("provision org %s: %v", org, err)
}
}
// All should be lookupable
for _, org := range orgs {
if _, ok := db.GetOrgDBPath(org); !ok {
t.Fatalf("org %s not found after provisioning", org)
}
}
}
// --- Slug validation ---
func TestOrgDB_InvalidSlug(t *testing.T) {
db, _ := testOrgDB(t)
cases := []string{"", "..", "../etc", "org/../../etc", "UPPER", "has space", "special!"}
for _, slug := range cases {
if _, err := db.ProvisionOrg(slug); err == nil {
t.Fatalf("expected error for slug %q", slug)
}
}
}
func TestOrgDB_ValidSlugs(t *testing.T) {
db, _ := testOrgDB(t)
cases := []string{"acme", "org-123", "my_org", "a", "ab", "a-b-c-d"}
for _, slug := range cases {
if _, err := db.ProvisionOrg(slug); err != nil {
t.Fatalf("unexpected error for slug %q: %v", slug, err)
}
}
}
// --- DEK derivation ---
func TestOrgDB_OrgDEK_Deterministic(t *testing.T) {
db, _ := testOrgDB(t)
dek1 := db.OrgDEK("acme")
dek2 := db.OrgDEK("acme")
if dek1 != dek2 {
t.Fatal("OrgDEK should be deterministic for same input")
}
if len(dek1) != 64 { // 32 bytes = 64 hex chars
t.Fatalf("expected 64 hex chars, got %d", len(dek1))
}
}
func TestOrgDB_OrgDEK_UniquePerOrg(t *testing.T) {
db, _ := testOrgDB(t)
dek1 := db.OrgDEK("acme")
dek2 := db.OrgDEK("globex")
if dek1 == dek2 {
t.Fatal("different orgs should have different DEKs")
}
}
func TestOrgDB_UserDEK_UniquePerUser(t *testing.T) {
db, _ := testOrgDB(t)
dek1 := db.UserDEK("acme", "user-001")
dek2 := db.UserDEK("acme", "user-002")
dek3 := db.UserDEK("globex", "user-001")
if dek1 == dek2 {
t.Fatal("different users in same org should have different DEKs")
}
if dek1 == dek3 {
t.Fatal("same user ID in different orgs should have different DEKs")
}
}
func TestOrgDB_DEK_OrgVsUser_Different(t *testing.T) {
db, _ := testOrgDB(t)
orgDEK := db.OrgDEK("acme")
userDEK := db.UserDEK("acme", "acme") // same string as org slug
if orgDEK == userDEK {
t.Fatal("org DEK and user DEK should differ even with same input (different HKDF info)")
}
}
func TestOrgDB_DEK_EmptyMasterKey(t *testing.T) {
dir := t.TempDir()
app := &mockApp{dataDir: dir}
db := NewOrgDB(app, "") // no master key
if dek := db.OrgDEK("acme"); dek != "" {
t.Fatalf("expected empty DEK with no master key, got %s", dek)
}
if dek := db.UserDEK("acme", "user-001"); dek != "" {
t.Fatalf("expected empty DEK with no master key, got %s", dek)
}
}
// --- Lookup ---
func TestOrgDB_GetOrgDBPath_NotProvisioned(t *testing.T) {
db, _ := testOrgDB(t)
if _, ok := db.GetOrgDBPath("nonexistent"); ok {
t.Fatal("should not find non-provisioned org")
}
}
func TestOrgDB_GetUserDBPath_NotProvisioned(t *testing.T) {
db, _ := testOrgDB(t)
if _, ok := db.GetUserDBPath("acme", "user-001"); ok {
t.Fatal("should not find non-provisioned user")
}
}
func TestOrgDB_GetOrgDBPath_AfterProvision(t *testing.T) {
db, _ := testOrgDB(t)
expected, _ := db.ProvisionOrg("acme")
got, ok := db.GetOrgDBPath("acme")
if !ok {
t.Fatal("should find provisioned org")
}
if got != expected {
t.Fatalf("expected %s, got %s", expected, got)
}
}
// --- Delete ---
func TestOrgDB_DeleteUser(t *testing.T) {
db, _ := testOrgDB(t)
db.ProvisionOrg("acme")
path, _ := db.ProvisionUser("acme", "user-001")
// Create the actual DB file so the directory has content
os.WriteFile(path, []byte("test"), 0600)
if err := db.DeleteUser("acme", "user-001"); err != nil {
t.Fatal(err)
}
if _, ok := db.GetUserDBPath("acme", "user-001"); ok {
t.Fatal("user should be deleted from lookup")
}
if _, err := os.Stat(filepath.Dir(path)); !os.IsNotExist(err) {
t.Fatal("user directory should be removed")
}
}
// --- Integration: OrgDB + PoolManager ---
func TestOrgDB_WithPoolManager(t *testing.T) {
db, _ := testOrgDB(t)
pm := NewDBPoolManager(DBPoolConfig{MaxPools: 16, NumShards: 1})
defer pm.Close()
// Provision org
orgPath, _ := db.ProvisionOrg("acme")
// Open a pool for it
pool, err := pm.Get(orgPath)
if err != nil {
t.Fatal(err)
}
defer pool.Release()
// Create table and insert data
_, err = pool.Nonconcurrent.NewQuery("CREATE TABLE config (k TEXT PRIMARY KEY, v TEXT)").Execute()
if err != nil {
t.Fatalf("create table: %v", err)
}
_, err = pool.Nonconcurrent.NewQuery("INSERT INTO config (k, v) VALUES ('name', 'Acme Corp')").Execute()
if err != nil {
t.Fatalf("insert: %v", err)
}
// Read back via concurrent pool
var name string
if err := pool.Concurrent.NewQuery("SELECT v FROM config WHERE k = 'name'").Row(&name); err != nil {
t.Fatalf("select: %v", err)
}
if name != "Acme Corp" {
t.Fatalf("expected 'Acme Corp', got %q", name)
}
// Provision user in same org
userPath, _ := db.ProvisionUser("acme", "user-42")
userPool, err := pm.Get(userPath)
if err != nil {
t.Fatal(err)
}
defer userPool.Release()
// User DB is independent from org DB
_, err = userPool.Nonconcurrent.NewQuery("CREATE TABLE orders (id INTEGER PRIMARY KEY, symbol TEXT)").Execute()
if err != nil {
t.Fatalf("create user table: %v", err)
}
_, err = userPool.Nonconcurrent.NewQuery("INSERT INTO orders (symbol) VALUES ('AAPL')").Execute()
if err != nil {
t.Fatalf("insert user data: %v", err)
}
// Org DB should NOT have the orders table
var count int
err = pool.Concurrent.NewQuery("SELECT count(*) FROM sqlite_master WHERE name='orders'").Row(&count)
if err != nil {
t.Fatalf("check org db: %v", err)
}
if count != 0 {
t.Fatal("org DB should not have user's orders table — data isolation broken")
}
// Stats
stats := pm.Stats()
if stats.Opens != 2 {
t.Fatalf("expected 2 opens (org + user), got %d", stats.Opens)
}
}
// --- Multi-org isolation ---
func TestOrgDB_MultiOrgIsolation(t *testing.T) {
db, _ := testOrgDB(t)
pm := NewDBPoolManager(DBPoolConfig{MaxPools: 16, NumShards: 1})
defer pm.Close()
// Two orgs
path1, _ := db.ProvisionOrg("acme")
path2, _ := db.ProvisionOrg("globex")
p1, _ := pm.Get(path1)
defer p1.Release()
p2, _ := pm.Get(path2)
defer p2.Release()
// Each org creates the same table name
p1.Nonconcurrent.NewQuery("CREATE TABLE accounts (id INTEGER, name TEXT)").Execute()
p2.Nonconcurrent.NewQuery("CREATE TABLE accounts (id INTEGER, name TEXT)").Execute()
p1.Nonconcurrent.NewQuery("INSERT INTO accounts (name) VALUES ('Alice')").Execute()
p2.Nonconcurrent.NewQuery("INSERT INTO accounts (name) VALUES ('Bob')").Execute()
// Data is isolated
var name1, name2 string
p1.Concurrent.NewQuery("SELECT name FROM accounts").Row(&name1)
p2.Concurrent.NewQuery("SELECT name FROM accounts").Row(&name2)
if name1 != "Alice" {
t.Fatalf("acme should have Alice, got %q", name1)
}
if name2 != "Bob" {
t.Fatalf("globex should have Bob, got %q", name2)
}
}