mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-01 08:20:39 +00:00
Resolve the PR #8/#9/#10 stack on current dev, including Bitrix24 install callback hardening, migration renumbering, duplicate-domain fail-closed routing, UI textarea/mobile cleanup, and review hardening.
282 lines
8.0 KiB
Go
282 lines
8.0 KiB
Go
//go:build sqlite || sqliteonly
|
|
|
|
package sqlitestore
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
const bitrixTestEncKey = "0123456789abcdef0123456789abcdef" // 32 bytes for AES-256
|
|
|
|
func newTestSQLiteBitrixPortalStore(t *testing.T, encKey string) (*SQLiteBitrixPortalStore, *sql.DB, uuid.UUID) {
|
|
t.Helper()
|
|
|
|
db, err := OpenDB(filepath.Join(t.TempDir(), "bitrix.db"))
|
|
if err != nil {
|
|
t.Fatalf("OpenDB: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
if err := EnsureSchema(db); err != nil {
|
|
t.Fatalf("EnsureSchema: %v", err)
|
|
}
|
|
|
|
// Create a tenant row so FK constraint is satisfied.
|
|
tenantID := store.GenNewID()
|
|
if _, err := db.Exec(
|
|
`INSERT INTO tenants (id, name, slug, status, settings, created_at, updated_at)
|
|
VALUES (?, 'test-tenant', 'test-tenant', 'active', '{}', datetime('now'), datetime('now'))`,
|
|
tenantID,
|
|
); err != nil {
|
|
t.Fatalf("insert tenant: %v", err)
|
|
}
|
|
|
|
return NewSQLiteBitrixPortalStore(db, encKey), db, tenantID
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_CreateAndGet(t *testing.T) {
|
|
ps, _, tenantID := newTestSQLiteBitrixPortalStore(t, bitrixTestEncKey)
|
|
ctx := context.Background()
|
|
|
|
p := &store.BitrixPortalData{
|
|
TenantID: tenantID,
|
|
Name: "prod",
|
|
Domain: "example.bitrix24.com",
|
|
Credentials: []byte(`{"client_id":"abc","client_secret":"shh"}`),
|
|
State: []byte(`{"access_token":"tkn"}`),
|
|
}
|
|
if err := ps.Create(ctx, p); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if p.ID == uuid.Nil {
|
|
t.Fatal("expected ID to be assigned")
|
|
}
|
|
|
|
got, err := ps.GetByName(ctx, tenantID, "prod")
|
|
if err != nil {
|
|
t.Fatalf("GetByName: %v", err)
|
|
}
|
|
if got.ID != p.ID {
|
|
t.Fatalf("id mismatch: got %v, want %v", got.ID, p.ID)
|
|
}
|
|
if got.Domain != "example.bitrix24.com" {
|
|
t.Fatalf("domain mismatch: got %q", got.Domain)
|
|
}
|
|
if string(got.Credentials) != `{"client_id":"abc","client_secret":"shh"}` {
|
|
t.Fatalf("credentials decrypt mismatch: got %q", got.Credentials)
|
|
}
|
|
if string(got.State) != `{"access_token":"tkn"}` {
|
|
t.Fatalf("state decrypt mismatch: got %q", got.State)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_EncryptsOnDisk(t *testing.T) {
|
|
ps, db, tenantID := newTestSQLiteBitrixPortalStore(t, bitrixTestEncKey)
|
|
ctx := context.Background()
|
|
|
|
plaintext := `{"client_id":"visible"}`
|
|
p := &store.BitrixPortalData{
|
|
TenantID: tenantID,
|
|
Name: "enc",
|
|
Domain: "enc.bitrix24.com",
|
|
Credentials: []byte(plaintext),
|
|
}
|
|
if err := ps.Create(ctx, p); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
// Read raw bytes directly — should NOT contain plaintext.
|
|
var raw []byte
|
|
if err := db.QueryRowContext(ctx,
|
|
`SELECT credentials FROM bitrix_portals WHERE tenant_id = ? AND name = ?`,
|
|
tenantID.String(), "enc",
|
|
).Scan(&raw); err != nil {
|
|
t.Fatalf("raw query: %v", err)
|
|
}
|
|
if len(raw) == 0 {
|
|
t.Fatal("expected non-empty credentials bytes")
|
|
}
|
|
if string(raw) == plaintext {
|
|
t.Fatal("credentials stored as plaintext on disk; expected AES-GCM ciphertext")
|
|
}
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_EmptyKeyPassThrough(t *testing.T) {
|
|
ps, db, tenantID := newTestSQLiteBitrixPortalStore(t, "") // empty key — no encryption
|
|
ctx := context.Background()
|
|
|
|
plaintext := `{"client_id":"pt"}`
|
|
if err := ps.Create(ctx, &store.BitrixPortalData{
|
|
TenantID: tenantID,
|
|
Name: "pt",
|
|
Domain: "pt.bitrix24.com",
|
|
Credentials: []byte(plaintext),
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
var raw []byte
|
|
if err := db.QueryRowContext(ctx,
|
|
`SELECT credentials FROM bitrix_portals WHERE tenant_id = ? AND name = ?`,
|
|
tenantID.String(), "pt",
|
|
).Scan(&raw); err != nil {
|
|
t.Fatalf("raw query: %v", err)
|
|
}
|
|
if string(raw) != plaintext {
|
|
t.Fatalf("empty-key mode should pass-through; got %q", raw)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_UpdateCredentialsAndState(t *testing.T) {
|
|
ps, _, tenantID := newTestSQLiteBitrixPortalStore(t, bitrixTestEncKey)
|
|
ctx := context.Background()
|
|
|
|
if err := ps.Create(ctx, &store.BitrixPortalData{
|
|
TenantID: tenantID,
|
|
Name: "u",
|
|
Domain: "u.bitrix24.com",
|
|
Credentials: []byte(`{"v":1}`),
|
|
State: []byte(`{"s":1}`),
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
if err := ps.UpdateCredentials(ctx, tenantID, "u", []byte(`{"v":2}`)); err != nil {
|
|
t.Fatalf("UpdateCredentials: %v", err)
|
|
}
|
|
if err := ps.UpdateState(ctx, tenantID, "u", []byte(`{"s":2}`)); err != nil {
|
|
t.Fatalf("UpdateState: %v", err)
|
|
}
|
|
|
|
got, err := ps.GetByName(ctx, tenantID, "u")
|
|
if err != nil {
|
|
t.Fatalf("GetByName: %v", err)
|
|
}
|
|
if string(got.Credentials) != `{"v":2}` {
|
|
t.Fatalf("credentials not updated: %q", got.Credentials)
|
|
}
|
|
if string(got.State) != `{"s":2}` {
|
|
t.Fatalf("state not updated: %q", got.State)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_ListByTenantAndAll(t *testing.T) {
|
|
ps, db, tenantA := newTestSQLiteBitrixPortalStore(t, bitrixTestEncKey)
|
|
ctx := context.Background()
|
|
|
|
// Second tenant for isolation check.
|
|
tenantB := store.GenNewID()
|
|
if _, err := db.Exec(
|
|
`INSERT INTO tenants (id, name, slug, status, settings, created_at, updated_at)
|
|
VALUES (?, 'tenant-b', 'tenant-b', 'active', '{}', datetime('now'), datetime('now'))`,
|
|
tenantB,
|
|
); err != nil {
|
|
t.Fatalf("insert tenant B: %v", err)
|
|
}
|
|
|
|
for _, rec := range []struct {
|
|
tid uuid.UUID
|
|
name string
|
|
}{
|
|
{tenantA, "alpha"},
|
|
{tenantA, "beta"},
|
|
{tenantB, "gamma"},
|
|
} {
|
|
if err := ps.Create(ctx, &store.BitrixPortalData{
|
|
TenantID: rec.tid,
|
|
Name: rec.name,
|
|
Domain: rec.name + ".bitrix24.com",
|
|
Credentials: []byte(`{}`),
|
|
}); err != nil {
|
|
t.Fatalf("Create %s: %v", rec.name, err)
|
|
}
|
|
}
|
|
|
|
listA, err := ps.ListByTenant(ctx, tenantA)
|
|
if err != nil {
|
|
t.Fatalf("ListByTenant A: %v", err)
|
|
}
|
|
if len(listA) != 2 {
|
|
t.Fatalf("expected 2 portals for tenant A, got %d", len(listA))
|
|
}
|
|
// Sorted by name.
|
|
if listA[0].Name != "alpha" || listA[1].Name != "beta" {
|
|
t.Fatalf("unexpected order: %s, %s", listA[0].Name, listA[1].Name)
|
|
}
|
|
|
|
// Tenant isolation — B must not see A's rows.
|
|
listB, err := ps.ListByTenant(ctx, tenantB)
|
|
if err != nil {
|
|
t.Fatalf("ListByTenant B: %v", err)
|
|
}
|
|
if len(listB) != 1 || listB[0].Name != "gamma" {
|
|
t.Fatalf("tenant isolation broken; B sees %d rows", len(listB))
|
|
}
|
|
|
|
all, err := ps.ListAllForLoader(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListAllForLoader: %v", err)
|
|
}
|
|
if len(all) != 3 {
|
|
t.Fatalf("expected 3 rows across tenants, got %d", len(all))
|
|
}
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_Delete(t *testing.T) {
|
|
ps, _, tenantID := newTestSQLiteBitrixPortalStore(t, bitrixTestEncKey)
|
|
ctx := context.Background()
|
|
|
|
if err := ps.Create(ctx, &store.BitrixPortalData{
|
|
TenantID: tenantID,
|
|
Name: "gone",
|
|
Domain: "gone.bitrix24.com",
|
|
Credentials: []byte(`{}`),
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
if err := ps.Delete(ctx, tenantID, "gone"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
|
|
_, err := ps.GetByName(ctx, tenantID, "gone")
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("expected sql.ErrNoRows after delete, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteBitrixPortalStore_NilGuards(t *testing.T) {
|
|
ps, _, tenantID := newTestSQLiteBitrixPortalStore(t, bitrixTestEncKey)
|
|
ctx := context.Background()
|
|
|
|
if err := ps.Create(ctx, nil); err == nil {
|
|
t.Fatal("expected error on nil portal")
|
|
}
|
|
if err := ps.Create(ctx, &store.BitrixPortalData{Name: "x", Domain: "x"}); err == nil {
|
|
t.Fatal("expected error on nil tenant_id")
|
|
}
|
|
if err := ps.Create(ctx, &store.BitrixPortalData{TenantID: tenantID}); err == nil {
|
|
t.Fatal("expected error on empty name/domain")
|
|
}
|
|
if err := ps.UpdateCredentials(ctx, uuid.Nil, "x", []byte("v")); err == nil {
|
|
t.Fatal("expected error on nil tenant_id UpdateCredentials")
|
|
}
|
|
if err := ps.UpdateState(ctx, uuid.Nil, "x", []byte("v")); err == nil {
|
|
t.Fatal("expected error on nil tenant_id UpdateState")
|
|
}
|
|
if err := ps.Delete(ctx, uuid.Nil, "x"); err == nil {
|
|
t.Fatal("expected error on nil tenant_id Delete")
|
|
}
|
|
if _, err := ps.GetByName(ctx, uuid.Nil, "x"); err == nil {
|
|
t.Fatal("expected error on nil tenant_id GetByName")
|
|
}
|
|
}
|