before merge to main
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -472,3 +474,280 @@ func TestLineDbUniqueFieldsNoUniqueFields(t *testing.T) {
|
||||
t.Fatalf("Expected 2 records when no unique fields, got %d", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLineDbEncode проверяет шифрование записей при Encode
|
||||
func TestLineDbEncode(t *testing.T) {
|
||||
os.RemoveAll("./testdata")
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "secret",
|
||||
AllocSize: 512,
|
||||
Encode: true,
|
||||
EncodeKey: "test-secret-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
db := linedb.NewLineDb(nil)
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
defer func() { db.Close(); os.RemoveAll("./testdata") }()
|
||||
|
||||
opts := linedb.LineDbAdapterOptions{}
|
||||
if err := db.Insert(map[string]any{"name": "secret", "value": 42}, "secret", opts); err != nil {
|
||||
t.Fatalf("Insert failed: %v", err)
|
||||
}
|
||||
|
||||
all, err := db.Read("secret", opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Read failed: %v", err)
|
||||
}
|
||||
if len(all) != 1 {
|
||||
t.Fatalf("Expected 1 record, got %d", len(all))
|
||||
}
|
||||
r := all[0].(map[string]any)
|
||||
if r["name"] != "secret" || r["value"] != float64(42) {
|
||||
t.Fatalf("Expected name=secret value=42, got %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLineDbAllocSizeOverflow проверяет ошибку при записи, превышающей allocSize-1
|
||||
func TestLineDbAllocSizeOverflow(t *testing.T) {
|
||||
os.RemoveAll("./testdata")
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{CollectionName: "tiny", AllocSize: 64},
|
||||
},
|
||||
}
|
||||
db := linedb.NewLineDb(nil)
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
defer func() { db.Close(); os.RemoveAll("./testdata") }()
|
||||
|
||||
opts := linedb.LineDbAdapterOptions{}
|
||||
// Короткая запись — поместится (allocSize-1=63)
|
||||
if err := db.Insert(map[string]any{"x": "short"}, "tiny", opts); err != nil {
|
||||
t.Fatalf("Insert short failed: %v", err)
|
||||
}
|
||||
// Длинная — не поместится (JSON > 63 символов)
|
||||
longStr := strings.Repeat("a", 80)
|
||||
err := db.Insert(map[string]any{"x": longStr}, "tiny", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for record exceeding allocSize-1")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Fatalf("Expected 'exceeds' in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLineDbInitNormalizeExistingFile проверяет нормализацию при Init существующего файла
|
||||
func TestLineDbInitNormalizeExistingFile(t *testing.T) {
|
||||
os.RemoveAll("./testdata")
|
||||
os.MkdirAll("./testdata", 0755)
|
||||
// Файл с записями разной длины
|
||||
content := `{"id":1,"a":"x"}
|
||||
{"id":2,"a":"yy"}
|
||||
{"id":3,"a":"zzz"}`
|
||||
if err := os.WriteFile("./testdata/norm.jsonl", []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Write file: %v", err)
|
||||
}
|
||||
defer os.RemoveAll("./testdata")
|
||||
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{CollectionName: "norm", AllocSize: 100},
|
||||
},
|
||||
}
|
||||
db := linedb.NewLineDb(nil)
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
all, err := db.Read("norm", linedb.LineDbAdapterOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Read failed: %v", err)
|
||||
}
|
||||
if len(all) != 3 {
|
||||
t.Fatalf("Expected 3 records, got %d", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// setupCollectionWithFieldConstraints создаёт БД с FieldConstraints (required/unique)
|
||||
func setupCollectionWithFieldConstraints(t *testing.T, constraints []linedb.FieldConstraint) (*linedb.LineDb, func()) {
|
||||
t.Helper()
|
||||
os.RemoveAll("./testdata")
|
||||
initOptions := &linedb.LineDbInitOptions{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Minute,
|
||||
DBFolder: "./testdata",
|
||||
Collections: []linedb.JSONLFileOptions{
|
||||
{
|
||||
CollectionName: "items",
|
||||
AllocSize: 256,
|
||||
FieldConstraints: constraints,
|
||||
},
|
||||
},
|
||||
}
|
||||
db := linedb.NewLineDb(nil)
|
||||
if err := db.Init(false, initOptions); err != nil {
|
||||
t.Fatalf("Failed to init database: %v", err)
|
||||
}
|
||||
return db, func() { db.Close(); os.RemoveAll("./testdata") }
|
||||
}
|
||||
|
||||
// TestLineDbRequiredFieldsInsert проверяет, что required поля обязательны при Insert
|
||||
func TestLineDbRequiredFieldsInsert(t *testing.T) {
|
||||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||||
{Name: "email", Required: true},
|
||||
{Name: "name", Required: true},
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
opts := linedb.LineDbAdapterOptions{}
|
||||
|
||||
// Успех — оба поля заполнены
|
||||
if err := db.Insert(map[string]any{"name": "alice", "email": "a@test.com"}, "items", opts); err != nil {
|
||||
t.Fatalf("Insert with required fields failed: %v", err)
|
||||
}
|
||||
|
||||
// Ошибка — отсутствует email (AbsentKey)
|
||||
err := db.Insert(map[string]any{"name": "bob"}, "items", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for missing required field email")
|
||||
} else {
|
||||
log.Println("Error: ", err)
|
||||
}
|
||||
|
||||
// Ошибка — email = nil
|
||||
err = db.Insert(map[string]any{"name": "bob", "email": nil}, "items", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for nil required field email")
|
||||
} else {
|
||||
log.Println("Error: ", err)
|
||||
}
|
||||
|
||||
// Ошибка — email = ""
|
||||
err = db.Insert(map[string]any{"name": "bob", "email": ""}, "items", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for empty string in required field email")
|
||||
} else {
|
||||
log.Println("Error: ", err)
|
||||
}
|
||||
|
||||
all, _ := db.Read("items", opts)
|
||||
if len(all) != 1 {
|
||||
t.Fatalf("Expected 1 record, got %d", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLineDbRequiredFieldsEmptyMode проверяет разные режимы интерпретации пустоты
|
||||
func TestLineDbRequiredFieldsEmptyMode(t *testing.T) {
|
||||
// Режим: только отсутствие ключа — nil и "" допустимы
|
||||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||||
{Name: "title", Required: true, EmptyMode: linedb.EmptyModeAbsentKey},
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
opts := linedb.LineDbAdapterOptions{}
|
||||
|
||||
// nil — допустимо (только AbsentKey проверяется)
|
||||
if err := db.Insert(map[string]any{"title": nil}, "items", opts); err != nil {
|
||||
t.Fatalf("Insert with nil (AbsentKey only) failed: %v", err)
|
||||
}
|
||||
// "" — допустимо
|
||||
if err := db.Insert(map[string]any{"title": ""}, "items", opts); err != nil {
|
||||
t.Fatalf("Insert with empty string (AbsentKey only) failed: %v", err)
|
||||
}
|
||||
// Отсутствие ключа — ошибка (поле не в записи)
|
||||
err := db.Insert(map[string]any{"other": "x"}, "items", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for absent required key")
|
||||
}
|
||||
|
||||
// Режим: только nil — AbsentKey и "" допустимы? Нет: AbsentKey при отсутствии — пусто.
|
||||
// При EmptyModeNil: пусто только когда v==nil. AbsentKey при отсутствии ключа: exists=false.
|
||||
// isEmptyByMode: if mode&EmptyModeAbsentKey && !exists return true. So if we use only EmptyModeNil,
|
||||
// AbsentKey is not in the mask, so we don't return true for !exists. Good.
|
||||
db2, cleanup2 := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||||
{Name: "flag", Required: true, EmptyMode: linedb.EmptyModeNil},
|
||||
})
|
||||
defer cleanup2()
|
||||
|
||||
// Ключ отсутствует — при EmptyModeNil мы не проверяем AbsentKey, так что !exists не даёт return true.
|
||||
// isEmptyByMode: проверяет mode&EmptyModeAbsentKey - если нет, то !exists не приводит к true.
|
||||
// Потом mode&EmptyModeNil && v==nil - но v при !exists будет нулевым при v, exists := m[key]. v = nil, exists = false.
|
||||
// Но мы заходим в v, exists := m[key]. Если !exists, v будет nil (zero value). So v==nil is true.
|
||||
// And mode&EmptyModeNil - if we have only Nil, we'd return true. So absent key would still be "empty" if we access v.
|
||||
// Actually no - we first check mode&EmptyModeAbsentKey && !exists - if that's not in mode, we skip.
|
||||
// Then mode&EmptyModeNil && v==nil - for absent key, v is nil. So we'd return true. So absent key + Nil mode would still fail.
|
||||
// The logic is: we return true (empty) if ANY of the conditions matching the mode are true. So:
|
||||
// - AbsentKey + !exists -> empty
|
||||
// - Nil + v==nil -> empty
|
||||
// - ZeroValue + ("" or 0) -> empty
|
||||
// For EmptyModeNil only: we don't check AbsentKey. We check Nil - and for absent key, v is nil (zero value from map lookup). So we'd return true. So absent key would still be empty. Good.
|
||||
// For EmptyModeZeroValue only: we'd only check ZeroValue. Absent key: v is nil, we don't have a case for nil in the switch (we'd fall through and return false). So absent key would NOT be empty. And nil would not be empty. Only "" and 0 would be.
|
||||
// Let me add a test for EmptyModeZeroValue: required field, value 0 should fail, value "" should fail, but absent key... would not fail? That might be surprising. Let me document it.
|
||||
if err := db2.Insert(map[string]any{"flag": false}, "items", opts); err != nil {
|
||||
t.Fatalf("Insert with false (bool, Nil mode) failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLineDbRequiredFieldsUpdate проверяет, что Update не может установить пустое в required
|
||||
func TestLineDbRequiredFieldsUpdate(t *testing.T) {
|
||||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||||
{Name: "email", Required: true},
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
opts := linedb.LineDbAdapterOptions{}
|
||||
|
||||
if err := db.Insert(map[string]any{"name": "a", "email": "a@test.com"}, "items", opts); err != nil {
|
||||
t.Fatalf("Insert failed: %v", err)
|
||||
}
|
||||
|
||||
// Ошибка — пытаемся установить email в ""
|
||||
_, err := db.Update(map[string]any{"email": ""}, "items", map[string]any{"email": "a@test.com"}, opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when setting required field to empty")
|
||||
}
|
||||
|
||||
// Успех — обновляем другое поле
|
||||
_, err = db.Update(map[string]any{"name": "alice"}, "items", map[string]any{"email": "a@test.com"}, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Update non-required field failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLineDbRequiredAndUniqueCombined проверяет комбинацию required и unique
|
||||
func TestLineDbRequiredAndUniqueCombined(t *testing.T) {
|
||||
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
|
||||
{Name: "email", Required: true, Unique: true},
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
opts := linedb.LineDbAdapterOptions{}
|
||||
|
||||
if err := db.Insert(map[string]any{"email": "x@test.com"}, "items", opts); err != nil {
|
||||
t.Fatalf("Insert failed: %v", err)
|
||||
}
|
||||
err := db.Insert(map[string]any{"email": "x@test.com"}, "items", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected unique violation")
|
||||
}
|
||||
err = db.Insert(map[string]any{"email": ""}, "items", opts)
|
||||
if err == nil {
|
||||
t.Fatal("Expected required violation for empty email")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user