Files
Duy /zuey/andGitHub ef829c1ab4 fix(bitrix24): resolve PR 8-10 stack findings
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.
2026-05-22 20:53:01 +07:00

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")
}
}